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.
- package/README.md +23 -12
- package/config/agentic.config.schema.json +77 -18
- package/dist/agentic-config.d.ts +41 -7
- package/dist/agentic-config.js +36 -12
- package/dist/checks/agents-map.d.ts +9 -1
- package/dist/checks/agents-map.js +60 -17
- package/dist/checks/decision-records.d.ts +5 -0
- package/dist/checks/decision-records.js +121 -16
- package/dist/checks/file-size-ratchet.js +7 -5
- package/dist/checks/index.js +2 -0
- package/dist/checks/markdown-budget.d.ts +2 -5
- package/dist/checks/markdown-budget.js +6 -15
- package/dist/checks/markdown-refs.js +1 -11
- package/dist/checks/routed-docs.d.ts +6 -0
- package/dist/checks/routed-docs.js +49 -0
- package/dist/checks/tree.d.ts +2 -0
- package/dist/checks/tree.js +13 -3
- package/dist/commands/check.d.ts +1 -1
- package/dist/commands/check.js +6 -4
- package/dist/commands/gate.d.ts +10 -4
- package/dist/commands/gate.js +202 -86
- package/dist/commands/pr-reviewed.js +6 -22
- package/dist/commands/pr-ship.js +119 -30
- package/dist/commands/references-sync.js +5 -6
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +17 -9
- package/dist/drift.d.ts +17 -0
- package/dist/drift.js +68 -0
- package/dist/gate-cache.d.ts +13 -0
- package/dist/gate-cache.js +39 -0
- package/dist/git-env.d.ts +2 -0
- package/dist/git-env.js +10 -0
- package/dist/markdown.d.ts +19 -0
- package/dist/markdown.js +46 -2
- package/dist/package-version.d.ts +2 -0
- package/dist/package-version.js +13 -0
- package/dist/pr/decide.d.ts +37 -9
- package/dist/pr/decide.js +113 -32
- package/dist/pr/delta.d.ts +11 -0
- package/dist/pr/delta.js +60 -0
- package/dist/pr/read.d.ts +8 -3
- package/dist/pr/read.js +56 -10
- package/dist/pr/route.d.ts +14 -0
- package/dist/pr/route.js +52 -0
- package/dist/pr/state.d.ts +48 -2
- package/dist/pr/state.js +42 -9
- package/dist/router-block.d.ts +4 -1
- package/dist/router-block.js +31 -7
- package/dist/run-command.d.ts +24 -0
- package/dist/run-command.js +121 -0
- package/dist/setup-gate.js +3 -12
- package/dist/setup-worker.d.ts +11 -0
- package/dist/setup-worker.js +37 -0
- package/dist/working-tree.d.ts +13 -0
- package/dist/working-tree.js +60 -0
- package/package.json +1 -1
- 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
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
47
|
-
|
|
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.
|
|
54
|
-
|
|
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
|
|
61
|
-
`dw-sdlc pr reviewed <n> --head <sha>` posts after a clean review. It posts
|
|
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
|
|
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
|
|
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 `
|
|
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
|
|
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", "
|
|
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.*`; `
|
|
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
|
|
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
|
package/dist/agentic-config.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|
package/dist/agentic-config.js
CHANGED
|
@@ -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
|
-
/**
|
|
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: {
|
|
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
|
-
/**
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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)) && !
|
|
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
|
|
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
|
-
|
|
149
|
-
|
|
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
|
|
183
|
+
throw new CannotRun({ file, message: "the map does not exist", remedy: REMEDIES.missing });
|
|
152
184
|
}
|
|
153
|
-
return
|
|
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",
|
|
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 {};
|