dw-sdlc 0.7.0 → 0.10.0

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 (57) hide show
  1. package/README.md +23 -12
  2. package/config/agentic.config.schema.json +77 -18
  3. package/dist/agentic-config.d.ts +41 -7
  4. package/dist/agentic-config.js +36 -12
  5. package/dist/checks/agents-map.d.ts +9 -1
  6. package/dist/checks/agents-map.js +60 -17
  7. package/dist/checks/decision-records.d.ts +5 -0
  8. package/dist/checks/decision-records.js +121 -16
  9. package/dist/checks/file-size-ratchet.js +7 -5
  10. package/dist/checks/index.js +2 -0
  11. package/dist/checks/markdown-budget.d.ts +2 -5
  12. package/dist/checks/markdown-budget.js +6 -15
  13. package/dist/checks/markdown-refs.js +1 -11
  14. package/dist/checks/routed-docs.d.ts +6 -0
  15. package/dist/checks/routed-docs.js +49 -0
  16. package/dist/checks/tree.d.ts +2 -0
  17. package/dist/checks/tree.js +13 -3
  18. package/dist/commands/check.d.ts +1 -1
  19. package/dist/commands/check.js +6 -4
  20. package/dist/commands/gate.d.ts +10 -4
  21. package/dist/commands/gate.js +202 -86
  22. package/dist/commands/pr-reviewed.js +6 -22
  23. package/dist/commands/pr-ship.js +119 -30
  24. package/dist/commands/references-sync.js +5 -6
  25. package/dist/commands/setup.d.ts +2 -0
  26. package/dist/commands/setup.js +17 -9
  27. package/dist/drift.d.ts +17 -0
  28. package/dist/drift.js +68 -0
  29. package/dist/gate-cache.d.ts +13 -0
  30. package/dist/gate-cache.js +39 -0
  31. package/dist/git-env.d.ts +2 -0
  32. package/dist/git-env.js +10 -0
  33. package/dist/markdown.d.ts +19 -0
  34. package/dist/markdown.js +46 -2
  35. package/dist/package-version.d.ts +2 -0
  36. package/dist/package-version.js +13 -0
  37. package/dist/pr/decide.d.ts +37 -9
  38. package/dist/pr/decide.js +113 -32
  39. package/dist/pr/delta.d.ts +11 -0
  40. package/dist/pr/delta.js +60 -0
  41. package/dist/pr/read.d.ts +8 -3
  42. package/dist/pr/read.js +56 -10
  43. package/dist/pr/route.d.ts +14 -0
  44. package/dist/pr/route.js +52 -0
  45. package/dist/pr/state.d.ts +48 -2
  46. package/dist/pr/state.js +42 -9
  47. package/dist/router-block.d.ts +4 -1
  48. package/dist/router-block.js +31 -7
  49. package/dist/run-command.d.ts +24 -0
  50. package/dist/run-command.js +121 -0
  51. package/dist/setup-gate.js +3 -12
  52. package/dist/setup-worker.d.ts +11 -0
  53. package/dist/setup-worker.js +37 -0
  54. package/dist/working-tree.d.ts +13 -0
  55. package/dist/working-tree.js +60 -0
  56. package/package.json +1 -1
  57. package/templates/router.md +1 -0
package/README.md CHANGED
@@ -16,9 +16,14 @@ pnpm add -D dw-sdlc
16
16
  ## The Gate
17
17
 
18
18
  `dw-sdlc gate` runs the Steps under `gate.steps` in order from the repository root, then every Check
19
- under `gate.checks`. It exits 0 when everything passes, 1 when a Step or a Check fails, and 2 when the
20
- Gate cannot run. `--json` prints the result as one document. `dw-sdlc check <name>` runs one Check
21
- alone, with the same lines, exit codes and document.
19
+ under `gate.checks`. It prints one line per Step and per Check, and the last 50 lines of a command
20
+ that fails; `--verbose` streams every command's output instead. A Step that creates, changes or
21
+ deletes a file git tracks or would track fails the Gate as Drift, unless `gate.drift` is `"allow"`.
22
+ It exits 0 when everything passes, 1 when a Step or a Check fails or a Step drifts, and 2 when the
23
+ Gate cannot run. `--json` prints the result as one document. `--cache` skips the run when the
24
+ working tree and the resolved Gate match the last green run in that checkout, so a pre-push hook
25
+ costs nothing on an unchanged tree. `dw-sdlc check <name>` runs one Check alone, with the same
26
+ lines, exit codes and document.
22
27
 
