dw-sdlc 0.20.0 → 0.21.1

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 (97) hide show
  1. package/README.md +13 -13
  2. package/config/agentic.config.schema.json +1 -1
  3. package/dist/agentic-config.d.ts +16 -0
  4. package/dist/agentic-config.js +28 -23
  5. package/dist/cannot-run.d.ts +27 -0
  6. package/dist/cannot-run.js +48 -0
  7. package/dist/catalogue.js +2 -1
  8. package/dist/checks/agents-map.js +7 -8
  9. package/dist/checks/check.d.ts +10 -8
  10. package/dist/checks/check.js +9 -7
  11. package/dist/checks/decision-records.js +3 -2
  12. package/dist/checks/file-size-ratchet.js +13 -17
  13. package/dist/checks/index.d.ts +6 -5
  14. package/dist/checks/index.js +10 -9
  15. package/dist/checks/markdown-refs.js +58 -6
  16. package/dist/checks/primitives.js +5 -5
  17. package/dist/checks/routed-docs.js +1 -3
  18. package/dist/checks/skill-sections.js +12 -20
  19. package/dist/checks/tree.js +7 -7
  20. package/dist/cli.d.ts +2 -1
  21. package/dist/cli.js +26 -5
  22. package/dist/commands/check.js +27 -42
  23. package/dist/commands/config.js +22 -27
  24. package/dist/commands/doctor.d.ts +2 -0
  25. package/dist/commands/doctor.js +11 -18
  26. package/dist/commands/gate.js +35 -40
  27. package/dist/commands/lead-start.js +28 -42
  28. package/dist/commands/pr-reviewed.js +41 -59
  29. package/dist/commands/pr-ship.js +72 -87
  30. package/dist/commands/primitives.js +38 -40
  31. package/dist/commands/recap.js +23 -74
  32. package/dist/commands/references-setup.js +28 -76
  33. package/dist/commands/references-sync.js +29 -53
  34. package/dist/commands/setup.d.ts +2 -7
  35. package/dist/commands/setup.js +38 -90
  36. package/dist/commands/sweep.js +46 -157
  37. package/dist/commands/worker-ls.js +15 -31
  38. package/dist/commands/worker-report.js +36 -52
  39. package/dist/commands/worker-start.js +43 -57
  40. package/dist/drift.js +2 -1
  41. package/dist/exec.d.ts +20 -0
  42. package/dist/exec.js +37 -4
  43. package/dist/gate-cache.js +2 -1
  44. package/dist/git.d.ts +32 -0
  45. package/dist/git.js +60 -0
  46. package/dist/json-schema.js +1 -3
  47. package/dist/landed.js +11 -22
  48. package/dist/paused-operation.d.ts +2 -0
  49. package/dist/paused-operation.js +17 -0
  50. package/dist/pins.js +2 -1
  51. package/dist/pr/decide.d.ts +4 -16
  52. package/dist/pr/decide.js +48 -83
  53. package/dist/pr/delta.d.ts +3 -4
  54. package/dist/pr/delta.js +11 -20
  55. package/dist/pr/read.d.ts +27 -30
  56. package/dist/pr/read.js +96 -158
  57. package/dist/pr/reply.d.ts +34 -0
  58. package/dist/pr/reply.js +112 -0
  59. package/dist/pr/route.d.ts +11 -3
  60. package/dist/pr/route.js +5 -5
  61. package/dist/pr/standing.d.ts +138 -0
  62. package/dist/pr/standing.js +181 -0
  63. package/dist/pr/state.d.ts +14 -69
  64. package/dist/pr/state.js +75 -148
  65. package/dist/pr/upstream.d.ts +1 -6
  66. package/dist/pr/upstream.js +2 -2
  67. package/dist/primitives-diff.js +1 -1
  68. package/dist/recap-next.js +34 -47
  69. package/dist/recap-primitives.js +1 -1
  70. package/dist/references-key.d.ts +0 -1
  71. package/dist/references-key.js +3 -4
  72. package/dist/run-command.d.ts +4 -1
  73. package/dist/run-command.js +32 -11
  74. package/dist/setup-gate.d.ts +1 -7
  75. package/dist/setup-rules.js +2 -6
  76. package/dist/setup-writer.d.ts +32 -0
  77. package/dist/setup-writer.js +60 -0
  78. package/dist/shell-line.d.ts +2 -0
  79. package/dist/shell-line.js +5 -0
  80. package/dist/sweep-removal.d.ts +31 -0
  81. package/dist/sweep-removal.js +133 -0
  82. package/dist/sweep-terminals.d.ts +1 -1
  83. package/dist/sweep-terminals.js +21 -57
  84. package/dist/sweep-trees.d.ts +19 -3
  85. package/dist/sweep-trees.js +67 -31
  86. package/dist/worker/herdr.d.ts +67 -23
  87. package/dist/worker/herdr.js +126 -77
  88. package/dist/worker/launch.js +32 -45
  89. package/dist/worker/origin.d.ts +1 -1
  90. package/dist/worker/origin.js +9 -11
  91. package/dist/worker/tabs.js +11 -7
  92. package/dist/working-tree.d.ts +0 -8
  93. package/dist/working-tree.js +6 -22
  94. package/dist/worktree-lock.js +1 -1
  95. package/package.json +1 -1
  96. package/dist/git-env.d.ts +0 -2
  97. package/dist/git-env.js +0 -10
