Skip to content

Commit 3548825

Browse files
authored
Merge pull request #31 from rubakas/refactor/model-catalog-and-mission-trim
#17 refactor: self-updating model catalog, then trim agent-notes to its mission
2 parents b6d9a33 + 7af1310 commit 3548825

144 files changed

Lines changed: 3852 additions & 5816 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/validate.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
runs-on: ubuntu-latest
1111
strategy:
1212
matrix:
13-
python-version: ["3.10", "3.12"]
13+
python-version: ["3.11", "3.13"]
1414
steps:
1515
- uses: actions/checkout@v4
1616
- name: Install uv

README.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ agent-notes doctor
1212

1313
**What's Included**
1414
- 19 specialized AI subagents (Opus reasons, Sonnet executes, Haiku explores)
15-
- 50+ on-demand skills (Rails, Docker, Git, Kamal, Process)
15+
- 25 on-demand skills (Rails, Docker, Git, Kamal, Process)
1616
- Global rules and guardrails (including a ban on AI self-attribution in commits/PRs)
1717
- Agent memory with 3 storage options (Local, Obsidian, Wiki)
1818
- Configuration for Claude Code, OpenCode, and GitHub Copilot
@@ -262,7 +262,7 @@ agent-notes memory lint # health-check
262262
<details>
263263
<summary>Skills</summary>
264264