23
28
  ```json
24
29
  {
@@ -43,23 +48,29 @@ its `roots` to `maxProseLines` lines of prose, 3 by default.
43
48
  `dw-sdlc pr ship [<n>] [-R owner/name]` reads one pull request through an authenticated `gh` and
44
49
  answers the one Action it needs next, as a line or, with `--json`, one document. The exit code says
45
50
  whose turn it is: 0 done, 10 the agent's, 20 wait, 30 the dev's, 2 it cannot run. The script
46
- performs its own Actions: it updates a branch behind its base, marks a draft ready once nothing else
47
- holds it back, and squash merges a pull request that is green, reviewed and opened by the `gh`
48
- account, when the merge is its own. `--dry-run` performs nothing: it answers the first Action the
51
+ performs its own Actions: it updates a branch behind its base, marks the head reviewed when the
52
+ Review depth is `off`, marks a draft ready once nothing else holds it back, and squash merges a pull
53
+ request that is green, reviewed and opened by the `gh` account, when the merge is its own. `--dry-run` performs nothing: it answers the first Action the
49
54
  script would perform as the dev's. It never deletes a branch; a merged pull request answers with
50
55
  the delete command for the dev.
51
56
 
52
57
  `pr.merge` in the committed `.ai/agentic.config.json` says who merges: the script under `auto`, the
53
- dev under `ask`, the default. The local layer may tighten it and never loosen it, and `--merge` acts
54
- as `auto` for one call. A pull request the `gh` account did not open is always the dev's to merge.
58
+ dev under `ask`, the default. `pr.review` says how closely a head no one has reviewed is read: the
59
+ agent reviews it under `full`, one `sdlc-reviewer` subagent of the plugin does under `light`, and no
60
+ one does under `off`. Under `auto`, the default, the script routes the depth from the changes:
61
+ lockfiles alone `off`; a workflow file, more than 100 changed lines outside lockfiles, or a change
62
+ too large to size `full`; the rest `light`. The local layer may tighten either and never loosen it;
63
+ `--merge` acts as `auto` and `--review full|light|off` sets the depth for one call. A pull request
64
+ the `gh` account did not open is always the dev's to merge.
55
65
 
56
66
  ```json
57
- { "pr": { "merge": "auto" } }
67
+ { "pr": { "merge": "auto", "review": "full" } }
58
68
  ```
59
69
 
60
- `pr ship` answers `review` until the head carries the commit status `dw-sdlc/review`, which
61
- `dw-sdlc pr reviewed <n> --head <sha>` posts after a clean review. It posts nothing and exits 2 once
62
- a push moved the head, so a new head needs a fresh review.
70
+ `pr ship` answers `review`, its depth under `details`, until the head carries the commit status
71
+ `dw-sdlc/review`, which `dw-sdlc pr reviewed <n> --head <sha>` posts after a clean review. It posts
72
+ nothing and exits 2 once a push moved the head, so a new head needs a fresh review. Under `off` the
73
+ script posts that status itself, its description saying why, and goes on.
63
74
 
64
75
  ## Commands
65
76
 
@@ -114,13 +114,18 @@
114
114
  "minLength": 1
115
115
  },
116
116
  "steps": {
117
- "description": "The Steps: toolchain commands run in order from the repository root through `sh -c`, each one's output passed through; the first that fails ends the Gate and no Check runs. A command the shell cannot find (exit 127) or execute (126) means the Gate cannot run.",
117
+ "description": "The Steps: toolchain commands run in order from the repository root through `sh -c`; the first that does not pass ends the Gate and no Check runs. Each one's output is captured, and its last 50 lines are printed when it does not pass; `--verbose` passes the output through instead. A command the shell cannot find (exit 127) or execute (126) means the Gate cannot run.",
118
118
  "type": "array",
119
119
  "items": {
120
120
  "type": "string",
121
121
  "minLength": 1
122
122
  }
123
123
  },
124
+ "drift": {
125
+ "description": "Whether a Step may write the tree. `fail`, the default, watches every Step for Drift: before and after each Step the Gate records the content of every file git tracks or would track, and each file the Step created, changed or deleted is one failure, `\"<step>\" created it`, `changed it` or `deleted it`, without stopping the Steps after it. Ignored files and edits made before the run never drift. A tree the Gate cannot record, outside a git checkout, means the Gate cannot run. `allow` turns the watch off.",
126
+ "enum": ["fail", "allow"],
127
+ "default": "fail"
128
+ },
124
129
  "checks": {
125
130
  "description": "The Checks the Gate runs after its steps, every one even when another fails: the `run` scripts first, then the Checks the package ships, in the order listed here. A Check the package ships is `true` for its defaults, an options object over them, or `false` to stay off. A key names a Check the package ships, and a name it does not ship is refused.",
126
131
  "type": "object",
@@ -167,7 +172,7 @@
167
172
  "additionalProperties": false
168
173
  },
169
174
  "agents-map": {
170
- "description": "The agent map: within its line and byte budget, naming the resolved `gate.command`, carrying a Task Router row to the Primitives catalogue where there is one, every path the Task Router points at existing, and the skills directory, `skills-lock.json` and the `.claude/skills` links in step where the repository has them.",
175
+ "description": "The agent map: within its byte budget, naming the resolved `gate.command`, carrying a Task Router row to the Primitives catalogue where there is one, every path the Task Router points at existing, `CLAUDE.md` a symbolic link to the map unless the map is `CLAUDE.md`, and the skills directory, `skills-lock.json` and the `.claude/skills` links in step where the repository has them.",
171
176
  "type": ["boolean", "object"],
172
177
  "properties": {
173
178
  "file": {
@@ -175,25 +180,45 @@
175
180
  "type": "string",
176
181
  "minLength": 1
177
182
  },
178
- "maxLines": {
179
- "description": "Default 150.",
180
- "type": "integer",
181
- "minimum": 1
182
- },
183
183
  "maxBytes": {
184
- "description": "Default 10240.",
184
+ "description": "The map's budget in bytes. Default 10240.",
185
185
  "type": "integer",
186
186
  "minimum": 1
187
187
  }
188
188
  },
189
189
  "additionalProperties": false
190
190
  },