package/README.md CHANGED
@@ -65,19 +65,19 @@ line; a `gate.checks.run` entry takes a `hint` beside `name` and `command`.
65
65
  Under `gate.checks`, a Check is `true` for its defaults, an object of its options, or `false`. The
66
66
  schema's description of each one names its options and defaults.
67
67
 
68
- | Check | fails | reads first |
69
- | -------------------- | --------------------------------------------------------------------------- | ------------------------- |
70
- | `markdown-refs` | a relative link or image whose path or heading anchor does not resolve | nothing |
71
- | `temporal-language` | a changelog phrase in a durable document, outside code | nothing |
72
- | `comment-budget` | a comment block over 3 lines of prose in JS, TS or shell | nothing |
73
- | `disable-directives` | a linter or type-checker disable comment with no reason | nothing |
74
- | `agents-map` | an `AGENTS.md` over budget, missing the Gate command or pointing at nothing | `AGENTS.md` |
75
- | `primitives` | a catalogue entry out of step with the tree, or a file no entry owns | `.ai/primitives.json` |
76
- | `routed-docs` | a document under `docs/` no Task Router row or Primitive leads to | the map, the catalogue |
77
- | `markdown-budget` | a markdown file over the byte budget of its glob | the files its globs match |
78
- | `decision-records` | a decision record misnamed, misnumbered or without a valid status | a records directory |
79
- | `file-size-ratchet` | a code file over its line limit that the baseline does not allow | its baseline |
80
- | `skill-sections` | a README section a skill copies that differs from its source | the catalogue |
68
+ | Check | fails | reads first |
69
+ | -------------------- | ------------------------------------------------------------------------------ | ------------------------- |
70
+ | `markdown-refs` | a relative link, image or repository URL whose path or anchor does not resolve | nothing |
71
+ | `temporal-language` | a changelog phrase in a durable document, outside code | nothing |
72
+ | `comment-budget` | a comment block over 3 lines of prose in JS, TS or shell | nothing |
73
+ | `disable-directives` | a linter or type-checker disable comment with no reason | nothing |
74
+ | `agents-map` | an `AGENTS.md` over budget, missing the Gate command or pointing at nothing | `AGENTS.md` |
75
+ | `primitives` | a catalogue entry out of step with the tree, or a file no entry owns | `.ai/primitives.json` |
76
+ | `routed-docs` | a document under `docs/` no Task Router row or Primitive leads to | the map, the catalogue |
77
+ | `markdown-budget` | a markdown file over the byte budget of its glob | the files its globs match |
78
+ | `decision-records` | a decision record misnamed, misnumbered or without a valid status | a records directory |
79
+ | `file-size-ratchet` | a code file over its line limit that the baseline does not allow | its baseline |
80
+ | `skill-sections` | a README section a skill copies that differs from its source | the catalogue |
81
81
 
82
82
  Each Check's options and defaults:
83
83
  [its description in the schema](https://github.com/dominikwozniak/dw-sdlc/blob/main/packages/dw-sdlc/config/agentic.config.schema.json).
@@ -264,7 +264,7 @@
264
264
  "additionalProperties": false
265
265
  },
