@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.
- package/.claude/agents/api.md +2 -3
- package/.claude/agents/app.md +2 -2
- package/.claude/agents/product.md +18 -7
- package/.claude/agents/quick.md +1 -1
- package/.claude/agents/ui.md +1 -1
- package/.claude/agents/verifier.md +8 -4
- package/.claude/agents/web.md +2 -2
- package/.claude/hooks/orchestrator-guard.sh +3 -1
- package/.claude/hooks/post-compact.sh +20 -4
- package/.claude/hooks/stop-verify.sh +8 -3
- package/.claude/skills/arch-api/references/review.md +17 -0
- package/.claude/skills/arch-app/references/review.md +15 -0
- package/.claude/skills/arch-db/references/review.md +2 -0
- package/.claude/skills/arch-web/references/tests.md +15 -0
- package/.claude/skills/brief/SKILL.md +32 -3
- package/.claude/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/.claude/skills/domain-modeling/GLOSSARY-FORMAT.md +60 -0
- package/.claude/skills/domain-modeling/SKILL.md +74 -0
- package/.claude/skills/domain-modeling/agents/openai.yaml +3 -0
- package/.claude/skills/grill-with-docs/SKILL.md +10 -16
- package/.claude/skills/orchestrate/SKILL.md +19 -12
- package/.claude/skills/review/SKILL.md +3 -3
- package/.claude/skills/wayfinder/SKILL.md +146 -0
- package/.claude/skills/wayfinder/agents/openai.yaml +5 -0
- package/README.md +83 -11
- package/VERSION +1 -1
- package/bin/agents.mjs +46 -22
- package/bin/chew.mjs +53 -9
- package/docs/adr/README.md +3 -1
- package/docs/plans/README.md +38 -7
- package/package.json +1 -1
- package/scripts/bootstrap.mjs +112 -7
- package/scripts/check-catalog.mjs +4 -1
- 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
|
-
|
|
9
|
-
import {
|
|
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] [
|
|
14
|
+
const HELP = `Usage: agents <command> [dir] [flags]
|
|
12
15
|
|
|
13
16
|
Commands:
|
|
14
|
-
init
|
|
15
|
-
update
|
|
16
|
-
check
|
|
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
|
|
20
|
-
|
|
21
|
-
--dry-run
|
|
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(
|
|
27
|
-
|
|
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
|
-
|
|
31
|
-
if (
|
|
32
|
-
if (!
|
|
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
|
|
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
|
|
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
|
-
|
|
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] [
|
|
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
|
|
33
|
-
|
|
34
|
-
--dry-run
|
|
35
|
-
--local
|
|
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
|
|
104
|
-
const [ns, command, dir] =
|
|
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
|
-
|
|
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
|
}
|
package/docs/adr/README.md
CHANGED
|
@@ -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
|
package/docs/plans/README.md
CHANGED
|
@@ -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.
|
|
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": {
|
package/scripts/bootstrap.mjs
CHANGED
|
@@ -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
|
-
|
|
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) && !
|
|
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 && !
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
else if (r.
|
|
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:
|
|
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": "
|
|
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",
|