191
+ "routed-docs": {
192
+ "description": "Every document a task leads to: each file `files` matches and `exempt` does not is reached by a Task Router row of the agent map, through a code span in a cell after the first naming it, a directory above it or a glob that matches it, or by the `docs` of a Primitive in the catalogue naming it, its heading anchor dropped; a path reads from the root, `./` or `/` before it or not. The map is the file `agents-map` reads and the catalogue the one `primitives` reads, each Check on or off; a catalogue that is missing or invalid reaches nothing. A map with no Task Router table fails once on the map, and a missing map leaves the Gate unable to run.",
193
+ "type": ["boolean", "object"],
194
+ "properties": {
195
+ "files": {
196
+ "description": "Globs of the documents, against the files git tracks or would track; `*` and `**` skip a name that starts with a dot. Default `docs/**/*.md`.",
197
+ "type": "array",
198
+ "items": {
199
+ "type": "string",
200
+ "minLength": 1
201
+ },
202
+ "uniqueItems": true
203
+ },
204
+ "exempt": {
205
+ "description": "Globs of the documents left out. Default `docs/research/**`: a research note is read through the spec it backs. A list replaces the default.",
206
+ "type": "array",
207
+ "items": {
208
+ "type": "string",
209
+ "minLength": 1
210
+ },
211
+ "uniqueItems": true
212
+ }
213
+ },
214
+ "additionalProperties": false
215
+ },
191
216
  "markdown-budget": {
192
- "description": "Each file a glob of `budgets` matches within that glob's `maxLines` and `maxBytes`, and every glob matching a file. A glob matches the files git tracks or would track, a symbolic link left out when the file it points at is matched too; `*` and `**` skip a name that starts with a dot.",
217
+ "description": "Each file a glob of `budgets` matches within that glob's `maxBytes`, and every glob matching a file. A glob matches the files git tracks or would track, a symbolic link left out when the file it points at is matched too; `*` and `**` skip a name that starts with a dot.",
193
218
  "type": ["boolean", "object"],
194
219
  "properties": {
195
220
  "budgets": {
196
- "description": "Default `docs/agents/*.md` within 100 lines and 6144 bytes; a list replaces the default.",
221
+ "description": "Default `docs/agents/*.md` within 6144 bytes; a list replaces the default, such as one that adds `GLOSSARY.md` at a budget of its own.",
197
222
  "type": "array",
198
223
  "items": {
199
224
  "type": "object",
@@ -202,16 +227,12 @@
202
227
  "type": "string",
203
228
  "minLength": 1
204
229
  },
205
- "maxLines": {
206
- "type": "integer",
207
- "minimum": 1
208
- },
209
230
  "maxBytes": {
210
231
  "type": "integer",
211
232
  "minimum": 1
212
233
  }
213
234
  },
214
- "required": ["glob", "maxLines", "maxBytes"],
235
+ "required": ["glob", "maxBytes"],
215
236
  "additionalProperties": false
216
237
  }
217
238
  }
@@ -289,16 +310,24 @@
289
310
  "minLength": 1
290
311
  },
291
312
  "uniqueItems": true
313
+ },
314
+ "fields": {
315
+ "description": "The frontmatter fields each record is held to beyond `status`; none by default. `date`: every record carries the day it was decided, `YYYY-MM-DD`. `touches`: a record in force, its status `accepted` or `active`, names the paths it governs. Each is a file or directory git tracks or would track, or a glob that matches such a file. A `superseded` record names none. `superseded-by`: a `superseded` record names the number of each record that replaces it. `amended-by`: each number it names is the number of a record. A number names a record in the same directory. A list is comma-separated after its key, or one `- ` item a line under it.",
316
+ "type": "array",
317
+ "items": {
318
+ "enum": ["date", "touches", "superseded-by", "amended-by"]
319
+ },
320
+ "uniqueItems": true
292
321
  }
293
322
  },
294
323
  "additionalProperties": false
295
324
  },