266
266
  "markdown-refs": {
267
- "description": "Every relative link and image in the markdown files resolves: its path to a file or directory git tracks or would track, its heading anchor to a heading of the markdown file it names, and `#anchor` alone to a heading of its own file. A link with a scheme, such as `https:`, and a link inside code are not judged.",
267
+ "description": "Every relative link and image in the markdown files resolves: its path to a file or directory git tracks or would track, its heading anchor to a heading of the markdown file it names, and `#anchor` alone to a heading of its own file. A URL into the GitHub repository `origin` names, at its default branch (the one `origin/HEAD` names, else `main`), as a `blob`, `tree` or raw file URL, resolves the same way, inside code too. Any other link with a scheme, such as `https:`, and a relative link inside code are not judged.",
268
268
  "type": ["boolean", "object"],
269
269
  "properties": {
270
270
  "files": {
@@ -8,6 +8,7 @@ export type CommitConvention = (typeof COMMIT_CONVENTIONS)[number];
8
8
  export type NodePackageManager = "pnpm" | "npm" | "yarn" | "bun";
9
9
  export type ForcePush = "deny" | "ask" | "lease";
10
10
  export type CommitOnProtected = "deny" | "allow";
11
+ export type FilteredOutput = "deny" | "allow";
11
12
  export type EnvPolicy = "deny" | "git";
12
13
  /** Which env files may be read: `deny` list, then `allow` list, then `policy` for the rest. */
13
14
  export type EnvFiles = {
@@ -47,6 +48,7 @@ export type WorkerConfig = {
47
48
  export type Guardrails = {
48
49
  forcePush: ForcePush;
49
50
  commitOnProtected: CommitOnProtected;
51
+ filteredOutput: FilteredOutput;
50
52
  envFiles: EnvFiles;
51
53
  protectedBranches: string[];
52
54
  };
@@ -159,6 +161,7 @@ export type Resolution = {
159
161
  export declare const STRICTNESS: {
160
162
  readonly forcePush: readonly ["deny", "ask", "lease"];
161
163
  readonly commitOnProtected: readonly ["deny", "allow"];
164
+ readonly filteredOutput: readonly ["deny", "allow"];
162
165
  readonly envFiles: readonly ["deny", "git"];
163
166
  readonly merge: readonly ["ask", "auto"];
164
167
  readonly review: readonly ["full", "auto", "light", "off"];
@@ -206,8 +209,21 @@ export declare function mainCheckout(root: string): string | undefined;
206
209
  * one, and `resolveConfig` fills `commit.pattern` from it when no layer sets a pattern.
207
210
  */
208
211
  export declare function inferConfig(root: string): ResolvedConfig;
212
+ type CommitRules = {
213
+ commit: {
214
+ convention?: string;
215
+ pattern?: string;
216
+ trailer?: string;
217
+ };
218
+ };
219
+ /**
220
+ * What a report says is in force while a layer is invalid: the strictest guardrails, and no commit
221
+ * rule where no valid layer sets a convention, pattern or trailer, since the inferred layer names none.
222
+ */
223
+ export declare function invalidLayerEffect({ commit }: CommitRules): string;
209
224
  /**
210
225
  * Local over committed over inferred, for the repository at `root`. A linked worktree reads a layer it
211
226
  * lacks from its main checkout, where a repository that keeps `.ai/` out of git has the only copy.
212
227
  */
213
228
  export declare function resolveConfig(root: string): Resolution;
229
+ export {};
@@ -3,7 +3,8 @@
3
3
  // `hooks/config.jq` is the same merge for the guards; this file is the reference, and the tests run both.
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
- import { exec } from "./exec.js";
6
+ import { messageOf } from "./cannot-run.js";
7
+ import { asked, gitDirs } from "./git.js";
7
8
  import { validate } from "./json-schema.js";
8
9
  export const COMMITTED_FILE = ".ai/agentic.config.json";
9
10
  export const LOCAL_FILE = ".ai/agentic.config.local.json";
@@ -27,6 +28,7 @@ export function stepHint(step) {
27
28
  export const STRICTNESS = {
28
29
  forcePush: ["deny", "ask", "lease"],
29
30
  commitOnProtected: ["deny", "allow"],
31
+ filteredOutput: ["deny", "allow"],
30
32
  envFiles: ["deny", "git"],
31
33
  merge: ["ask", "auto"],
32
34
  review: ["full", "auto", "light", "off"]
@@ -35,6 +37,7 @@ export const STRICTNESS = {
35
37
  export const STRICTEST_GUARDRAILS = {
36
38
  forcePush: "deny",
37
39
  commitOnProtected: "deny",
40
+ filteredOutput: "deny",
38
41
  envFiles: { policy: "deny", allow: [], deny: [] },
39
42
  protectedBranches: ["master", "main", "production"]
40
43
  };
@@ -53,6 +56,7 @@ const DEFAULT_WORKER = { command: "claude", model: "sonnet", effort: "high" };
53
56
  export const DEFAULT_GUARDRAILS = {
54
57
  forcePush: "lease",
55
58
  commitOnProtected: "allow",
59
+ filteredOutput: "deny",
56
60
  envFiles: { policy: "git", allow: ["**/.env.example", "**/.env.sample", "**/.env.template"], deny: [] },
57
61
  protectedBranches: ["master", "main", "production"]
58
62
  };
@@ -60,6 +64,7 @@ function cloneGuardrails(source) {
60
64
  return {
61
65
  forcePush: source.forcePush,
62
66
  commitOnProtected: source.commitOnProtected,
67
+ filteredOutput: source.filteredOutput,
63
68
  envFiles: { policy: source.envFiles.policy, allow: [...source.envFiles.allow], deny: [...source.envFiles.deny] },
64
69
  protectedBranches: [...source.protectedBranches]
65
70
  };
@@ -99,9 +104,6 @@ const NODE_LOCKFILES = [
99
104
  ["bun.lock", "bun"],
100
105
  ["bun.lockb", "bun"]
101
106
  ];
102
- function messageOf(error) {
103
- return error instanceof Error ? error.message : String(error);
104
- }
105
107
  function show(value) {
106
108
  return JSON.stringify(value);
107
109
  }
@@ -151,8 +153,7 @@ function readLayer(checkouts, file) {
151
153
  }
152
154
  /** The top level of the repository holding `dir`, or undefined outside one. */
153
155
  export function findRepository(dir) {
154
- const toplevel = exec("git", ["-C", dir, "rev-parse", "--show-toplevel"]);
155
- return toplevel.status === 0 ? toplevel.stdout.trim() : undefined;
156
+ return asked(dir, ["rev-parse", "--show-toplevel"]);
156
157
  }
157
158
  /** The top level of the repository holding `dir`, or `dir` itself outside one. */
158
159
  export function repositoryRoot(dir) {
@@ -163,13 +164,8 @@ export function repositoryRoot(dir) {
163
164
  * repository: a linked worktree's git dir differs from the common one, which sits in the main checkout.
164
165
  */
165
166
  export function mainCheckout(root) {
166
- const args = ["-C", root, "rev-parse", "--path-format=absolute", "--git-dir", "--git-common-dir"];
167
- const result = exec("git", args);
168
- if (result.status !== 0) {
169
- return undefined;
170
- }
171
- const [gitDir, commonDir] = result.stdout.trim().split("\n");
172
- return gitDir === undefined || commonDir === undefined || gitDir === commonDir ? undefined : dirname(commonDir);
167
+ const dirs = gitDirs(root);
168
+ return dirs === undefined || dirs.gitDir === dirs.commonDir ? undefined : dirname(dirs.commonDir);
173
169
  }
174
170
  function inferPackageManagers(root) {
175
171
  const managers = {};
@@ -245,24 +241,24 @@ function rank(key, value) {
245
241
  return order.indexOf(value);
246
242
  }
247
243
  const LOOSENS = "the local file may tighten guardrails, never loosen them";
244
+ /** The guardrails that hold one value, each ranked under its own name in `STRICTNESS`. */
245
+ const RANKED_GUARDRAILS = ["forcePush", "commitOnProtected", "filteredOutput"];
248
246
  /**
249
247
  * Every guardrail the local layer sets less strictly than what it would override: a lower-ranked
250
- * `forcePush`, `commitOnProtected` or `envFiles.policy`, an added `envFiles.allow` entry, a dropped
251
- * `envFiles.deny` entry, a dropped protected branch.
248
+ * `forcePush`, `commitOnProtected`, `filteredOutput` or `envFiles.policy`, an added `envFiles.allow`
249
+ * entry, a dropped `envFiles.deny` entry, a dropped protected branch.
252
250
  */
253
251
  function loosenings(base, local) {
254
252
  if (local === undefined) {
255
253
  return [];
256
254
  }
257
255
  const problems = [];
258
- if (local.forcePush !== undefined && rank("forcePush", local.forcePush) > rank("forcePush", base.forcePush)) {
259
- const message = `${show(local.forcePush)} loosens ${show(base.forcePush)}; ${LOOSENS}`;
260
- problems.push({ file: LOCAL_FILE, path: "guardrails.forcePush", message });
261
- }
262
- if (local.commitOnProtected !== undefined &&
263
- rank("commitOnProtected", local.commitOnProtected) > rank("commitOnProtected", base.commitOnProtected)) {
264
- const message = `${show(local.commitOnProtected)} loosens ${show(base.commitOnProtected)}; ${LOOSENS}`;
265
- problems.push({ file: LOCAL_FILE, path: "guardrails.commitOnProtected", message });
256
+ for (const key of RANKED_GUARDRAILS) {
257
+ const value = local[key];
258
+ if (value !== undefined && rank(key, value) > rank(key, base[key])) {
259
+ const message = `${show(value)} loosens ${show(base[key])}; ${LOOSENS}`;
260
+ problems.push({ file: LOCAL_FILE, path: `guardrails.${key}`, message });
261
+ }
266
262
  }
267
263
  const policy = local.envFiles?.policy;
268
264
  if (policy !== undefined && rank("envFiles", policy) > rank("envFiles", base.envFiles.policy)) {
@@ -318,6 +314,15 @@ function finalize(merged) {
318
314
  }
319
315
  return { ...merged, commit: { ...merged.commit, pattern: COMMIT_PATTERNS[convention] } };
320
316
  }
317
+ /**
318
+ * What a report says is in force while a layer is invalid: the strictest guardrails, and no commit
319
+ * rule where no valid layer sets a convention, pattern or trailer, since the inferred layer names none.
320
+ */
321
+ export function invalidLayerEffect({ commit }) {
322
+ const strictest = "the guards run on the strictest defaults";
323
+ const unchecked = commit.convention === undefined && commit.pattern === undefined && commit.trailer === undefined;
324
+ return unchecked ? `${strictest} and the commit rules are off` : strictest;
325
+ }
321
326
  /**
322
327
  * Local over committed over inferred, for the repository at `root`. A linked worktree reads a layer it
323
328
  * lacks from its main checkout, where a repository that keeps `.ai/` out of git has the only copy.
@@ -0,0 +1,27 @@
1
+ import { parseArgs, type ParseArgsConfig } from "node:util";
2
+ /** Where the problem sits, and what the bin prints below its line. */
3
+ export type Detail = {
4
+ /** The file, and its line, of an input a Check reads. */
5
+ file?: string;
6
+ line?: number;
7
+ /** Lines the bin prints below the problem's, as a command's usage. */
8
+ after?: string;
9
+ };
10
+ export declare class CannotRun extends Error {
11
+ readonly remedy: string | undefined;
12
+ readonly detail: Detail;
13
+ constructor(message: string, remedy?: string, detail?: Detail);
14
+ /** The same problem, with `remedy` where it names none. */
15
+ orRemedy(remedy: string): CannotRun;
16
+ }
17
+ /** What an error says, whatever was thrown. */
18
+ export declare function messageOf(error: unknown): string;
19
+ /** A command line the command cannot read: the problem, then its usage after an empty line. */
20
+ export declare function usageError(problem: string, usage: string): CannotRun;
21
+ /** `parseArgs` over a command line; one it cannot read stops the command with its usage. */
22
+ export declare function readArgs<T extends ParseArgsConfig>(config: T, usage: string): ReturnType<typeof parseArgs<T>>;
23
+ /**
24
+ * The directory `--root` names, or the current one without it. A `--root` that names no directory
25
+ * stops the command, which would otherwise read the empty repository a typo leaves.
26
+ */
27
+ export declare function rootDir(root: string | undefined): string;
@@ -0,0 +1,48 @@
1
+ // What stops a command before it reaches a verdict: exit 2, one problem and, where there is one, its
2
+ // remedy. The bin alone prints it, as `dw-sdlc <command>: <problem> — <remedy>`; the Gate reports a
3
+ // Check's as one of its failures. A command line the command cannot read carries its usage below.
4
+ import { statSync } from "node:fs";
5
+ import { parseArgs } from "node:util";
6
+ export class CannotRun extends Error {
7
+ remedy;
8
+ detail;
9
+ constructor(message, remedy, detail = {}) {
10
+ super(message);
11
+ this.remedy = remedy;
12
+ this.detail = detail;
13
+ }
14
+ /** The same problem, with `remedy` where it names none. */
15
+ orRemedy(remedy) {
16
+ return this.remedy === undefined ? new CannotRun(this.message, remedy, this.detail) : this;
17
+ }
18
+ }
19
+ /** What an error says, whatever was thrown. */
20
+ export function messageOf(error) {
21
+ return error instanceof Error ? error.message : String(error);
22
+ }
23
+ /** A command line the command cannot read: the problem, then its usage after an empty line. */
24
+ export function usageError(problem, usage) {
25
+ return new CannotRun(problem, undefined, { after: `\n${usage}` });
26
+ }
27
+ /** `parseArgs` over a command line; one it cannot read stops the command with its usage. */
28
+ export function readArgs(config, usage) {
29
+ try {
30
+ return parseArgs(config);
31
+ }
32
+ catch (error) {
33
+ throw usageError(messageOf(error), usage);
34
+ }
35
+ }
36
+ /**
37
+ * The directory `--root` names, or the current one without it. A `--root` that names no directory
38
+ * stops the command, which would otherwise read the empty repository a typo leaves.
39
+ */
40
+ export function rootDir(root) {
41
+ if (root === undefined) {
42
+ return process.cwd();
43
+ }
44
+ if (statSync(root, { throwIfNoEntry: false })?.isDirectory() !== true) {
45
+ throw new CannotRun(`--root ${root} is not a directory`, "pass --root a directory inside the repository");
46
+ }
47
+ return root;
48
+ }
package/dist/catalogue.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // says and `dw-sdlc primitives --paths` answers from it.
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { join } from "node:path";
6
+ import { messageOf } from "./cannot-run.js";
6
7
  import { validate } from "./json-schema.js";
7
8
  /** Where a repository keeps its catalogue unless its config says otherwise. */
8
9
  export const CATALOGUE_FILE = ".ai/primitives.json";
@@ -41,7 +42,7 @@ export function parseCatalogue(text) {
41
42
  parsed = JSON.parse(text);
42
43
  }
43
44
  catch (error) {
44
- return { state: "invalid", problems: [`not JSON: ${error instanceof Error ? error.message : String(error)}`] };
45
+ return { state: "invalid", problems: [`not JSON: ${messageOf(error)}`] };
45
46
  }
46
47
  const problems = validate(SCHEMA, parsed).map(({ path: at, message }) => (at === "" ? message : `${at}: ${message}`));
47
48
  if (problems.length > 0) {
@@ -3,9 +3,9 @@
3
3
  // Claude Code reads it, and the skills directory, its lock and its links in step.
4
4
  import { existsSync, globSync, lstatSync, readdirSync, readFileSync, readlinkSync, realpathSync, statSync } from "node:fs";
5
5
  import { join, normalize } from "node:path";
6
- import { resolveConfig } from "../agentic-config.js";
6
+ import { CannotRun, messageOf } from "../cannot-run.js";
7
7
  import { codeSpans, tableRows } from "../markdown.js";
8
- import { CannotRun, optionsOf } from "./check.js";
8
+ import { 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";
@@ -45,9 +45,8 @@ export function routes(text) {
45
45
  }));
46
46
  }
47
47
  /** The budget, the Gate command, the row to the catalogue and every path the Task Router points at. */
48
- function judgeMap(root, { file, ...budget }, text) {
48
+ function judgeMap(root, { file, ...budget }, text, gate) {
49
49
  const findings = overBudget(text, budget).map((message) => ({ file, message, remedy: REMEDIES.budget }));
50
- const { gate } = resolveConfig(root).config;
51
50
  if (gate.command === undefined) {
52
51
  findings.push({ message: "the Gate has no command for the map to name", remedy: REMEDIES.noCommand });
53
52
  }
@@ -110,7 +109,7 @@ function lockedSkills(root) {
110
109
  lock = JSON.parse(readFileSync(join(root, LOCK), "utf8"));
111
110
  }
112
111
  catch (error) {
113
- return { problem: `not JSON: ${error instanceof Error ? error.message : String(error)}` };
112
+ return { problem: `not JSON: ${messageOf(error)}` };
114
113
  }
115
114
  if (typeof lock !== "object" || lock === null || !("skills" in lock)) {
116
115
  return { problem: "it has no skills object" };
@@ -180,13 +179,13 @@ function linksTo(link, target) {
180
179
  export function readMap(root, file) {
181
180
  const path = join(root, file);
182
181
  if (!existsSync(path)) {
183
- throw new CannotRun({ file, message: "the map does not exist", remedy: REMEDIES.missing });
182
+ throw new CannotRun("the map does not exist", REMEDIES.missing, { file });
184
183
  }
185
184
  return readFileSync(path, "utf8");
186
185
  }
187
- function run(root, options) {
186
+ function run(root, options, gate) {
188
187
  return [
189
- ...judgeMap(root, options, readMap(root, options.file)),
188
+ ...judgeMap(root, options, readMap(root, options.file), gate),
190
189
  ...judgeLink(root, options.file),
191
190
  ...judgeSkills(root)
192
191
  ];
@@ -1,11 +1,10 @@
1
+ import type { Gate } from "../agentic-config.ts";
2
+ import type { CannotRun } from "../cannot-run.ts";
1
3
  import type { Failure } from "../report.ts";
2
4
  /** A failure as a Check finds it: the file and line where it has one, what is wrong, the remedy. */
3
5
  export type Finding = Omit<Failure, "rule">;
4
- /** An input the Check needs is missing or unreadable, so the Gate cannot run: exit 2, not 1. */
5
- export declare class CannotRun extends Error {
6
- finding: Finding;
7
- constructor(finding: Finding);
8
- }
6
+ /** What a Check reports when it cannot judge: the input it lacks, in the file it names, and the remedy. */
7
+ export declare function findingOf({ message, remedy, detail }: CannotRun, fallback: string): Finding;
9
8
  /**
10
9
  * A Ratchet's committed baseline: its file, and its refresh from the tree, which writes it tighter or
11
10
  * records it where there is none, says what it did, and throws `CannotRun` when it cannot.
@@ -18,12 +17,15 @@ export type Check<Options extends object> = {
18
17
  /** The Check's key under `gate.checks`. */
19
18
  name: string;
20
19
  defaults: Options;
21
- /** Every failure of the tree at `root`, none when it passes; throws `CannotRun` when it cannot judge. */
22
- run: (root: string, options: Options) => Finding[];
20
+ /**
21
+ * Every failure of the tree at `root`, none when it passes; throws `CannotRun` when it cannot judge.
22
+ * `gate` is the resolved Gate it runs under, where it reads another Check's setting.
23
+ */
24
+ run: (root: string, options: Options, gate: Gate) => Finding[];
23
25
  /** Present on a Ratchet. */
24
26
  baseline?: Baseline<Options>;
25
27
  /** Present on a Check that derives files from their sources: writes them, says what it wrote. */
26
- write?: (root: string, options: Options) => string;
28
+ write?: (root: string, options: Options, gate: Gate) => string;
27
29
  };
28
30
  /** The options `setting` turns the Check on with: the defaults under `true`, an object over them, else off. */
29
31
  export declare function optionsOf<Options extends object>(check: Check<Options>, setting: unknown): Options | undefined;
@@ -1,12 +1,14 @@
1
1
  // What every Check the package ships is: a name, the options `true` stands for, and a run over the
2
2
  // tree that answers with its failures. The Gate names the Check as each failure's rule.
3
- /** An input the Check needs is missing or unreadable, so the Gate cannot run: exit 2, not 1. */
4
- export class CannotRun extends Error {
5
- finding;
6
- constructor(finding) {
7
- super(finding.message);
8
- this.finding = finding;
9
- }
3
+ /** What a Check reports when it cannot judge: the input it lacks, in the file it names, and the remedy. */
4
+ export function findingOf({ message, remedy, detail }, fallback) {
5
+ const { file, line } = detail;
6
+ return {
7
+ ...(file === undefined ? {} : { file }),
8
+ ...(line === undefined ? {} : { line }),
9
+ message,
10
+ remedy: remedy ?? fallback
11
+ };
10
12
  }
11
13
  /** The options `setting` turns the Check on with: the defaults under `true`, an object over them, else off. */
12
14
  export function optionsOf(check, setting) {
@@ -3,8 +3,9 @@
3
3
  // a frontmatter status from the allowed set and the frontmatter fields the config turns on.
4
4
  import { existsSync, readFileSync, statSync } from "node:fs";
5
5
  import { join, matchesGlob, posix } from "node:path";
6
+ import { CannotRun } from "../cannot-run.js";
6
7
  import { frontmatter, frontmatterList, headings } from "../markdown.js";
7
- import { CannotRun } from "./check.js";
8
+ import {} from "./check.js";
8
9
  import { directoriesOf, treeFiles } from "./tree.js";
9
10
  /** The frontmatter fields beyond `status` a record can be held to, in the order they are judged. */
10
11
  const FIELDS = ["date", "touches", "superseded-by", "amended-by"];
@@ -201,7 +202,7 @@ function judge(root, dir, { statuses, fields }, tree) {
201
202
  function run(root, options) {
202
203
  const present = options.dirs.filter((dir) => isDirectory(join(root, dir)));
203
204
  if (present.length === 0) {
204
- throw new CannotRun({ message: `none of ${options.dirs.join(", ")} exists`, remedy: REMEDIES.missing });
205
+ throw new CannotRun(`none of ${options.dirs.join(", ")} exists`, REMEDIES.missing);
205
206
  }
206
207
  let cached;
207
208
  const tree = () => {
@@ -2,8 +2,9 @@
2
2
  // within the count recorded there. A refresh only tightens the baseline: it lowers and drops, and adds no file.
3
3
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
4
4
  import { dirname, join, matchesGlob } from "node:path";
5
- import { exec } from "../exec.js";
6
- import { CannotRun } from "./check.js";
5
+ import { CannotRun, messageOf } from "../cannot-run.js";
6
+ import { asked } from "../git.js";
7
+ import {} from "./check.js";
7
8
  import { matching, treeFiles } from "./tree.js";
8
9
  const BASELINE = ".ai/file-size-ratchet.baseline.json";
9
10
  const REFRESH = "dw-sdlc gate --update-baseline";
@@ -15,11 +16,11 @@ const REMEDIES = {
15
16
  unreadable: "fix the failures it names"
16
17
  };
17
18
  /** A committed baseline gone from the tree: recording a new one would let every grown file back in. */
18
- const DELETED = {
19
- file: BASELINE,
20
- message: "the baseline is committed but gone from the tree",
21
- remedy: `restore it with git restore ${BASELINE}`
22
- };
19
+ function deleted() {
20
+ return new CannotRun("the baseline is committed but gone from the tree", `restore it with git restore ${BASELINE}`, {
21
+ file: BASELINE
22
+ });
23
+ }
23
24
  function show(value) {
24
25
  return JSON.stringify(value);
25
26
  }
@@ -58,7 +59,7 @@ function keyLines(text, path) {
58
59
  function readBaseline(root) {
59
60
  const path = join(root, BASELINE);
60
61
  if (!existsSync(path)) {
61
- const committed = exec("git", ["-C", root, "cat-file", "-e", `HEAD:${BASELINE}`]).status === 0;
62
+ const committed = asked(root, ["cat-file", "-e", `HEAD:${BASELINE}`]) !== undefined;
62
63
  return { state: "missing", committed };
63
64
  }
64
65
  const text = readFileSync(path, "utf8");
@@ -67,7 +68,7 @@ function readBaseline(root) {
67
68
  parsed = JSON.parse(text);
68
69
  }
69
70
  catch (error) {
70
- const message = `not JSON: ${error instanceof Error ? error.message : String(error)}`;
71
+ const message = `not JSON: ${messageOf(error)}`;
71
72
  return { state: "invalid", findings: [{ file: BASELINE, message, remedy: REMEDIES.shape }] };
72
73
  }
73
74
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
@@ -110,8 +111,7 @@ function contradictions(entries, groups) {
110
111
  function run(root, { groups }) {
111
112
  const read = readBaseline(root);
112
113
  if (read.state === "missing") {
113
- const missing = { file: BASELINE, message: "the baseline does not exist", remedy: REMEDIES.record };
114
- throw new CannotRun(read.committed ? DELETED : missing);
114
+ throw read.committed ? deleted() : new CannotRun("the baseline does not exist", REMEDIES.record, { file: BASELINE });
115
115
  }
116
116
  if (read.state === "invalid") {
117
117
  return read.findings;
@@ -142,14 +142,10 @@ function write(root, entries) {
142
142
  function refresh(root, { groups }) {
143
143
  const read = readBaseline(root);
144
144
  if (read.state === "invalid") {
145
- throw new CannotRun({
146
- file: BASELINE,
147
- message: "it is not a baseline the Ratchet can read",
148
- remedy: REMEDIES.unreadable
149
- });
145
+ throw new CannotRun("it is not a baseline the Ratchet can read", REMEDIES.unreadable, { file: BASELINE });
150
146
  }
151
147
  if (read.state === "missing" && read.committed) {
152
- throw new CannotRun(DELETED);
148
+ throw deleted();
153
149
  }
154
150
  const measured = measure(root, groups);
155
151
  if (read.state === "missing") {
@@ -1,5 +1,6 @@
1
+ import type { Gate } from "../agentic-config.ts";
1
2
  import { type Finding } from "./check.ts";
2
- /** A Check turned on by its setting, ready to judge a tree, and to refresh its baseline if it is a Ratchet. */
3
+ /** A Check turned on by its setting in a Gate, ready to judge a tree, and to refresh its baseline if it is a Ratchet. */
3
4
  export type EnabledCheck = {
4
5
  name: string;
5
6
  run: (root: string) => Finding[];
@@ -11,7 +12,7 @@ export type EnabledCheck = {
11
12
  };
12
13
  /** The name of every Check the package ships, in the order the Gate runs them. */
13
14
  export declare const CHECK_NAMES: readonly string[];
14
- /** Every shipped Check a resolved `gate.checks` turns on with `true` or its options. */
15
- export declare function enabledChecks(checks: Record<string, unknown>): EnabledCheck[];
16
- /** The shipped Check `name` on `setting`, or on its defaults where `setting` leaves it off. */
17
- export declare function shippedCheck(name: string, setting: unknown): EnabledCheck | undefined;
15
+ /** Every shipped Check the resolved `gate` turns on with `true` or its options under `gate.checks`. */
16
+ export declare function enabledChecks(gate: Gate): EnabledCheck[];
17
+ /** The shipped Check `name` on the setting `gate` gives it, or on its defaults where the setting leaves it off. */
18
+ export declare function shippedCheck(name: string, gate: Gate): EnabledCheck | undefined;
@@ -15,19 +15,19 @@ import { temporalLanguage } from "./temporal-language.js";
15
15
  function ship(check) {
16
16
  return {
17
17
  name: check.name,
18
- enable: (setting) => {
18
+ enable: (setting, gate) => {
19
19
  const options = optionsOf(check, setting);
20
20
  if (options === undefined) {
21
21
  return undefined;
22
22
  }
23
- const enabled = { name: check.name, run: (root) => check.run(root, options) };
23
+ const enabled = { name: check.name, run: (root) => check.run(root, options, gate) };
24
24
  const { baseline } = check;
25
25
  if (baseline !== undefined) {
26
26
  enabled.baseline = { file: baseline.file, refresh: (root) => baseline.refresh(root, options) };
27
27
  }
28
28
  const { write } = check;
29
29
  if (write !== undefined) {
30
- enabled.write = (root) => write(root, options);
30
+ enabled.write = (root) => write(root, options, gate);
31
31
  }
32
32
  return enabled;
33
33
  }
@@ -48,12 +48,13 @@ const SHIPPED = [
48
48
  ];
49
49
  /** The name of every Check the package ships, in the order the Gate runs them. */
50
50
  export const CHECK_NAMES = SHIPPED.map(({ name }) => name);
51
- /** Every shipped Check a resolved `gate.checks` turns on with `true` or its options. */
52
- export function enabledChecks(checks) {
53
- return SHIPPED.flatMap(({ name, enable }) => enable(checks[name]) ?? []);
51
+ /** Every shipped Check the resolved `gate` turns on with `true` or its options under `gate.checks`. */
52
+ export function enabledChecks(gate) {
53
+ return SHIPPED.flatMap(({ name, enable }) => enable(gate.checks[name], gate) ?? []);
54
54
  }
55
- /** The shipped Check `name` on `setting`, or on its defaults where `setting` leaves it off. */
56
- export function shippedCheck(name, setting) {
55
+ /** The shipped Check `name` on the setting `gate` gives it, or on its defaults where the setting leaves it off. */
56
+ export function shippedCheck(name, gate) {
57
+ const setting = gate.checks[name];
57
58
  const on = setting === false || setting === undefined ? true : setting;
58
- return SHIPPED.find((check) => check.name === name)?.enable(on);
59
+ return SHIPPED.find((check) => check.name === name)?.enable(on, gate);
59
60
  }