265-
50+ on-demand knowledge modules across Rails, Docker, Kamal, Git, and Process. Run `agent-notes list skills` for the current list, or browse `agent_notes/data/skills/`.
265+
25 on-demand knowledge modules across Rails, Docker, Kamal, Git, and Process. Run `agent-notes list skills` for the current list, or browse `agent_notes/data/skills/`. For additional workflows (brainstorming, test-driven development, writing skills), see the complementary [Superpowers](https://github.com/obra/superpowers) plugin.
266266

267267
The session context hook auto-generates a skill index from SKILL.md frontmatter at install time, so agents always know what skills are available without loading full skill content. This keeps context overhead low while maintaining skill discoverability.
268268

@@ -279,14 +279,13 @@ Load the docker-compose skill for multi-service setup
279279
|---|---|---|
280280
| Misalignment | Claude starts building before the problem is resolved | `/grill-me`, `/grilling`, `/grill-with-docs` |
281281
| Vague scope | Work starts without a spec or ticketed breakdown | `/to-spec`, `/to-tickets`, `/wayfinder`, `/triage` |
282-
| Broken code | Claude codes without a feedback loop or evidence trail | `/tdd`, `/diagnosing-bugs` |
282+
| Broken code | Claude codes without a feedback loop or evidence trail | `/diagnosing-bugs` |
283283
| Ball of mud | Architecture drifts; modules grow shallow and tangled | `/improve-codebase-architecture`, `/codebase-design`, `/domain-modeling` |
284284

285285
**Skill descriptions:**
286286

287287
- `/grill-me`, `/grilling` — Interview the user relentlessly until the plan is fully resolved before touching code
288288
- `/grill-with-docs` — Same, but cross-references CONTEXT.md and ADRs and updates them inline
289-
- `/tdd` — RED-GREEN-REFACTOR with tracer-bullet vertical slices; integration-first testing
290289
- `/diagnosing-bugs` — Diagnosis loop for hard bugs and performance regressions (build a feedback loop first)
291290
- `/improve-codebase-architecture` — Scan for deepening opportunities, present a visual HTML report, then grill the chosen one
292291
- `/codebase-design` — Shared vocabulary for designing deep modules and choosing where seams go
@@ -299,7 +298,6 @@ Load the docker-compose skill for multi-service setup
299298
- `/research` — Investigate a question against primary sources and capture findings as Markdown
300299
- `/prototype` — Throwaway prototypes: terminal app for logic or UI variations for visual
301300
- `/setup-agent-tracker` — One-time per-repo setup of the issue tracker, triage labels, and domain doc layout
302-
- `/writing-great-skills` — Reference for writing and editing skills well
303301

304302
</details>
305303

@@ -399,7 +397,7 @@ agent-notes config provider <name> # check if configured (without exposing
399397

400398
### Building and testing
401399

402-
Python 3.10+ required. Create an isolated environment and run tests:
400+
Python 3.11+ required. Create an isolated environment and run tests:
403401

404402
```bash
405403
python -m venv .venv && source .venv/bin/activate
@@ -492,7 +490,7 @@ See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md), [docs/ADD_CLI.md](docs/ADD_CLI
492490
## Inspired by
493491

494492
- [Andrej Karpathy's LLM Wiki](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) — The wiki memory backend implements his compile-once, query-forever knowledge pattern with structured page types and three core operations (ingest, query, lint)
495-
- [Matt Pocock's skills repo](https://github.com/mattpocock/skills) — Skill format (SKILL.md per directory), failure-mode table (misalignment, broken code, architectural degradation), and a set of engineering/productivity skills vendored from the upstream repo (tdd, grill-me, grilling, grill-with-docs, diagnosing-bugs, improve-codebase-architecture, codebase-design, domain-modeling, prototype, handoff, research, to-spec, to-tickets, triage, wayfinder, writing-great-skills). Last synced from upstream commit [`391a270`](https://github.com/mattpocock/skills/commit/391a2701dd948f94f56a39f7533f8eea9a859c87) (2026-07-10), reviewed for prompt-injection before import.
493+
- [Matt Pocock's skills repo](https://github.com/mattpocock/skills) — Skill format (SKILL.md per directory), failure-mode table (misalignment, broken code, architectural degradation), and a set of engineering/productivity skills vendored from the upstream repo (grill-me, grilling, grill-with-docs, diagnosing-bugs, improve-codebase-architecture, codebase-design, domain-modeling, prototype, handoff, research, to-spec, to-tickets, triage, wayfinder). Last synced from upstream commit [`391a270`](https://github.com/mattpocock/skills/commit/391a2701dd948f94f56a39f7533f8eea9a859c87) (2026-07-10), reviewed for prompt-injection before import.
496494

497495
## License
498496

agent_notes/cli.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ def main():
283283
# memory
284284
p_memory = subparsers.add_parser("memory", help="Manage agent memory")
285285
p_memory.add_argument("action", nargs="?", default="list",
286-
choices=["init", "list", "vault", "index", "add", "size", "show", "reset", "export", "import", "ingest", "query", "lint"],
286+
choices=["init", "list", "vault", "index", "add", "size", "show", "reset", "export", "import"],
287287
help="Memory action")
288288
p_memory.add_argument("name", nargs="?", help="Agent name / note title (for show/reset/add)")
289289
p_memory.add_argument("extra", nargs="*", help="Additional args (for add: body [type] [agent] [project])")
@@ -299,6 +299,16 @@ def main():
299299
p_cost_report.add_argument("--since", help="Only include messages at or after this UTC datetime (ISO 8601)")
300300
p_cost_report.add_argument("--session", help="Session ID to report on (Claude Code only)")
301301

302+
# models
303+
p_models = subparsers.add_parser("models", help="Manage the model catalog")
304+
p_models_sub = p_models.add_subparsers(dest="subaction", metavar="")
305+
p_refresh = p_models_sub.add_parser("refresh", help="Fetch latest models from providers and update cache")
306+
p_refresh.add_argument("--provider", choices=["anthropic", "openai"],
307+
help="Limit to one provider")
308+
p_refresh.add_argument("--dry-run", action="store_true", dest="dry_run",
309+
help="Print diff but do not write cache")
310+
p_models_sub.add_parser("freeze", help="Promote cache to seed.json for committing")
311+
302312
# config
303313
p_config = subparsers.add_parser("config", help="Reconfigure role/agent/model/memory/skill assignments after install")
304314
p_config.add_argument("action", nargs="?", default="wizard",
@@ -364,27 +374,30 @@ def main():
364374
elif args.command == "memory":
365375
from .commands.memory import memory
366376
memory(args.action, args.name, getattr(args, "extra", None), description=getattr(args, "description", ""))
377+
elif args.command == "models":
378+
subaction = getattr(args, "subaction", None)
379+
if subaction == "refresh":
380+
from .commands.models import refresh
381+
refresh(
382+
provider=getattr(args, "provider", None),
383+
dry_run=getattr(args, "dry_run", False),
384+
)
385+
elif subaction == "freeze":
386+
from .commands.models import freeze
387+
freeze()
388+
else:
389+
parser.parse_args(["models", "--help"])
367390
elif args.command == "config":
368391
from .commands.config import config
369392
config(action=args.action, args=getattr(args, "extra", None) or [], cli_filter=args.cli)
370393
elif args.command == "hook":
371394
from .commands.hook import hook
372395
hook(args.subaction)
373396
elif args.command == "cost-report":
374-
# Rebuild sys.argv slice so cost_report.main() can parse it normally
375-
argv = []
376-
if args.since:
377-
argv += ["--since", args.since]
378-
if args.session:
379-
argv += ["--session", args.session]
380-
import sys
381-
old_argv = sys.argv
382-
sys.argv = ["agent-notes cost-report"] + argv
383-
try:
384-
from .scripts.cost_report import main as _cost_report_main
385-
sys.exit(_cost_report_main())
386-
finally:
387-
sys.argv = old_argv
397+
from .cost.cost_report import main as _cost_report_main, _parse_since
398+
since = _parse_since(args.since) if args.since else None
399+
session_id = args.session or None
400+
sys.exit(_cost_report_main(since=since, session_id=session_id))
388401

389402
if __name__ == "__main__":
390403
main()

agent_notes/commands/build.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def build(role_models=None, role_efforts=None, scope='global', project_path=None
129129

130130
# Read configuration
131131
try:
132-
agents_config, tiers = load_agents_config()
132+
agents_config = load_agents_config()
133133
except FileNotFoundError as e:
134134
print(f"Error: {e}")
135135
return
@@ -139,7 +139,7 @@ def build(role_models=None, role_efforts=None, scope='global', project_path=None
139139

140140
# Generate agent files (state=None is backward compatible)
141141
print("Generating agent files...")
142-
agent_files = generate_agent_files(agents_config, tiers, state=state,
142+
agent_files = generate_agent_files(agents_config, state=state,
143143
scope=scope, project_path=project_path,
144144
role_models=role_models, role_efforts=role_efforts,
145145
profile_label=profile_label)

agent_notes/commands/config.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from pathlib import Path
88
from typing import Optional
99

10-
from ..constants import DEFAULT_VAULT_DIR, DEFAULT_VAULT_NAME, Wiki, Obsidian
10+
from ..constants import DEFAULT_VAULT_DIR, DEFAULT_VAULT_NAME, Obsidian
1111

1212

1313
def _load_state():
@@ -277,8 +277,6 @@ def show(state=None) -> None:
277277
mem = state.memory
278278
if mem.backend == "obsidian":
279279
mem_label = f"Obsidian session ({mem.path})" if mem.path else "Obsidian session"
280-
elif mem.backend == "wiki":
281-
mem_label = f"Obsidian wiki ({mem.path})" if mem.path else "Obsidian wiki"
282280
elif mem.backend == "local":
283281
mem_label = "Local markdown"
284282
else:
@@ -483,8 +481,7 @@ def _wizard_memory(state, before: str) -> bool:
483481
storage_options = {
484482
"1": ("local", "default - Claude Code built-in md files"),
485483
"2": ("obsidian", "Obsidian - session"),
486-
"3": ("wiki", "Obsidian - brain"),
487-
"4": ("none", "None"),
484+
"3": ("none", "None"),
488485
}
489486

490487
print("\nMemory storage options:")
@@ -499,8 +496,8 @@ def _wizard_memory(state, before: str) -> bool:
499496
backend, label = storage_options[choice]
500497
path = ""
501498

502-
if backend in ("obsidian", "wiki"):
503-
subfolder = Obsidian.SUBFOLDER if backend == "obsidian" else Wiki.SUBFOLDER
499+
if backend == "obsidian":
500+
subfolder = Obsidian.SUBFOLDER
504501
default_vault = str(Path.home() / DEFAULT_VAULT_DIR / DEFAULT_VAULT_NAME)
505502
print(f" Folder name: {subfolder}")
506503
print(" Press Tab to autocomplete paths")

agent_notes/commands/doctor.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
# docker-compose → group: docker). There is no single source-of-truth
5353
# constant elsewhere in the codebase, so the full vocabulary is listed here.
5454
_VALID_GROUPS = {"process", "domain", "rails", "docker", "kamal"}
55-
_VALID_MEMORY_BACKENDS = {"obsidian", "wiki", "local", "none"}
55+
_VALID_MEMORY_BACKENDS = {"obsidian", "local", "none"}
5656

5757

5858
def check_skill_frontmatter(scope: str, issues: list, fix_actions: list, profile_label: str = "") -> None:
@@ -61,7 +61,7 @@ def check_skill_frontmatter(scope: str, issues: list, fix_actions: list, profile
6161
Checks every skill for:
6262
- non-empty name and description
6363
- group, if present, is in {"process", "domain"}
64-
- requires_memory tokens, if present, are each in {"obsidian", "wiki", "local", "none"}
64+
- requires_memory tokens, if present, are each in {"obsidian", "local", "none"}
6565
6666
Violations are printed as advisories and do NOT affect issues/fix_actions or exit code.
6767
"""
@@ -118,7 +118,7 @@ def _check_session_hook(scope: str, issues: list) -> None:
118118
# Memory bridge check on the default backend only
119119
settings_path, _, _ = _session_hook_paths(claude_backend, scope)
120120
from ..constants import Hooks
121-
if state and state.memory.backend in ("obsidian", "wiki"):
121+
if state and state.memory.backend == "obsidian":
122122
if settings_path.exists() and not has_hook(settings_path, "SessionStart", Hooks.MEMORY_BRIDGE):
123123
issues.append(Issue(
124124
"missing_hook",
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
"""Hook command — Claude Code hook integrations.
2+
3+
Entry-point functions (_guard_credentials, _memory_bridge, _precompact_memory_bridge)
4+
are defined here so that ``patch("agent_notes.commands.hook.<name>", ...)`` targets
5+
in tests resolve to this module's namespace and intercept calls correctly.
6+
7+
Implementation logic is split across focused submodules:
8+
_guard.py — credential detection helpers + evaluate_credential_access
9+
_session.py — _session_discover
10+
_memory.py — _load_memory_index (shared renderer)
11+
"""
12+
13+
from __future__ import annotations
14+
15+
import json
16+
import sys
17+
18+
# Re-export everything tests import from this package
19+
from ._guard import (
20+
evaluate_credential_access,
21+
_is_credential_path,
22+
_bash_reads_credential,
23+
_keyword_in_segment,
24+
_deny_payload,
25+
_is_template_file,
26+
_is_path_shaped,
27+
_first_effective_command,
28+
_strip_token_punctuation,
29+
_command_contains_credential_path,
30+
_TEMPLATE_MARKERS,
31+
_HARD_SECRET_EXTS,
32+
_SOURCE_EXTS,
33+
_CREDENTIAL_BASENAME_PATTERNS,
34+
_SEG_SEPS,
35+
_TOKEN_SPLIT,
36+
_CREDENTIAL_SEGMENT_KEYWORDS,
37+
_SAFE_EXISTENCE_COMMANDS,
38+
_COMMAND_PREFIXES,
39+
)
40+
from ._session import _session_discover
41+
from ._memory import _load_memory_index
42+
43+
44+
# ---------------------------------------------------------------------------
45+
# Dispatch
46+
# ---------------------------------------------------------------------------
47+
48+
def hook(subaction: str) -> None:
49+
"""Handle hook subactions."""
50+
if subaction == "memory-bridge":
51+
_memory_bridge()
52+
elif subaction == "precompact-memory-bridge":
53+
_precompact_memory_bridge()
54+
elif subaction == "session-discover":
55+
_session_discover()
56+
elif subaction == "guard-credentials":
57+
_guard_credentials()
58+
59+
60+
# ---------------------------------------------------------------------------
61+
# Entry-point functions that call module-level names (kept here so that
62+
# patch("agent_notes.commands.hook.<dep>", ...) intercepts their calls).
63+
# ---------------------------------------------------------------------------
64+
65+
def _guard_credentials() -> None:
66+
"""PreToolUse hook that denies attempts to read credential files.
67+
68+
Reads the PreToolUse JSON payload from stdin, calls evaluate_credential_access,
69+
and prints a deny or allow response. Always exits 0 so Claude Code parses stdout.
70+
71+
Fail-closed for recognized tool types: if stdin parsed and tool_name is a
72+
guarded tool (Read/Bash/Grep), any downstream error emits a static deny.
73+
Only a total stdin/JSON parse failure fails open (writes to stderr).
74+
"""
75+
raw = ""
76+
try:
77+
raw = sys.stdin.read()
78+
payload = json.loads(raw)
79+
except Exception as exc:
80+
# Total infrastructure failure — cannot parse stdin at all; fail open
81+
# with a stderr note so operators can diagnose.
82+
print(f"credential-guard: stdin parse error ({exc})", file=sys.stderr)
83+
return
84+
85+
tool_name = payload.get("tool_name", "")
86+
tool_input = payload.get("tool_input", {})
87+
88+
_GUARDED_TOOLS = {"Read", "Bash", "Grep"}
89+
90+
if tool_name in _GUARDED_TOOLS:
91+
try:
92+
decision = evaluate_credential_access(tool_name, tool_input)
93+
if decision is not None:
94+
print(json.dumps(decision))
95+
except Exception:
96+
# Evaluation error on a guarded tool — fail closed
97+
print(json.dumps(_deny_payload(
98+
"Credential guard: evaluation error — denying as a precaution; "
99+
"remove the guard hook from settings.json to override."
100+
)))
101+
else:
102+
# Non-guarded tool: pass through (hook matcher already scopes to Read|Bash|Grep,
103+
# but defensive check here in case Claude Code invokes us for other tools)
104+
try:
105+
decision = evaluate_credential_access(tool_name, tool_input)
106+
if decision is not None:
107+
print(json.dumps(decision))
108+
except Exception:
109+
pass # Non-guarded tool errors fail open
110+
111+
112+
def _memory_bridge() -> None:
113+
"""SessionStart hook that prints the agent-notes memory index.
114+
115+
Unconditionally loads and prints the memory index so it is visible in
116+
context at the start of every Claude Code session.
117+
"""
118+
content = _load_memory_index()
119+
if content is None:
120+
return
121+
print("<!-- agent-notes memory index (auto-loaded) -->")
122+
print(content)
123+
124+
125+
def _precompact_memory_bridge() -> None:
126+
"""PreCompact hook that re-emits the memory index before context compaction.
127+
128+
When Claude Code compacts a long conversation the SessionStart context
129+
injected by memory-bridge can be summarised away, losing the pointer to
130+
durable memory. This hook fires immediately before compaction and returns
131+
the index via the `additionalContext` field in the hook JSON output schema
132+
(see docs/CLI_CAPABILITIES.md §Hooks → "JSON output schema"). Claude Code
133+
merges `additionalContext` into the compacted context, keeping the memory
134+
pointer alive.
135+
136+
Contract: exit 0, emit a single JSON object to stdout:
137+
{"additionalContext": "<header>\\n<index content>"}
138+
If the memory index is unavailable (no backend configured, no index file)
139+
the hook exits silently with no output so compaction proceeds normally.
140+
"""
141+
content = _load_memory_index()
142+
if content is None:
143+
return
144+
payload = {
145+
"additionalContext": "<!-- agent-notes memory index (auto-loaded) -->\n" + content,
146+
}
147+
print(json.dumps(payload))

0 commit comments

Comments
 (0)