@evertonferreira96/agents 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.claude/agents/api.md +2 -3
  2. package/.claude/agents/app.md +2 -2
  3. package/.claude/agents/product.md +18 -7
  4. package/.claude/agents/quick.md +1 -1
  5. package/.claude/agents/ui.md +1 -1
  6. package/.claude/agents/verifier.md +8 -4
  7. package/.claude/agents/web.md +2 -2
  8. package/.claude/hooks/orchestrator-guard.sh +3 -1
  9. package/.claude/hooks/post-compact.sh +20 -4
  10. package/.claude/hooks/stop-verify.sh +8 -3
  11. package/.claude/skills/arch-api/references/review.md +17 -0
  12. package/.claude/skills/arch-app/references/review.md +15 -0
  13. package/.claude/skills/arch-db/references/review.md +2 -0
  14. package/.claude/skills/arch-web/references/tests.md +15 -0
  15. package/.claude/skills/brief/SKILL.md +32 -3
  16. package/.claude/skills/domain-modeling/ADR-FORMAT.md +47 -0
  17. package/.claude/skills/domain-modeling/GLOSSARY-FORMAT.md +60 -0
  18. package/.claude/skills/domain-modeling/SKILL.md +74 -0
  19. package/.claude/skills/domain-modeling/agents/openai.yaml +3 -0
  20. package/.claude/skills/grill-with-docs/SKILL.md +10 -16
  21. package/.claude/skills/orchestrate/SKILL.md +19 -12
  22. package/.claude/skills/review/SKILL.md +3 -3
  23. package/.claude/skills/wayfinder/SKILL.md +146 -0
  24. package/.claude/skills/wayfinder/agents/openai.yaml +5 -0
  25. package/README.md +83 -11
  26. package/VERSION +1 -1
  27. package/bin/agents.mjs +46 -22
  28. package/bin/chew.mjs +53 -9
  29. package/docs/adr/README.md +3 -1
  30. package/docs/plans/README.md +38 -7
  31. package/package.json +1 -1
  32. package/scripts/bootstrap.mjs +112 -7
  33. package/scripts/check-catalog.mjs +4 -1
  34. package/skills-lock.json +18 -3
package/bin/agents.mjs CHANGED
@@ -3,43 +3,67 @@
3
3
  // agents init [dir] copy the config into the project (default: current directory); never overwrites
4
4
  // agents update [dir] refresh files the project did not edit, add new ones, delete untouched ones that left the config
5
5
  // agents check [dir] report missing, outdated, edited and obsolete files; exit 1 on drift
6
+ // agents print-agents-md print this version's AGENTS.md in its managed block (--raw: the file alone)
6
7
  // Flags: --force (update also overwrites files edited in the project, except CLAUDE.md, settings.local.json and agent memory),
7
- // --dry-run (init/update: show what would change, write nothing).
8
- import { resolve } from "node:path";
9
- import { DEFAULT_SRC, drift, install, readPackage, report } from "../scripts/bootstrap.mjs";
8
+ // --dry-run (init/update: show what would change, write nothing),
9
+ // --agents-md=keep|overwrite|append|print (a project AGENTS.md that differs; asked in a terminal, default keep).
10
+ import { readFileSync } from "node:fs";
11
+ import { join, resolve } from "node:path";
12
+ import { AGENTS_MD_CHOICES, DEFAULT_SRC, agentsBlock, drift, install, readPackage, report, settleAgentsMd } from "../scripts/bootstrap.mjs";
10
13
 
11
- const HELP = `Usage: agents <command> [dir] [--force] [--dry-run]
14
+ const HELP = `Usage: agents <command> [dir] [flags]
12
15
 
13
16
  Commands:
14
- init copy the config into the project (never overwrites existing files)
15
- update refresh untouched files, add new ones, delete untouched files that left the config
16
- check report missing, outdated, edited and obsolete files (exit 1 on drift)
17
+ init copy the config into the project (never overwrites existing files)
18
+ update refresh untouched files, add new ones, delete untouched files that left the config
19
+ check report missing, outdated, edited and obsolete files (exit 1 on drift)
20
+ print-agents-md print the config AGENTS.md inside its managed block (--raw: the file alone)
17
21
 
18
22
  Flags:
19
- --force update also overwrites files edited in the project
20
- (CLAUDE.md, .claude/settings.local.json and .claude/agent-memory/** are never touched)
21
- --dry-run show what init/update would change without writing
23
+ --force update also overwrites files edited in the project
24
+ (CLAUDE.md, .claude/settings.local.json and .claude/agent-memory/** are never touched)
25
+ --dry-run show what init/update would change without writing
26
+ --agents-md=<choice>
27
+ when the project already has its own AGENTS.md (asked in a terminal; default keep):
28
+ keep, overwrite, append (managed block refreshed by later updates) or print
22
29
 
23
30
  dir defaults to the current directory.`;
24
31
 
25
32
  const args = process.argv.slice(2);
26
- const flags = new Set(args.filter((a) => a.startsWith("-")));
27
- const [command, dir] = args.filter((a) => !a.startsWith("-"));
33
+ const flags = new Set(), opts = {}, pos = [];
34
+ for (let i = 0; i < args.length; i++) {
35
+ const a = args[i];
36
+ if (a === "--agents-md") opts["agents-md"] = args[++i] ?? "";
37
+ else if (a.startsWith("--") && a.includes("=")) opts[a.slice(2, a.indexOf("="))] = a.slice(a.indexOf("=") + 1);
38
+ else if (a.startsWith("-")) flags.add(a);
39
+ else pos.push(a);
40
+ }
41
+ const [command, dir] = pos;
28
42
  const pkg = readPackage(DEFAULT_SRC);
29
43
 
30
- if (flags.has("--version") || flags.has("-v")) { console.log(pkg.version); process.exit(0); }
31
- if (!command || command === "help" || flags.has("--help") || flags.has("-h")) { console.log(`${pkg.name} ${pkg.version}\n\n${HELP}`); process.exit(command ? 0 : 2); }
32
- if (!["init", "update", "check"].includes(command)) { console.error(`unknown command: ${command}\n\n${HELP}`); process.exit(2); }
44
+ async function main() {
45
+ if (flags.has("--version") || flags.has("-v")) { console.log(pkg.version); return 0; }
46
+ if (!command || command === "help" || flags.has("--help") || flags.has("-h")) { console.log(`${pkg.name} ${pkg.version}\n\n${HELP}`); return command ? 0 : 2; }
47
+ if (!["init", "update", "check", "print-agents-md"].includes(command)) { console.error(`unknown command: ${command}\n\n${HELP}`); return 2; }
48
+ const agentsMd = opts["agents-md"];
49
+ if (agentsMd !== undefined && !AGENTS_MD_CHOICES.includes(agentsMd)) { console.error(`--agents-md must be one of: ${AGENTS_MD_CHOICES.join(", ")}`); return 2; }
50
+
51
+ if (command === "print-agents-md") {
52
+ process.stdout.write(flags.has("--raw") ? readFileSync(join(DEFAULT_SRC, "AGENTS.md"), "utf8") : agentsBlock(DEFAULT_SRC));
53
+ return 0;
54
+ }
33
55
 
34
- try {
35
56
  const target = resolve(dir ?? process.cwd());
36
- const r = install({ target, mode: command, force: flags.has("--force"), dryRun: flags.has("--dry-run") });
57
+ const dryRun = flags.has("--dry-run");
58
+ const r = install({ target, mode: command, force: flags.has("--force"), dryRun });
37
59
  report(r);
38
- if (command === "init" && !flags.has("--dry-run")) {
60
+ if (command !== "check") {
61
+ await settleAgentsMd({ target, r, choice: agentsMd, dryRun, interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY) });
62
+ }
63
+ if (command === "init" && !dryRun) {
39
64
  console.log("\nnext: fill in purpose and commands in CLAUDE.md; optional: `npx impeccable install` (for /ui) and `/graphify .`");
40
65
  }
41
- process.exit(command === "check" && drift(r) ? 1 : 0);
42
- } catch (e) {
43
- console.error(e.message);
44
- process.exit(2);
66
+ return command === "check" && drift(r) ? 1 : 0;
45
67
  }
68
+
69
+ main().then((code) => { process.exitCode = code; }, (e) => { console.error(e.message); process.exitCode = 2; });
package/bin/chew.mjs CHANGED
@@ -3,8 +3,10 @@
3
3
  // chew agents setup:pull [dir] download the latest published config and install it (fresh project: everything;
4
4
  // configured project: refresh what was not edited there, add new files, drop obsolete ones)
5
5
  // chew agents setup:check [dir] compare the project with the latest published config; exit 1 on drift
6
+ // chew agents setup:print print the latest config AGENTS.md in its managed block (--raw: the file alone)
6
7
  // Flags: --force (also overwrite files edited in the project), --dry-run (show, write nothing),
7
- // --local (use the version installed with chew instead of downloading the latest).
8
+ // --local (use the version installed with chew instead of downloading the latest),
9
+ // --agents-md=keep|overwrite|append|print (a project AGENTS.md that differs; asked in a terminal, default keep).
8
10
  // The install runs with the engine of the downloaded version, so an old global chew still applies the newest setup.
9
11
  // Test hook: CHEW_TARBALL=<path to a .tgz from `npm pack`> replaces the registry download.
10
12
  import { spawnSync } from "node:child_process";
@@ -21,18 +23,26 @@ const SCOPE = SELF.name.startsWith("@") ? SELF.name.split("/")[0] : null;
21
23
 
22
24
  const HELP = `chew ${SELF.version}
23
25
 
24
- Usage: chew agents <command> [dir] [--force] [--dry-run] [--local]
26
+ Usage: chew agents <command> [dir] [flags]
25
27
 
26
28
  Commands:
27
29
  setup:pull download the latest ${SELF.name} and install it into the project
28
30
  (fresh project: everything; configured project: refresh what was not edited there)
29
31
  setup:check compare the project with the latest published config (exit 1 on drift)
32
+ setup:print print the latest config AGENTS.md inside its managed block, to concatenate into yours
33
+ (--raw: the file alone, without the markers)
30
34
 
31
35
  Flags:
32
- --force also overwrite files edited in the project
33
- (CLAUDE.md, .claude/settings.local.json and .claude/agent-memory/** are never touched)
34
- --dry-run show what would change without writing
35
- --local use the version installed with chew (${SELF.version}) instead of downloading
36
+ --force also overwrite files edited in the project
37
+ (CLAUDE.md, .claude/settings.local.json and .claude/agent-memory/** are never touched)
38
+ --dry-run show what would change without writing
39
+ --local use the version installed with chew (${SELF.version}) instead of downloading
40
+ --agents-md=<choice>
41
+ when the project already has its own AGENTS.md (asked in a terminal; default keep):
42
+ keep leave yours as it is
43
+ overwrite replace it with the config version
44
+ append add the config version at the end, in a managed block that later pulls refresh
45
+ print print the config version to concatenate yourself
36
46
 
37
47
  dir defaults to the current directory.`;
38
48
 
@@ -99,16 +109,46 @@ function fetchConfig(local) {
99
109
 
100
110
  const newer = (a, b) => { const x = a.split(".").map(Number), y = b.split(".").map(Number); for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] > y[i]; return false; };
101
111
 
112
+ const COMMANDS = ["setup:pull", "setup:check", "setup:print"];
113
+ const AGENTS_MD = ["keep", "overwrite", "append", "print"];
114
+
115
+ // Flags, `--name=value` / `--name value` options and positionals.
116
+ function parseArgs(argv) {
117
+ const flags = new Set(), opts = {}, pos = [];
118
+ for (let i = 0; i < argv.length; i++) {
119
+ const a = argv[i];
120
+ if (a === "--agents-md") opts["agents-md"] = argv[++i] ?? "";
121
+ else if (a.startsWith("--") && a.includes("=")) { const k = a.slice(2, a.indexOf("=")); opts[k] = a.slice(a.indexOf("=") + 1); }
122
+ else if (a.startsWith("-")) flags.add(a);
123
+ else pos.push(a);
124
+ }
125
+ return { flags, opts, pos };
126
+ }
127
+
102
128
  async function main(argv) {
103
- const flags = new Set(argv.filter((a) => a.startsWith("-")));
104
- const [ns, command, dir] = argv.filter((a) => !a.startsWith("-"));
129
+ const { flags, opts, pos } = parseArgs(argv);
130
+ const [ns, command, dir] = pos;
105
131
  if (flags.has("--version") || flags.has("-v")) { console.log(SELF.version); return 0; }
106
132
  if (!ns || ns === "help" || flags.has("--help") || flags.has("-h")) { console.log(HELP); return ns ? 0 : 2; }
107
- if (ns !== "agents" || !["setup:pull", "setup:check"].includes(command)) {
133
+ const agentsMd = opts["agents-md"];
134
+ if (agentsMd !== undefined && !AGENTS_MD.includes(agentsMd)) { console.error(`--agents-md must be one of: ${AGENTS_MD.join(", ")}`); return 2; }
135
+ if (ns !== "agents" || !COMMANDS.includes(command)) {
108
136
  console.error(`unknown command: chew ${[ns, command].filter(Boolean).join(" ")}\n\n${HELP}`);
109
137
  return 2;
110
138
  }
111
139
 
140
+ if (command === "setup:print") { // stdout carries only the content, so it can be redirected or piped
141
+ const config = fetchConfig(flags.has("--local"));
142
+ try {
143
+ const engine = await import(pathToFileURL(join(config.dir, "scripts", "bootstrap.mjs")).href);
144
+ const raw = readFileSync(join(config.dir, "AGENTS.md"), "utf8");
145
+ process.stdout.write(flags.has("--raw") || !engine.agentsBlock ? raw : engine.agentsBlock(config.dir));
146
+ return 0;
147
+ } finally {
148
+ config.cleanup();
149
+ }
150
+ }
151
+
112
152
  const target = resolve(dir ?? process.cwd());
113
153
  if (!existsSync(target)) throw new Error(`target is not a directory: ${target}`);
114
154
  try { if (JSON.parse(readFileSync(join(target, "package.json"), "utf8")).name === SELF.name) throw new Error("this is the config repo itself; run chew inside a project"); }
@@ -120,6 +160,10 @@ async function main(argv) {
120
160
  const mode = command === "setup:check" ? "check" : "update";
121
161
  const r = engine.install({ src: config.dir, target, mode, force: flags.has("--force"), dryRun: flags.has("--dry-run") });
122
162
  engine.report(r);
163
+ if (mode === "update" && engine.settleAgentsMd) {
164
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
165
+ await engine.settleAgentsMd({ src: config.dir, target, r, choice: agentsMd, dryRun: flags.has("--dry-run"), interactive });
166
+ }
123
167
  if (mode === "update" && !flags.has("--dry-run") && r.written.some((w) => w.startsWith("CLAUDE.md"))) {
124
168
  console.log("\nnext: fill in purpose and commands in CLAUDE.md; optional: `npx impeccable install` (for /ui) and `/graphify .`");
125
169
  }
@@ -12,12 +12,14 @@ One file per architecture decision: `docs/adr/NNNN-<slug>.md`.
12
12
 
13
13
  `/architect` returns an ADR proposal, the orchestrator copies it into `docs/plans/<task>.md`, and at DoD creates the ADR file in the same diff as the change.
14
14
 
15
+ Skill `domain-modeling` decides whether a decision earns an ADR (hard to reverse, surprising without context, the result of a real trade-off); this file decides how it looks and lives, and wins over the skill's `ADR-FORMAT.md`. In a multi-context repo (`GLOSSARY-MAP.md` at the root), context decisions live in `<context>/docs/adr/` with their own numbering and system-wide ones here.
16
+
15
17
  ## Template
16
18
 
17
19
  ```markdown
18
20
  # NNNN - <title>
19
21
 
20
- Status: Proposed | Accepted | Superseded by NNNN | Deprecated
22
+ Status: Proposed | Accepted | Partly superseded by NNNN (<which part>) | Superseded by NNNN | Deprecated
21
23
  Date: YYYY-MM-DD
22
24
 
23
25
  ## Context
@@ -7,9 +7,11 @@ The newest plan is re-injected after context compaction by the post-compact hook
7
7
  Rules of the file:
8
8
 
9
9
  - Paths appear only under Scope. Decisions name modules, interfaces and contracts, never files or code.
10
- - Test seams are the contract between `/product`, the implementers (`tdd`) and `/verifier`: a seam not listed here is not tested, a seam listed here must be.
10
+ - Test seams are the contract between `/product`, the implementers (`tdd`) and `/verifier`: a seam not listed here is not tested, a seam listed here must be. One row per level (e2e, entry, service, repository, unit) and public interface; each row says which criteria it proves, what it catches and what it leaves to another level. The user confirms the table at the ticket gate (`Confirmed:` line), which makes the rows the pre-agreed seams `tdd` asks for: implementers never ask, and never add a row.
11
+ - Terms holds the domain words this plan introduces or uses differently from `GLOSSARY.md`. The user confirms them at the ticket gate and the orchestrator moves them into `GLOSSARY.md` (skill `domain-modeling`).
11
12
  - Tickets are vertical slices with `Blocked by` edges; the orchestrator works the frontier (tickets whose blockers are done) and marks Status as they land.
12
13
  - Session state is the compaction insurance: everything the orchestrator would lose with its window goes there first (skill `orchestrate` section 14).
14
+ - Grill record is the grill on paper: the orchestrator appends to it after every round, while the conversation is still in its window, and `/product` plans from it. Nobody rewrites it later; corrections are appended. It is the last section because the post-compact hook re-injects only the first 120 lines, which must keep carrying Session state and the tickets; for the same reason Test seams, which grows with the levels, comes after Tickets.
13
15
  - A section that does not apply says `n/a` and why, so a reader knows it was considered.
14
16
 
15
17
  ## Template
@@ -48,20 +50,27 @@ Size: S | M | L
48
50
  ## Decisions
49
51
  - <decision> - <why> (modules, interfaces, schema, contracts; architect verdict and constraints)
50
52
 
51
- ## Test seams
52
- | Seam | Catches | Misses | Prior art |
53
- |------|---------|--------|-----------|
54
- | S1: <public interface> | <behavior it proves> | <what it cannot see> | <existing test, or none> |
55
-
56
53
  ## Tickets
57
54
  <one ticket = one tracer bullet = one brief = one agent = one context window>
58
55
  | # | Ticket | Delivers | Blocked by | Agent | Paths | Seams | Status |
59
56
  |---|--------|----------|------------|-------|-------|-------|--------|
60
57
  | 1 | <title> | <end-to-end behavior, from the user's perspective> | - | /api | <globs> | S1 | todo |
61
58
 
59
+ ## Test seams
60
+ <one row per level and public interface; default levels per size in agent `product` (step 6); levels per stack in the `arch-*` testing references>
61
+ Confirmed: <YYYY-MM-DD at the ticket gate | pending>
62
+ | # | Level | Seam (public interface) | ACs | Catches | Leaves to | Prior art |
63
+ |---|-------|-------------------------|-----|---------|-----------|-----------|
64
+ | S1 | e2e | <flow: create -> approve -> list> | AC1, AC2 | <the flow end to end, happy path and the failures that hurt most> | <rule detail: S3, S5> | <existing test, or none> |
65
+ | S2 | entry | <POST /invoices> | AC1 | <contract, validation, authorization, error codes> | <business rule: S3> | <...> |
66
+ | S3 | service | <CreateInvoice use case> | AC3 | <business rules and decisions, ports faked> | <SQL: S4> | <...> |
67
+ | S4 | repository | <invoices repository adapter> | AC4 | <SQL, mapping, constraints, pagination, real database> | <-> | <...> |
68
+ | S5 | unit | <invoice rules> | AC3 | <every edge case of the pure rule> | <-> | <...> |
69
+
62
70
  ## Definition of Done
63
71
  - [ ] lint, build, tests green (evidence pasted)
64
- - [ ] a test exists at every seam listed above
72
+ - [ ] a test exists at every seam listed above, at its level
73
+ - [ ] the slow suites (integration, e2e) ran in `/verifier`; the Stop hook runs only the fast one
65
74
  - [ ] diff only inside scoped paths
66
75
  - [ ] docs updated if route/job/store/integration/fallback changed
67
76
  - [ ] no secrets in diff
@@ -70,4 +79,26 @@ Size: S | M | L
70
79
 
71
80
  ## Open questions
72
81
  - ...
82
+
83
+ ## Terms
84
+ <domain words this plan introduces or uses differently from GLOSSARY.md, confirmed by the user at the ticket gate and then written to GLOSSARY.md; n/a when the plan uses only glossary terms>
85
+ - **<Term>**: <one or two sentences: what it is> _Avoid_: <synonyms> - new | conflicts with GLOSSARY.md (<entry>) | confirmed
86
+
87
+ ## Grill record
88
+ <appended by the orchestrator after every grill round; never rewritten, corrections appended; n/a when no grill ran (S, or M with no open decision)>
89
+ - Q<n> <question title>: <decision> - why: <reason, in the user's words when they add nuance>
90
+ - Rejected: <alternative> - <why not>
91
+ - Constraint: <anything said in passing that binds the work>
73
92
  ```
93
+
94
+ ## Wayfinder efforts
95
+
96
+ Work too big and too foggy for one plan (the destination is clear, the way is not) is charted by skill `wayfinder` in its own folder, one level down so the post-compact hook never mistakes it for a task plan:
97
+
98
+ ```
99
+ docs/plans/<effort>/
100
+ map.md Status, Destination, Notes, Decisions so far, Not yet specified, Out of scope
101
+ decisions/NN-<slug>.md one question per file: Type, Status, Blocked by, ## Question, ## Answer
102
+ ```
103
+
104
+ The map is an index: a decision lives in its ticket, and the map only gists it and links. When the map is done, the build starts as an L task with its own `docs/plans/<task>.md`, and `/product` reads the map as the source of the decisions already made.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evertonferreira96/agents",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Personal agents, rules, hooks and skills for Claude Code (portable to Cursor and Codex through AGENTS.md), installable into any project.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,6 +7,8 @@
7
7
  // node scripts/bootstrap.mjs <target> --force update --force: also overwrite files edited in the project
8
8
  // node scripts/bootstrap.mjs --sync inside a project bootstrapped from a local clone: pull it, then update
9
9
  // Protected (never written over): CLAUDE.md, .claude/settings.local.json, .claude/agent-memory/**
10
+ // Project-owned (never shipped, written over or deleted, even with --force): GLOSSARY.md and GLOSSARY-MAP.md, docs/plans/** and docs/adr/**
11
+ // except their README.md templates, .claude/agent-memory/**, .claude/handoffs/**. check-catalog.mjs fails if SYNC would ship one.
10
12
  // .claude/config-manifest.json records the hash of every file the config installed. `update` uses it to tell a file that is
11
13
  // only outdated (overwritten) from one edited in the project (kept unless --force), and deletes files that left the config
12
14
  // only when the project copy is untouched.
@@ -20,11 +22,20 @@ export const SYNC = [".claude/agents", ".claude/hooks", ".claude/rules", ".claud
20
22
  "docs/plans/README.md", "docs/adr/README.md", "docs/brief-template.md", "scripts/skills-update.mjs", "scripts/check-catalog.mjs", "scripts/bootstrap.mjs", "skills-lock.json"];
21
23
  export const SKIP = /(^|\/)(scripts\/bin|__pycache__|node_modules|\.context-band)(\/|$)|\.pyc$/;
22
24
  export const PROTECTED = /^(CLAUDE\.md|\.claude\/settings\.local\.json|\.claude\/agent-memory\/.*)$/;
25
+ // What the project's work produces: plans, wayfinder maps, ADRs, glossaries, agent memory, handoffs. The config only ships the two READMEs.
26
+ export const PROJECT_OWNED = /^((?!\.claude\/)(.+\/)?GLOSSARY\.md|GLOSSARY-MAP\.md|docs\/plans\/(?!README\.md$).+|((?!\.claude\/).+\/)?docs\/adr\/(?!README\.md$).+|\.claude\/agent-memory\/.+|\.claude\/handoffs\/.+)$/;
27
+ const isProtected = (rel) => PROTECTED.test(rel) || PROJECT_OWNED.test(rel);
23
28
  export const GITIGNORE = [".claude/worktrees/", ".claude/handoffs/", ".claude/.context-band", ".claude/.stop-verify-ran", ".claude/settings.local.json",
24
29
  ".claude/agent-memory-local/", "graphify-out/", ".claude/skills/impeccable/scripts/bin/", "__pycache__/", "*.pyc"];
25
30
  export const MANIFEST = ".claude/config-manifest.json";
26
31
  export const SOURCE_FILE = ".claude/config-source.json";
27
32
  export const DEFAULT_SRC = fileURLToPath(new URL("..", import.meta.url));
33
+ // AGENTS.md of a project that already had its own: the config version can live inside it as a managed block,
34
+ // which `update` refreshes in place without touching the rest of the file.
35
+ export const AGENTS_FILE = "AGENTS.md";
36
+ export const BLOCK_START = "<!-- agents-config:start -->";
37
+ export const BLOCK_END = "<!-- agents-config:end -->";
38
+ export const AGENTS_MD_CHOICES = ["keep", "overwrite", "append", "print"];
28
39
 
29
40
  const sha = (p) => createHash("sha256").update(readFileSync(p)).digest("hex");
30
41
  const readJson = (p) => { try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; } };
@@ -39,14 +50,15 @@ export function readPackage(src = DEFAULT_SRC) {
39
50
  }
40
51
 
41
52
  // Every file the config installs, keyed by its path relative to the config root (forward slashes).
42
- export function configFiles(src = DEFAULT_SRC) {
53
+ // Project-owned paths are never installed; `includeProjectOwned` lists them anyway, for check-catalog.mjs.
54
+ export function configFiles(src = DEFAULT_SRC, { includeProjectOwned = false } = {}) {
43
55
  const files = new Map();
44
56
  for (const entry of SYNC) {
45
57
  const abs = join(src, entry);
46
58
  if (!existsSync(abs)) continue;
47
59
  for (const f of walk(abs)) {
48
60
  const rel = relative(src, f).split(sep).join("/");
49
- if (!SKIP.test(rel)) files.set(rel, f);
61
+ if (!SKIP.test(rel) && (includeProjectOwned || !PROJECT_OWNED.test(rel))) files.set(rel, f);
50
62
  }
51
63
  }
52
64
  return files;
@@ -61,6 +73,82 @@ function removeEmptyParents(target, rel) {
61
73
  }
62
74
  }
63
75
 
76
+ // The config's AGENTS.md wrapped in the managed-block markers (LF line endings, trailing newline).
77
+ export function agentsBlock(src = DEFAULT_SRC) {
78
+ const body = readFileSync(join(src, AGENTS_FILE), "utf8").replace(/\r\n/g, "\n").trimEnd();
79
+ const note = `<!-- Managed by ${readPackage(src).name}: \`chew agents setup:pull\` refreshes this block. Edit outside the markers. -->`;
80
+ return `${BLOCK_START}\n${note}\n${body}\n${BLOCK_END}\n`;
81
+ }
82
+
83
+ // Position of the managed block in `text` (normalized to LF), or null when the file has none.
84
+ function findBlock(text) {
85
+ const t = text.replace(/\r\n/g, "\n");
86
+ const start = t.indexOf(BLOCK_START);
87
+ const end = start < 0 ? -1 : t.indexOf(BLOCK_END, start + BLOCK_START.length);
88
+ return end < 0 ? null : { text: t, start, end: end + BLOCK_END.length, eol: text.includes("\r\n") ? "\r\n" : "\n" };
89
+ }
90
+
91
+ function recordHash(target, rel, hash) {
92
+ const p = join(target, MANIFEST);
93
+ const m = readJson(p);
94
+ if (!m?.files) return;
95
+ m.files[rel] = hash;
96
+ writeFileSync(p, JSON.stringify(m, null, 2) + "\n");
97
+ }
98
+
99
+ // What to do with a project AGENTS.md that differs from the config and has no managed block:
100
+ // keep (nothing), overwrite (config version, refreshed by later updates), append (managed block at the end, refreshed by
101
+ // later updates), print (returns the managed block for the user to paste). The manifest records the config hash, so a
102
+ // file with a block never counts as untouched and is never overwritten whole.
103
+ export function applyAgentsMd({ src = DEFAULT_SRC, target, choice, dryRun = false }) {
104
+ if (!AGENTS_MD_CHOICES.includes(choice)) throw new Error(`unknown AGENTS.md choice: ${choice} (${AGENTS_MD_CHOICES.join(", ")})`);
105
+ const abs = join(src, AGENTS_FILE);
106
+ const dst = join(target, AGENTS_FILE);
107
+ if (choice === "print") return { choice, text: agentsBlock(src) };
108
+ if (choice === "keep" || dryRun) return { choice };
109
+ if (choice === "overwrite") copyFileSync(abs, dst);
110
+ if (choice === "append") {
111
+ const cur = existsSync(dst) ? readFileSync(dst, "utf8") : "";
112
+ const eol = cur.includes("\r\n") ? "\r\n" : "\n";
113
+ const block = agentsBlock(src).replace(/\n/g, eol);
114
+ const found = findBlock(cur);
115
+ if (found) writeFileSync(dst, (found.text.slice(0, found.start) + agentsBlock(src).trimEnd() + found.text.slice(found.end)).replace(/\n/g, eol));
116
+ else writeFileSync(dst, cur + (cur === "" || cur.endsWith(eol + eol) ? "" : cur.endsWith(eol) ? eol : eol + eol) + block);
117
+ }
118
+ recordHash(target, AGENTS_FILE, sha(abs));
119
+ return { choice };
120
+ }
121
+
122
+ // Ask once in a terminal (default keep).
123
+ export async function askAgentsMd({ input = process.stdin, output = process.stdout } = {}) {
124
+ const { createInterface } = await import("node:readline/promises");
125
+ output.write(`\nAGENTS.md already exists in this project and differs from the config. What should happen to it?
126
+ [k] keep yours as it is (default)
127
+ [o] overwrite it with the config version
128
+ [a] append the config version at the end, in a managed block that later pulls keep current
129
+ [p] print the config version here, to concatenate it yourself\n`);
130
+ const rl = createInterface({ input, output });
131
+ try {
132
+ const answer = (await rl.question("Choice [k/o/a/p]: ")).trim().toLowerCase();
133
+ return { o: "overwrite", a: "append", p: "print" }[answer[0]] ?? "keep";
134
+ } finally {
135
+ rl.close();
136
+ }
137
+ }
138
+
139
+ // After init/update: settle an AGENTS.md conflict with `choice`, or ask when interactive, or keep and say how to choose.
140
+ export async function settleAgentsMd({ src = DEFAULT_SRC, target, r, choice, dryRun = false, interactive = false, log = console.log }) {
141
+ if (!r.agentsMdConflict) return null;
142
+ const picked = choice || (interactive ? await askAgentsMd() : "keep");
143
+ const out = applyAgentsMd({ src, target, choice: picked, dryRun });
144
+ const dry = dryRun ? " (dry run: nothing written)" : "";
145
+ if (picked === "keep") log(`\nAGENTS.md: kept yours. To change that: --agents-md=overwrite, --agents-md=append or --agents-md=print`);
146
+ if (picked === "overwrite") log(`\nAGENTS.md: overwritten with the config version${dry}`);
147
+ if (picked === "append") log(`\nAGENTS.md: config version appended at the end in a managed block${dry}; later pulls refresh only that block`);
148
+ if (picked === "print") log(`\nAGENTS.md: the config version is below. Paste it into yours with the markers, so later pulls refresh it:\n\n${out.text}`);
149
+ return picked;
150
+ }
151
+
64
152
  // mode: "init" (copy what is missing), "update" (also refresh untouched files and drop obsolete ones), "check" (report only).
65
153
  export function install({ src = DEFAULT_SRC, target, mode = "init", force = false, dryRun = false }) {
66
154
  if (!["init", "update", "check"].includes(mode)) throw new Error(`unknown mode: ${mode}`);
@@ -86,24 +174,40 @@ export function install({ src = DEFAULT_SRC, target, mode = "init", force = fals
86
174
  }
87
175
  const have = sha(dst);
88
176
  if (have === want) { r.same++; next[rel] = want; continue; }
177
+ if (rel === AGENTS_FILE) {
178
+ const cur = readFileSync(dst, "utf8");
179
+ const found = findBlock(cur);
180
+ if (found) { // the project's own AGENTS.md carrying the config in a managed block: only the block is ours
181
+ next[rel] = want; // never the project file's hash, so no engine treats the file as untouched and replaces it whole
182
+ const block = agentsBlock(src).trimEnd();
183
+ if (found.text.slice(found.start, found.end) === block) { r.same++; continue; }
184
+ if (mode === "check") { r.outdated.push(`${rel} (managed block)`); continue; }
185
+ if (mode === "init") { r.kept.push(`${rel} (managed block outdated; run update)`); continue; }
186
+ if (write) writeFileSync(dst, (found.text.slice(0, found.start) + block + found.text.slice(found.end)).replace(/\n/g, found.eol));
187
+ r.updated.push(`${rel} (managed block)`);
188
+ continue;
189
+ }
190
+ }
89
191
  const untouched = installed?.[rel] === have;
90
192
  if (mode === "check") { (untouched ? r.outdated : r.edited).push(rel); continue; }
91
- if (mode === "update" && (untouched || force) && !PROTECTED.test(rel)) {
193
+ if (mode === "update" && (untouched || force) && !isProtected(rel)) {
92
194
  if (write) copyFileSync(abs, dst);
93
195
  r.updated.push(rel); next[rel] = want;
94
196
  continue;
95
197
  }
96
198
  r.kept.push(rel);
199
+ if (rel === AGENTS_FILE) r.agentsMdConflict = true; // the CLI offers keep / overwrite / append / print
97
200
  if (installed?.[rel]) next[rel] = installed[rel];
98
201
  }
99
202
 
100
203
  for (const [rel, hash] of Object.entries(installed ?? {})) {
101
204
  if (files.has(rel)) continue;
205
+ if (PROJECT_OWNED.test(rel)) continue; // an older config shipped it: it is the project's now, so stop tracking it, never delete it
102
206
  const dst = join(target, rel);
103
207
  if (!existsSync(dst)) continue;
104
208
  const untouched = sha(dst) === hash;
105
209
  if (mode === "check") { r.obsolete.push(rel); continue; }
106
- if (mode === "update" && untouched && !PROTECTED.test(rel)) {
210
+ if (mode === "update" && untouched && !isProtected(rel)) {
107
211
  if (write) { rmSync(dst); removeEmptyParents(target, rel); }
108
212
  r.removed.push(rel);
109
213
  continue;
@@ -155,9 +259,10 @@ export function report(r, log = console.log) {
155
259
  list("kept", r.kept, r.mode === "init" ? " (differs; init never overwrites)" : " (edited here)");
156
260
  list("kept", r.obsoleteKept, " (left the config, but edited here)");
157
261
  if (r.written.length) { log("written:"); for (const w of r.written) log(` ${w}`); }
158
- if (r.mode === "update" && !r.hadManifest && r.kept.length) log("no manifest from a previous install, so differing files count as edited; review them and run `update --force` once to adopt the config versions");
159
- else if (r.kept.length && r.mode === "update") log("`update --force` overwrites the files kept above (protected files excepted)");
160
- else if (r.kept.length) log("run `update` to refresh differing files");
262
+ const others = r.kept.filter((k) => !(r.agentsMdConflict && k === AGENTS_FILE)); // AGENTS.md gets its own choice (settleAgentsMd)
263
+ if (r.mode === "update" && !r.hadManifest && others.length) log("no manifest from a previous install, so differing files count as edited; review them and run `update --force` once to adopt the config versions");
264
+ else if (others.length && r.mode === "update") log("`update --force` overwrites the files kept above (protected files excepted)");
265
+ else if (others.length) log("run `update` to refresh differing files");
161
266
  }
162
267
 
163
268
  // Legacy CLI (scripts/bootstrap.mjs <target> [--check|--force] | --sync).
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
- // Drift checks: skills on disk vs docs/skills.md vs frontmatter vs skills-lock.json vs agents' skills: vs settings hooks.
2
+ // Drift checks: skills on disk vs docs/skills.md vs frontmatter vs skills-lock.json vs agents' skills: vs settings hooks,
3
+ // and no project-owned path (plans, ADRs, glossary, agent memory, handoffs) among the files the config ships.
3
4
  // Exit 1 on any error. Warnings do not fail.
4
5
  import { existsSync, readFileSync, readdirSync } from "node:fs";
6
+ import { configFiles, PROJECT_OWNED } from "./bootstrap.mjs";
5
7
  const errors = [], warnings = [];
6
8
  const read = (p) => readFileSync(p, "utf8");
7
9
  const fm = (p) => read(p).split("---")[1] ?? "";
@@ -37,6 +39,7 @@ for (const list of Object.values(settings.hooks ?? {})) for (const e of list) fo
37
39
  const m = /\$CLAUDE_PROJECT_DIR\/(\.claude\/hooks\/[^" ]+)/.exec(h.command ?? "");
38
40
  if (m && !existsSync(m[1])) errors.push(`settings.json: hook file ${m[1]} does not exist`);
39
41
  }
42
+ for (const rel of configFiles(undefined, { includeProjectOwned: true }).keys()) if (PROJECT_OWNED.test(rel)) errors.push(`${rel}: project-owned path shipped by the config (scripts/bootstrap.mjs SYNC)`);
40
43
  for (const w of warnings) console.log(`warn ${w}`);
41
44
  for (const e of errors) console.log(`ERROR ${e}`);
42
45
  console.log(`${skills.length} skills, ${rows.size} catalog rows, ${Object.keys(lock).length} lock entries: ${errors.length} error(s), ${warnings.length} warning(s)`);
package/skills-lock.json CHANGED
@@ -69,6 +69,13 @@
69
69
  "skillPath": "skills/engineering/diagnosing-bugs/SKILL.md",
70
70
  "sha256": "849198df88cdb65fd0d97236604c1fca7ff2f22d2f705f9724a800d0266b118d"
71
71
  },
72
+ "domain-modeling": {
73
+ "source": "mattpocock/skills",
74
+ "sourceType": "github",
75
+ "sourceCommit": "49dd158",
76
+ "skillPath": "skills/engineering/domain-modeling/SKILL.md",
77
+ "sha256": "7b925d7b1e341a2eeae33ad68a8a8c0ab889a38ddd22a7598e4c240e5a7556a3"
78
+ },
72
79
  "emil-design-eng": {
73
80
  "source": "emilkowalski/skills",
74
81
  "sourceType": "github",
@@ -107,10 +114,9 @@
107
114
  "sourceType": "github",
108
115
  "sourceCommit": "49dd158",
109
116
  "skillPath": "skills/engineering/grill-with-docs/SKILL.md",
110
- "note": "adapted: upstream calls grilling + domain-modeling; local version inlines glossary/ADR rules and adds docs/plans read-first, graphify and the decisions block for the orchestrator",
117
+ "note": "adapted: calls grilling + domain-modeling like upstream; adds docs/plans read-first, graphify, the Grill record, the ADR template and lifecycle of docs/adr/README.md (which wins over domain-modeling's ADR-FORMAT.md) and the decisions block for the orchestrator",
111
118
  "adapted": true,
112
- "sha256": "4a6d8bb82e01431ecee9a13b06ccfc4b911f6ee2efcf09bc6686a8e4cee58f72",
113
- "sha256Note": "baseline taken from the local file on 2026-10-17; replaced by the upstream hash on the next `skills-update.mjs --lock`"
119
+ "sha256": "7de372c13488f1ee96cc11cd8907b56b6809cc93eef776eeddd37de6b6cbe3fe"
114
120
  },
115
121
  "grilling": {
116
122
  "source": "mattpocock/skills",
@@ -253,6 +259,15 @@
253
259
  "adapted": true,
254
260
  "adaptedNote": "frontmatter only: disable-model-invocation: true (preloaded by the agents that use it)"
255
261
  },
262
+ "wayfinder": {
263
+ "source": "mattpocock/skills",
264
+ "sourceType": "github",
265
+ "sourceCommit": "49dd158",
266
+ "skillPath": "skills/engineering/wayfinder/SKILL.md",
267
+ "sha256": "9be7b478c389605a24517d27752f278933588da97a1b5921b165f455edf4c5b7",
268
+ "adapted": true,
269
+ "adaptedNote": "no issue tracker: the map is docs/plans/<effort>/map.md (with a Status line for the post-compact hook) and tickets are docs/plans/<effort>/decisions/NN-<slug>.md with Type, Status and Blocked by lines (upstream's local-markdown layout, moved under docs/plans); research tickets run as a read-only subagent with a brief instead of skill research (not vendored); prototype tickets go to /ui (skill prototype, emilkowalski) or the owning implementer in a spike worktree, never merged or pushed, since this repo's prototype is a different skill; sessions end with /clear; adds the hand-off to an L task in orchestrate when the way is clear (ADR 0012)"
270
+ },
256
271
  "writing-for-agents": {
257
272
  "source": "mattpocock/skills",
258
273
  "sourceType": "github",