296
325
  "file-size-ratchet": {
297
- "description": "The file-size Ratchet: each file git tracks or would track within the line limit of its group, or, where `.ai/file-size-ratchet.baseline.json` lists it, within the count recorded there. A file belongs to the first group with a glob that matches it; a file no group matches has no limit, and a symbolic link is left out when the file it points at is matched too. The baseline is committed and lists the files over their limit; one that lists a file twice, at a count within its group's limit or in no group fails the Check. `dw-sdlc gate --update-baseline` records it where there is none and otherwise only tightens it, and only when every Step and every other Check passes: a file within its limit leaves it, a count that shrank is lowered, and no file joins.",
326
+ "description": "The file-size Ratchet, which holds code: each file git tracks or would track within the line limit of its group, or, where `.ai/file-size-ratchet.baseline.json` lists it, within the count recorded there. A file belongs to the first group with a glob that matches it; a file no group matches has no limit, and a symbolic link is left out when the file it points at is matched too. The baseline is committed and lists the files over their limit; one that lists a file twice, at a count within its group's limit or in no group fails the Check. `dw-sdlc gate --update-baseline` records it where there is none and otherwise only tightens it, and only when every Step and every other Check passes: a file within its limit leaves it, a count that shrank is lowered, and no file joins.",
298
327
  "type": ["boolean", "object"],
299
328
  "properties": {
300
329
  "groups": {
301
- "description": "Default, in this order: `tests`, 2000 lines, a file under a `test`, `tests` or `__tests__` directory or named `*.test.*` or `*.spec.*`; `agent-docs`, 120 lines, `AGENTS.md`, `CLAUDE.md` and `GLOSSARY.md` anywhere and the markdown under `docs/agents`; `source`, 500 lines, JavaScript, TypeScript and shell.",
330
+ "description": "Default, in this order: `tests`, 2000 lines, a file under a `test`, `tests` or `__tests__` directory or named `*.test.*` or `*.spec.*`; `source`, 500 lines, JavaScript, TypeScript and shell. No default group holds markdown: `agents-map` and `markdown-budget` hold it to byte budgets.",
302
331
  "type": "array",
303
332
  "items": {
304
333
  "type": "object",
@@ -358,12 +387,42 @@
358
387
  "additionalProperties": false
359
388
  },
360
389
  "pr": {
361
- "description": "How the Ship script, `dw-sdlc pr ship`, ends a pull request. The local override may tighten it and never loosen it.",
390
+ "description": "How the Ship script, `dw-sdlc pr ship`, ends a pull request. The local override may tighten `merge` and `review` and never loosen them.",
362
391
  "type": "object",
363
392
  "properties": {
364
393
  "merge": {
365
394
  "description": "Who merges a pull request that is green, reviewed and authored by the account `gh` runs as. `auto`: the script squash merges it, naming the head it read. `ask`: it parks with the merge command for the dev. Default `ask`, which an invalid layer runs too. Strictest first: ask, auto.",
366
395
  "enum": ["ask", "auto"]
396
+ },
397
+ "review": {
398
+ "description": "The Review depth of a head the Ship script finds unreviewed, which the dev's `--review` overrides for one call. `full`: the agent runs the command `pr.reviewCommand` names over the changes. `light`: one `sdlc-reviewer` subagent reads the diff at medium effort. `off`: the script posts the `dw-sdlc/review` success itself and no one reads them. `auto`: built-in rules route the depth from the changes the Review would read: lockfiles alone `off`; a file under `.github/workflows/`, more than 100 changed lines outside lockfiles, or a change the script cannot size, more files than GitHub's compare lists or a file outside lockfiles with no patch, `full`; the rest `light`. Default `auto`; an invalid layer runs `full`. Strictest first: full, auto, light, off.",
399
+ "enum": ["full", "auto", "light", "off"]
400
+ },
401
+ "reviewCommand": {
402
+ "description": "The slash command a `full` Review runs over the pull request's changes, as `/pr-review-toolkit:review-pr code errors`. Default `/code-review`. No strictness order: the local override sets it freely.",
403
+ "type": "string",
404
+ "minLength": 1
405
+ }
406
+ },
407
+ "additionalProperties": false
408
+ },
409
+ "worker": {
410
+ "description": "How a Worker's Claude Code session starts: the command, the model and the effort. No strictness order, as with `pr.reviewCommand`: any layer sets any key freely.",
411
+ "type": "object",
412
+ "properties": {
413
+ "command": {
414
+ "description": "The command a Worker starts with: the words that start its Claude Code session, before the flags of its own, such as `--model` and `--effort`. Default `claude`. It belongs to one machine, so it sits in the local layer, where `dw-sdlc setup` writes `cswap run -- --dangerously-skip-permissions` when `cswap` is on PATH.",
415
+ "type": "string",
416
+ "minLength": 1
417
+ },
418
+ "model": {
419
+ "description": "The model a Worker runs on, as Claude Code's `--model` takes it. Default `sonnet`.",
420
+ "type": "string",
421
+ "minLength": 1
422
+ },
423
+ "effort": {
424
+ "description": "The effort a Worker runs at, one of Claude Code's effort levels. Default `high`.",
425
+ "enum": ["low", "medium", "high", "xhigh", "max"]
367
426
  }
368
427
  },
369
428
  "additionalProperties": false
@@ -17,6 +17,33 @@ export type EnvFiles = {
17
17
  };
18
18
  /** Who merges a pull request the Ship script finds ready: the script under `auto`, the dev under `ask`. */
19
19
  export type PrMerge = "ask" | "auto";
20
+ /**
21
+ * How closely a Review reads an unreviewed head: the command `pr.reviewCommand` names under `full`,
22
+ * one `sdlc-reviewer` subagent under `light`, no one under `off`.
23
+ */
24
+ export type ReviewDepth = "full" | "light" | "off";
25
+ /** The Review depths, strictest first: what the dev's `--review` names. */
26
+ export declare const REVIEW_DEPTHS: readonly ["full", "light", "off"];
27
+ /** `pr.review`: a Review depth, or `auto`, which routes one from the changes by built-in rules. */
28
+ export type PrReview = ReviewDepth | "auto";
29
+ /**
30
+ * How the Ship script ends a pull request: who merges it, how closely it is reviewed, and the slash
31
+ * command a `full` Review runs.
32
+ */
33
+ export type PrConfig = {
34
+ merge: PrMerge;
35
+ review: PrReview;
36
+ reviewCommand: string;
37
+ };
38
+ /** Claude Code's effort levels, lowest first: what `worker.effort` takes. */
39
+ declare const WORKER_EFFORTS: readonly ["low", "medium", "high", "xhigh", "max"];
40
+ export type WorkerEffort = (typeof WORKER_EFFORTS)[number];
41
+ /** How a Worker's Claude Code session starts: the words that start it, its model and its effort. */
42
+ export type WorkerConfig = {
43
+ command: string;
44
+ model: string;
45
+ effort: WorkerEffort;
46
+ };
20
47
  export type Guardrails = {
21
48
  forcePush: ForcePush;
22
49
  commitOnProtected: CommitOnProtected;
@@ -40,16 +67,20 @@ export type GateChecks = {
40
67
  run: RunCheck[];
41
68
  [check: string]: unknown;
42
69
  };
43
- /** The Gate: its command, the steps run in order, the Checks run after them. */
70
+ /** `gate.drift`: whether a Step may write the tree; `src/drift.ts` reads it. */
71
+ export type DriftSetting = "fail" | "allow";
72
+ /** The Gate: its command, the steps run in order, whether they may write the tree, the Checks run after them. */
44
73
  export type Gate = {
45
74
  command?: string;
46
75
  steps: string[];
76
+ drift: DriftSetting;
47
77
  checks: GateChecks;
48
78
  };
49
79
  /** `gate` as one layer writes it: every key optional, `checks` Check by Check. */
50
80
  export type GateLayer = {
51
81
  command?: string;
52
82
  steps?: string[];
83
+ drift?: DriftSetting;
53
84
  checks?: Partial<GateChecks>;
54
85
  };
55
86
  /** One layer as written in a file: every key optional, unknown top-level keys tolerated. */
@@ -68,9 +99,8 @@ export type AgenticConfig = {
68
99
  trailer?: string;
69
100
  };
70
101
  gate?: GateLayer;
71
- pr?: {
72
- merge?: PrMerge;
73
- };
102
+ pr?: Partial<PrConfig>;
103
+ worker?: Partial<WorkerConfig>;
74
104
  worktree?: {
75
105
  setup?: string[];
76
106
  };
@@ -90,9 +120,8 @@ export type ResolvedConfig = {
90
120
  trailer?: string;
91
121
  };
92
122
  gate: Gate;
93
- pr: {
94
- merge: PrMerge;
95
- };
123
+ pr: PrConfig;
124
+ worker: WorkerConfig;
96
125
  worktree: {
97
126
  setup: string[];
98
127
  };
@@ -130,11 +159,16 @@ export declare const STRICTNESS: {
130
159
  readonly commitOnProtected: readonly ["deny", "allow"];
131
160
  readonly envFiles: readonly ["deny", "git"];
132
161
  readonly merge: readonly ["ask", "auto"];
162
+ readonly review: readonly ["full", "auto", "light", "off"];
133
163
  };
134
164
  /** What the guards run on while any layer is invalid. */
135
165
  export declare const STRICTEST_GUARDRAILS: Guardrails;
136
166
  /** What the Ship script runs on while any layer is invalid, and with nothing declared: it merges nothing itself. */
137
167
  export declare const STRICTEST_PR_MERGE: PrMerge;
168
+ /** The Review depth while any layer is invalid: the agent reads every head in full. */
169
+ export declare const STRICTEST_PR_REVIEW: PrReview;
170
+ /** The command a `full` Review runs with nothing declared; it has no strictness order. */
171
+ export declare const DEFAULT_REVIEW_COMMAND = "/code-review";
138
172
  /** What a repository gets with nothing declared. */
139
173
  export declare const DEFAULT_GUARDRAILS: Guardrails;
140
174
  /**
@@ -11,12 +11,17 @@ export const LOCAL_FILE = ".ai/agentic.config.local.json";
11
11
  export const SCHEMA_FILE = "config/agentic.config.schema.json";
12
12
  export const SCHEMA_PATH = join(import.meta.dirname, "..", SCHEMA_FILE);
13
13
  export const COMMIT_CONVENTIONS = ["ticket-key", "conventional"];
14
+ /** The Review depths, strictest first: what the dev's `--review` names. */
15
+ export const REVIEW_DEPTHS = ["full", "light", "off"];
16
+ /** Claude Code's effort levels, lowest first: what `worker.effort` takes. */
17
+ const WORKER_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
14
18
  /** Strictest value first; the local override may only move towards the front. */
15
19
  export const STRICTNESS = {
16
20
  forcePush: ["deny", "ask", "lease"],
17
21
  commitOnProtected: ["deny", "allow"],
18
22
  envFiles: ["deny", "git"],
19
- merge: ["ask", "auto"]
23
+ merge: ["ask", "auto"],
24
+ review: ["full", "auto", "light", "off"]
20
25
  };
21
26
  /** What the guards run on while any layer is invalid. */
22
27
  export const STRICTEST_GUARDRAILS = {
@@ -27,6 +32,15 @@ export const STRICTEST_GUARDRAILS = {
27
32
  };
28
33
  /** What the Ship script runs on while any layer is invalid, and with nothing declared: it merges nothing itself. */
29
34
  export const STRICTEST_PR_MERGE = "ask";
35
+ /** The Review depth while any layer is invalid: the agent reads every head in full. */
36
+ export const STRICTEST_PR_REVIEW = "full";
37
+ const STRICTEST_PR = { merge: STRICTEST_PR_MERGE, review: STRICTEST_PR_REVIEW };
38
+ /** The command a `full` Review runs with nothing declared; it has no strictness order. */
39
+ export const DEFAULT_REVIEW_COMMAND = "/code-review";
40
+ /** What a repository gets with nothing declared: the merge the dev's, the depth routed from the changes. */
41
+ const DEFAULT_PR = { merge: STRICTEST_PR_MERGE, review: "auto", reviewCommand: DEFAULT_REVIEW_COMMAND };
42
+ /** What a Worker starts with when no layer says otherwise; no key has a strictness order. */
43
+ const DEFAULT_WORKER = { command: "claude", model: "sonnet", effort: "high" };
30
44
  /** What a repository gets with nothing declared. */
31
45
  export const DEFAULT_GUARDRAILS = {
32
46
  forcePush: "lease",
@@ -172,9 +186,12 @@ function hasCheckScript(root) {
172
186
  const { scripts } = manifest;
173
187
  return typeof scripts === "object" && scripts !== null && "check" in scripts && typeof scripts.check === "string";
174
188
  }
175
- /** The Gate with nothing declared: `pnpm check` where the manifest has a `check` script, else no command. */
189
+ /**
190
+ * The Gate with nothing declared: `pnpm check` where the manifest has a `check` script, else no command;
191
+ * no Step, no Check, and the watch for Drift on.
192
+ */
176
193
  function inferGate(root) {
177
- const gate = { steps: [], checks: { run: [] } };
194
+ const gate = { steps: [], drift: "fail", checks: { run: [] } };
178
195
  if (hasCheckScript(root)) {
179
196
  gate.command = "pnpm check";
180
197
  }
@@ -193,7 +210,8 @@ export function inferConfig(root) {
193
210
  guardrails: cloneGuardrails(DEFAULT_GUARDRAILS),
194
211
  commit: { convention: "conventional" },
195
212
  gate: inferGate(root),
196
- pr: { merge: STRICTEST_PR_MERGE },
213
+ pr: { ...DEFAULT_PR },
214
+ worker: { ...DEFAULT_WORKER },
197
215
  worktree: { setup: [] }
198
216
  };
199
217
  }
@@ -210,6 +228,7 @@ function applyLayer(base, layer) {
210
228
  commit: { ...base.commit, ...layer.commit },
211
229
  gate: { ...base.gate, ...layer.gate, checks: { ...base.gate.checks, ...layer.gate?.checks } },
212
230
  pr: { ...base.pr, ...layer.pr },
231
+ worker: { ...base.worker, ...layer.worker },
213
232
  worktree: { setup: layer.worktree?.setup ?? base.worktree.setup }
214
233
  };
215
234
  }
@@ -268,13 +287,17 @@ function loosenings(base, local) {
268
287
  }
269
288
  return problems;
270
289
  }
271
- /** A local `pr.merge` less strict than the one it would override: `auto` over `ask`. */
290
+ /** Each local `pr` key less strict than the one it would override: `merge` auto over ask, `review` off over light over auto over full. */
272
291
  function prLoosenings(base, local) {
273
- if (local === undefined || rank("merge", local) <= rank("merge", base)) {
274
- return [];
275
- }
276
- const message = `${show(local)} loosens ${show(base)}; the local file may tighten pr.merge, never loosen it`;
277
- return [{ file: LOCAL_FILE, path: "pr.merge", message }];
292
+ const keys = ["merge", "review"];
293
+ return keys.flatMap((key) => {
294
+ const value = local?.[key];
295
+ if (value === undefined || rank(key, value) <= rank(key, base[key])) {
296
+ return [];
297
+ }
298
+ const message = `${show(value)} loosens ${show(base[key])}; the local file may tighten pr.${key}, never loosen it`;
299
+ return [{ file: LOCAL_FILE, path: `pr.${key}`, message }];
300
+ });
278
301
  }
279
302
  /** `commit.pattern` from the convention's preset when no layer set one; a set pattern wins. */
280
303
  function finalize(merged) {
@@ -299,7 +322,7 @@ export function resolveConfig(root) {
299
322
  if (local.value !== undefined) {
300
323
  const loosened = [
301
324
  ...loosenings(config.guardrails, local.value.guardrails),
302
- ...prLoosenings(config.pr.merge, local.value.pr?.merge)
325
+ ...prLoosenings(config.pr, local.value.pr)
303
326
  ];
304
327
  if (loosened.length > 0) {
305
328
  problems.push(...loosened);
@@ -310,7 +333,8 @@ export function resolveConfig(root) {
310
333
  }
311
334
  }
312
335
  if (problems.length > 0) {
313
- config = { ...config, guardrails: cloneGuardrails(STRICTEST_GUARDRAILS), pr: { merge: STRICTEST_PR_MERGE } };
336
+ // `reviewCommand` and `worker` have no strictest value: they keep what the valid layers name.
337
+ config = { ...config, guardrails: cloneGuardrails(STRICTEST_GUARDRAILS), pr: { ...config.pr, ...STRICTEST_PR } };
314
338
  }
315
339
  return {
316
340
  config: finalize(config),
@@ -1,7 +1,15 @@
1
1
  import { type Check } from "./check.ts";
2
2
  export type AgentsMapOptions = {
3
3
  file: string;
4
- maxLines: number;
5
4
  maxBytes: number;
6
5
  };
6
+ /** The rows of the Task Router in `text`, each its line and the paths it points at; none when it has no such table. */
7
+ export declare function routes(text: string): {
8
+ line: number;
9
+ paths: string[];
10
+ }[];
11
+ /** The text of the map at `file`; throws `CannotRun` when it does not exist. */
12
+ export declare function readMap(root: string, file: string): string;
7
13
  export declare const agentsMap: Check<AgentsMapOptions>;
14
+ /** The agent map a resolved `gate.checks` points at: the `agents-map` Check's file, on or off. */
15
+ export declare function mapFile(checks: Record<string, unknown>): string;
@@ -1,17 +1,19 @@
1
1
  // `agents-map`: the agent map stays a map. Within its budget, naming the Gate command, routing to the
2
- // Primitives catalogue, every path its Task Router points at existing, and the skills directory, its
3
- // lock and its links in step.
4
- import { existsSync, globSync, lstatSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
5
- import { join } from "node:path";
2
+ // Primitives catalogue, every path its Task Router points at existing, `CLAUDE.md` a link to it so
3
+ // Claude Code reads it, and the skills directory, its lock and its links in step.
4
+ import { existsSync, globSync, lstatSync, readdirSync, readFileSync, readlinkSync, realpathSync, statSync } from "node:fs";
5
+ import { join, normalize } from "node:path";
6
6
  import { resolveConfig } from "../agentic-config.js";
7
7
  import { codeSpans, tableRows } from "../markdown.js";
8
- import { CannotRun } from "./check.js";
8
+ import { CannotRun, optionsOf } from "./check.js";
9
9
  import { overBudget } from "./markdown-budget.js";
10
10
  import { catalogueFile } from "./primitives.js";
11
11
  const ROUTER = "Task Router";
12
12
  const AGENTS_SKILLS = ".agents/skills";
13
13
  const CLAUDE_SKILLS = ".claude/skills";
14
14
  const LOCK = "skills-lock.json";
15
+ /** The file Claude Code reads at the repository root, in place of `AGENTS.md`. */
16
+ const CLAUDE_MD = "CLAUDE.md";
15
17
  const REMEDIES = {
16
18
  missing: "write it, or point gate.checks.agents-map.file at the map",
17
19
  budget: `cut it, or move a topic into a doc a ${ROUTER} row points at`,
@@ -34,6 +36,14 @@ function exists(root, path) {
34
36
  function escape(text) {
35
37
  return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
36
38
  }
39
+ /** The rows of the Task Router in `text`, each its line and the paths it points at; none when it has no such table. */
40
+ export function routes(text) {
41
+ // The first column says when a row applies; the others say what to read.
42
+ return tableRows(text, ROUTER).map(({ cells, line }) => ({
43
+ line,
44
+ paths: codeSpans(cells.slice(1).join(" ")).filter(isPath)
45
+ }));
46
+ }
37
47
  /** The budget, the Gate command, the row to the catalogue and every path the Task Router points at. */
38
48
  function judgeMap(root, { file, ...budget }, text) {
39
49
  const findings = overBudget(text, budget).map((message) => ({ file, message, remedy: REMEDIES.budget }));
@@ -45,17 +55,13 @@ function judgeMap(root, { file, ...budget }, text) {
45
55
  const message = `does not name the Gate command ${JSON.stringify(gate.command)}`;
46
56
  findings.push({ file, message, remedy: REMEDIES.command });
47
57
  }
48
- // The first column says when a row applies; the others say what to read.
49
- const routes = tableRows(text, ROUTER).map(({ cells, line }) => ({
50
- line,
51
- paths: codeSpans(cells.slice(1).join(" ")).filter(isPath)
52
- }));
58
+ const rows = routes(text);
53
59
  const catalogue = catalogueFile(gate.checks);
54
- if (existsSync(join(root, catalogue)) && !routes.some(({ paths }) => paths.includes(catalogue))) {
60
+ if (existsSync(join(root, catalogue)) && !rows.some(({ paths }) => paths.includes(catalogue))) {
55
61
  const message = `no ${ROUTER} row points at ${catalogue}`;
56
62
  findings.push({ file, message, remedy: REMEDIES.catalogue });
57
63
  }
58
- for (const { line, paths } of routes) {
64
+ for (const { line, paths } of rows) {
59
65
  for (const missing of paths.filter((path) => !exists(root, path))) {
60
66
  const message = `the ${ROUTER} points at ${JSON.stringify(missing)}, which does not exist`;
61
67
  findings.push({ file, line, message, remedy: REMEDIES.route });
@@ -63,6 +69,31 @@ function judgeMap(root, { file, ...budget }, text) {
63
69
  }
64
70
  return findings;
65
71
  }
72
+ /**
73
+ * `CLAUDE.md` a link to the map by its path from the root, so the link holds in every clone; nothing
74
+ * when the map is `CLAUDE.md` itself.
75
+ */
76
+ function judgeLink(root, map) {
77
+ if (normalize(map) === CLAUDE_MD) {
78
+ return [];
79
+ }
80
+ const path = join(root, CLAUDE_MD);
81
+ const command = `ln -s ${map} ${CLAUDE_MD}`;
82
+ const stat = lstatSync(path, { throwIfNoEntry: false });
83
+ if (stat === undefined) {
84
+ return [{ file: CLAUDE_MD, message: "does not exist", remedy: `link it to the map: ${command}` }];
85
+ }
86
+ if (!stat.isSymbolicLink()) {
87
+ const message = `is a file of its own, not a link to ${map}`;
88
+ return [{ file: CLAUDE_MD, message, remedy: `move what it holds into ${map}, remove it, then ${command}` }];
89
+ }
90
+ const target = readlinkSync(path);
91
+ if (normalize(target) !== normalize(map)) {
92
+ const message = `links to ${JSON.stringify(target)}, not to ${map}`;
93
+ return [{ file: CLAUDE_MD, message, remedy: `remove it, then ${command}` }];
94
+ }
95
+ return [];
96
+ }
66
97
  /** The names of the directories in `dir`, sorted; empty when there is no `dir`. */
67
98
  function directories(dir) {
68
99
  if (!existsSync(dir)) {
@@ -145,15 +176,27 @@ function linksTo(link, target) {
145
176
  return false;
146
177
  }
147
178
  }
148
- function run(root, options) {
149
- const path = join(root, options.file);
179
+ /** The text of the map at `file`; throws `CannotRun` when it does not exist. */
180
+ export function readMap(root, file) {
181
+ const path = join(root, file);
150
182
  if (!existsSync(path)) {
151
- throw new CannotRun({ file: options.file, message: "the map does not exist", remedy: REMEDIES.missing });
183
+ throw new CannotRun({ file, message: "the map does not exist", remedy: REMEDIES.missing });
152
184
  }
153
- return [...judgeMap(root, options, readFileSync(path, "utf8")), ...judgeSkills(root)];
185
+ return readFileSync(path, "utf8");
186
+ }
187
+ function run(root, options) {
188
+ return [
189
+ ...judgeMap(root, options, readMap(root, options.file)),
190
+ ...judgeLink(root, options.file),
191
+ ...judgeSkills(root)
192
+ ];
154
193
  }
155
194
  export const agentsMap = {
156
195
  name: "agents-map",
157
- defaults: { file: "AGENTS.md", maxLines: 150, maxBytes: 10240 },
196
+ defaults: { file: "AGENTS.md", maxBytes: 10240 },
158
197
  run
159
198
  };
199
+ /** The agent map a resolved `gate.checks` points at: the `agents-map` Check's file, on or off. */
200
+ export function mapFile(checks) {
201
+ return (optionsOf(agentsMap, checks["agents-map"]) ?? agentsMap.defaults).file;
202
+ }
@@ -1,6 +1,11 @@
1
1
  import { type Check } from "./check.ts";
2
+ /** The frontmatter fields beyond `status` a record can be held to, in the order they are judged. */
3
+ declare const FIELDS: readonly ["date", "touches", "superseded-by", "amended-by"];
4
+ export type DecisionField = (typeof FIELDS)[number];
2
5
  export type DecisionRecordsOptions = {
3
6
  dirs: string[];
4
7
  statuses: string[];
8
+ fields: DecisionField[];
5
9
  };
6
10
  export declare const decisionRecords: Check<DecisionRecordsOptions>;
11
+ export {};