dw-sdlc 0.7.0 → 0.9.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 +20 -12
- package/config/agentic.config.schema.json +56 -18
- package/dist/agentic-config.d.ts +23 -7
- package/dist/agentic-config.js +26 -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 +5 -21
- package/dist/commands/pr-ship.js +37 -17
- package/dist/commands/references-sync.js +5 -6
- 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 +31 -8
- package/dist/pr/decide.js +65 -31
- 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/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,26 @@ 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`, the default, and no one does under `off`. The local layer may tighten
|
|
60
|
+
either and never loosen it; `--merge` acts as `auto` and `--review full|off` sets the depth for one
|
|
61
|
+
call. A pull request the `gh` account did not open is always the dev's to merge.
|
|
55
62
|
|
|
56
63
|
```json
|
|
57
|
-
{ "pr": { "merge": "auto" } }
|
|
64
|
+
{ "pr": { "merge": "auto", "review": "full" } }
|
|
58
65
|
```
|
|
59
66
|
|
|
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.
|
|
67
|
+
`pr ship` answers `review`, its depth under `details`, until the head carries the commit status
|
|
68
|
+
`dw-sdlc/review`, which `dw-sdlc pr reviewed <n> --head <sha>` posts after a clean review. It posts
|
|
69
|
+
nothing and exits 2 once a push moved the head, so a new head needs a fresh review. Under `off` the
|
|
70
|
+
script posts that status itself, its description saying why, and goes on.
|
|
63
71
|
|
|
64
72
|
## Commands
|
|
65
73
|
|
|
@@ -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,21 @@
|
|
|
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 reviews the changes. `off`: the script posts the `dw-sdlc/review` success itself and no one reads them. Default `full`, which an invalid layer runs too. Strictest first: full, off.",
|
|
399
|
+
"enum": ["full", "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
|
|
367
405
|
}
|
|
368
406
|
},
|
|
369
407
|
"additionalProperties": false
|
package/dist/agentic-config.d.ts
CHANGED
|
@@ -17,6 +17,17 @@ 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
|
+
/** How closely a Review reads an unreviewed head: the agent reads it under `full`, no one under `off`. */
|
|
21
|
+
export type PrReview = "full" | "off";
|
|
22
|
+
/**
|
|
23
|
+
* How the Ship script ends a pull request: who merges it, how closely it is reviewed, and the slash
|
|
24
|
+
* command a `full` Review runs.
|
|
25
|
+
*/
|
|
26
|
+
export type PrConfig = {
|
|
27
|
+
merge: PrMerge;
|
|
28
|
+
review: PrReview;
|
|
29
|
+
reviewCommand: string;
|
|
30
|
+
};
|
|
20
31
|
export type Guardrails = {
|
|
21
32
|
forcePush: ForcePush;
|
|
22
33
|
commitOnProtected: CommitOnProtected;
|
|
@@ -40,16 +51,20 @@ export type GateChecks = {
|
|
|
40
51
|
run: RunCheck[];
|
|
41
52
|
[check: string]: unknown;
|
|
42
53
|
};
|
|
43
|
-
/**
|
|
54
|
+
/** `gate.drift`: whether a Step may write the tree; `src/drift.ts` reads it. */
|
|
55
|
+
export type DriftSetting = "fail" | "allow";
|
|
56
|
+
/** The Gate: its command, the steps run in order, whether they may write the tree, the Checks run after them. */
|
|
44
57
|
export type Gate = {
|
|
45
58
|
command?: string;
|
|
46
59
|
steps: string[];
|
|
60
|
+
drift: DriftSetting;
|
|
47
61
|
checks: GateChecks;
|
|
48
62
|
};
|
|
49
63
|
/** `gate` as one layer writes it: every key optional, `checks` Check by Check. */
|
|
50
64
|
export type GateLayer = {
|
|
51
65
|
command?: string;
|
|
52
66
|
steps?: string[];
|
|
67
|
+
drift?: DriftSetting;
|
|
53
68
|
checks?: Partial<GateChecks>;
|
|
54
69
|
};
|
|
55
70
|
/** One layer as written in a file: every key optional, unknown top-level keys tolerated. */
|
|
@@ -68,9 +83,7 @@ export type AgenticConfig = {
|
|
|
68
83
|
trailer?: string;
|
|
69
84
|
};
|
|
70
85
|
gate?: GateLayer;
|
|
71
|
-
pr?:
|
|
72
|
-
merge?: PrMerge;
|
|
73
|
-
};
|
|
86
|
+
pr?: Partial<PrConfig>;
|
|
74
87
|
worktree?: {
|
|
75
88
|
setup?: string[];
|
|
76
89
|
};
|
|
@@ -90,9 +103,7 @@ export type ResolvedConfig = {
|
|
|
90
103
|
trailer?: string;
|
|
91
104
|
};
|
|
92
105
|
gate: Gate;
|
|
93
|
-
pr:
|
|
94
|
-
merge: PrMerge;
|
|
95
|
-
};
|
|
106
|
+
pr: PrConfig;
|
|
96
107
|
worktree: {
|
|
97
108
|
setup: string[];
|
|
98
109
|
};
|
|
@@ -130,11 +141,16 @@ export declare const STRICTNESS: {
|
|
|
130
141
|
readonly commitOnProtected: readonly ["deny", "allow"];
|
|
131
142
|
readonly envFiles: readonly ["deny", "git"];
|
|
132
143
|
readonly merge: readonly ["ask", "auto"];
|
|
144
|
+
readonly review: readonly ["full", "off"];
|
|
133
145
|
};
|
|
134
146
|
/** What the guards run on while any layer is invalid. */
|
|
135
147
|
export declare const STRICTEST_GUARDRAILS: Guardrails;
|
|
136
148
|
/** What the Ship script runs on while any layer is invalid, and with nothing declared: it merges nothing itself. */
|
|
137
149
|
export declare const STRICTEST_PR_MERGE: PrMerge;
|
|
150
|
+
/** The Review depth while any layer is invalid, and with nothing declared: the agent reads every head. */
|
|
151
|
+
export declare const STRICTEST_PR_REVIEW: PrReview;
|
|
152
|
+
/** The command a `full` Review runs with nothing declared; it has no strictness order. */
|
|
153
|
+
export declare const DEFAULT_REVIEW_COMMAND = "/code-review";
|
|
138
154
|
/** What a repository gets with nothing declared. */
|
|
139
155
|
export declare const DEFAULT_GUARDRAILS: Guardrails;
|
|
140
156
|
/**
|
package/dist/agentic-config.js
CHANGED
|
@@ -16,7 +16,8 @@ export const STRICTNESS = {
|
|
|
16
16
|
forcePush: ["deny", "ask", "lease"],
|
|
17
17
|
commitOnProtected: ["deny", "allow"],
|
|
18
18
|
envFiles: ["deny", "git"],
|
|
19
|
-
merge: ["ask", "auto"]
|
|
19
|
+
merge: ["ask", "auto"],
|
|
20
|
+
review: ["full", "off"]
|
|
20
21
|
};
|
|
21
22
|
/** What the guards run on while any layer is invalid. */
|
|
22
23
|
export const STRICTEST_GUARDRAILS = {
|
|
@@ -27,6 +28,11 @@ export const STRICTEST_GUARDRAILS = {
|
|
|
27
28
|
};
|
|
28
29
|
/** What the Ship script runs on while any layer is invalid, and with nothing declared: it merges nothing itself. */
|
|
29
30
|
export const STRICTEST_PR_MERGE = "ask";
|
|
31
|
+
/** The Review depth while any layer is invalid, and with nothing declared: the agent reads every head. */
|
|
32
|
+
export const STRICTEST_PR_REVIEW = "full";
|
|
33
|
+
const STRICTEST_PR = { merge: STRICTEST_PR_MERGE, review: STRICTEST_PR_REVIEW };
|
|
34
|
+
/** The command a `full` Review runs with nothing declared; it has no strictness order. */
|
|
35
|
+
export const DEFAULT_REVIEW_COMMAND = "/code-review";
|
|
30
36
|
/** What a repository gets with nothing declared. */
|
|
31
37
|
export const DEFAULT_GUARDRAILS = {
|
|
32
38
|
forcePush: "lease",
|
|
@@ -172,9 +178,12 @@ function hasCheckScript(root) {
|
|
|
172
178
|
const { scripts } = manifest;
|
|
173
179
|
return typeof scripts === "object" && scripts !== null && "check" in scripts && typeof scripts.check === "string";
|
|
174
180
|
}
|
|
175
|
-
/**
|
|
181
|
+
/**
|
|
182
|
+
* The Gate with nothing declared: `pnpm check` where the manifest has a `check` script, else no command;
|
|
183
|
+
* no Step, no Check, and the watch for Drift on.
|
|
184
|
+
*/
|
|
176
185
|
function inferGate(root) {
|
|
177
|
-
const gate = { steps: [], checks: { run: [] } };
|
|
186
|
+
const gate = { steps: [], drift: "fail", checks: { run: [] } };
|
|
178
187
|
if (hasCheckScript(root)) {
|
|
179
188
|
gate.command = "pnpm check";
|
|
180
189
|
}
|
|
@@ -193,7 +202,7 @@ export function inferConfig(root) {
|
|
|
193
202
|
guardrails: cloneGuardrails(DEFAULT_GUARDRAILS),
|
|
194
203
|
commit: { convention: "conventional" },
|
|
195
204
|
gate: inferGate(root),
|
|
196
|
-
pr: {
|
|
205
|
+
pr: { ...STRICTEST_PR, reviewCommand: DEFAULT_REVIEW_COMMAND },
|
|
197
206
|
worktree: { setup: [] }
|
|
198
207
|
};
|
|
199
208
|
}
|
|
@@ -268,13 +277,17 @@ function loosenings(base, local) {
|
|
|
268
277
|
}
|
|
269
278
|
return problems;
|
|
270
279
|
}
|
|
271
|
-
/**
|
|
280
|
+
/** Each local `pr` key less strict than the one it would override: `merge` auto over ask, `review` off over full. */
|
|
272
281
|
function prLoosenings(base, local) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
282
|
+
const keys = ["merge", "review"];
|
|
283
|
+
return keys.flatMap((key) => {
|
|
284
|
+
const value = local?.[key];
|
|
285
|
+
if (value === undefined || rank(key, value) <= rank(key, base[key])) {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
const message = `${show(value)} loosens ${show(base[key])}; the local file may tighten pr.${key}, never loosen it`;
|
|
289
|
+
return [{ file: LOCAL_FILE, path: `pr.${key}`, message }];
|
|
290
|
+
});
|
|
278
291
|
}
|
|
279
292
|
/** `commit.pattern` from the convention's preset when no layer set one; a set pattern wins. */
|
|
280
293
|
function finalize(merged) {
|
|
@@ -299,7 +312,7 @@ export function resolveConfig(root) {
|
|
|
299
312
|
if (local.value !== undefined) {
|
|
300
313
|
const loosened = [
|
|
301
314
|
...loosenings(config.guardrails, local.value.guardrails),
|
|
302
|
-
...prLoosenings(config.pr
|
|
315
|
+
...prLoosenings(config.pr, local.value.pr)
|
|
303
316
|
];
|
|
304
317
|
if (loosened.length > 0) {
|
|
305
318
|
problems.push(...loosened);
|
|
@@ -310,7 +323,8 @@ export function resolveConfig(root) {
|
|
|
310
323
|
}
|
|
311
324
|
}
|
|
312
325
|
if (problems.length > 0) {
|
|
313
|
-
|
|
326
|
+
// `reviewCommand` has no strictest value: it keeps what the valid layers name.
|
|
327
|
+
config = { ...config, guardrails: cloneGuardrails(STRICTEST_GUARDRAILS), pr: { ...config.pr, ...STRICTEST_PR } };
|
|
314
328
|
}
|
|
315
329
|
return {
|
|
316
330
|
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 {};
|