dw-sdlc 0.21.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 (94) hide show
  1. package/dist/agentic-config.d.ts +16 -0
  2. package/dist/agentic-config.js +28 -23
  3. package/dist/cannot-run.d.ts +27 -0
  4. package/dist/cannot-run.js +48 -0
  5. package/dist/catalogue.js +2 -1
  6. package/dist/checks/agents-map.js +7 -8
  7. package/dist/checks/check.d.ts +10 -8
  8. package/dist/checks/check.js +9 -7
  9. package/dist/checks/decision-records.js +3 -2
  10. package/dist/checks/file-size-ratchet.js +13 -17
  11. package/dist/checks/index.d.ts +6 -5
  12. package/dist/checks/index.js +10 -9
  13. package/dist/checks/markdown-refs.js +1 -1
  14. package/dist/checks/primitives.js +5 -5
  15. package/dist/checks/routed-docs.js +1 -3
  16. package/dist/checks/skill-sections.js +12 -20
  17. package/dist/checks/tree.js +7 -7
  18. package/dist/cli.d.ts +2 -1
  19. package/dist/cli.js +26 -5
  20. package/dist/commands/check.js +27 -42
  21. package/dist/commands/config.js +22 -27
  22. package/dist/commands/doctor.d.ts +2 -0
  23. package/dist/commands/doctor.js +11 -18
  24. package/dist/commands/gate.js +35 -40
  25. package/dist/commands/lead-start.js +28 -42
  26. package/dist/commands/pr-reviewed.js +41 -59
  27. package/dist/commands/pr-ship.js +71 -89
  28. package/dist/commands/primitives.js +38 -40
  29. package/dist/commands/recap.js +23 -74
  30. package/dist/commands/references-setup.js +28 -76
  31. package/dist/commands/references-sync.js +29 -53
  32. package/dist/commands/setup.d.ts +2 -7
  33. package/dist/commands/setup.js +38 -90
  34. package/dist/commands/sweep.js +46 -157
  35. package/dist/commands/worker-ls.js +15 -31
  36. package/dist/commands/worker-report.js +36 -52
  37. package/dist/commands/worker-start.js +43 -57
  38. package/dist/drift.js +2 -1
  39. package/dist/exec.d.ts +20 -0
  40. package/dist/exec.js +37 -4
  41. package/dist/gate-cache.js +2 -1
  42. package/dist/git.d.ts +32 -0
  43. package/dist/git.js +60 -0
  44. package/dist/json-schema.js +1 -3
  45. package/dist/landed.js +11 -22
  46. package/dist/paused-operation.d.ts +2 -0
  47. package/dist/paused-operation.js +17 -0
  48. package/dist/pins.js +2 -1
  49. package/dist/pr/decide.d.ts +4 -16
  50. package/dist/pr/decide.js +48 -84
  51. package/dist/pr/delta.d.ts +3 -4
  52. package/dist/pr/delta.js +10 -19
  53. package/dist/pr/read.d.ts +27 -30
  54. package/dist/pr/read.js +94 -182
  55. package/dist/pr/reply.js +3 -5
  56. package/dist/pr/route.d.ts +11 -3
  57. package/dist/pr/route.js +5 -5
  58. package/dist/pr/standing.d.ts +138 -0
  59. package/dist/pr/standing.js +181 -0
  60. package/dist/pr/state.d.ts +10 -69
  61. package/dist/pr/state.js +42 -72
  62. package/dist/pr/upstream.d.ts +1 -6
  63. package/dist/pr/upstream.js +2 -2
  64. package/dist/primitives-diff.js +1 -1
  65. package/dist/recap-next.js +34 -47
  66. package/dist/recap-primitives.js +1 -1
  67. package/dist/references-key.d.ts +0 -1
  68. package/dist/references-key.js +3 -4
  69. package/dist/run-command.d.ts +4 -1
  70. package/dist/run-command.js +32 -11
  71. package/dist/setup-gate.d.ts +1 -7
  72. package/dist/setup-rules.js +2 -6
  73. package/dist/setup-writer.d.ts +32 -0
  74. package/dist/setup-writer.js +60 -0
  75. package/dist/shell-line.d.ts +2 -0
  76. package/dist/shell-line.js +5 -0
  77. package/dist/sweep-removal.d.ts +31 -0
  78. package/dist/sweep-removal.js +133 -0
  79. package/dist/sweep-terminals.d.ts +1 -1
  80. package/dist/sweep-terminals.js +21 -57
  81. package/dist/sweep-trees.d.ts +19 -3
  82. package/dist/sweep-trees.js +67 -31
  83. package/dist/worker/herdr.d.ts +67 -23
  84. package/dist/worker/herdr.js +126 -77
  85. package/dist/worker/launch.js +32 -45
  86. package/dist/worker/origin.d.ts +1 -1
  87. package/dist/worker/origin.js +9 -11
  88. package/dist/worker/tabs.js +11 -7
  89. package/dist/working-tree.d.ts +0 -8
  90. package/dist/working-tree.js +6 -22
  91. package/dist/worktree-lock.js +1 -1
  92. package/package.json +1 -1
  93. package/dist/git-env.d.ts +0 -2
  94. package/dist/git-env.js +0 -10
@@ -3,10 +3,10 @@
3
3
  // docs name. `dw-sdlc check skill-sections --write` writes each part from its section.
4
4
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { join, posix } from "node:path";
6
- import { resolveConfig } from "../agentic-config.js";
6
+ import { CannotRun } from "../cannot-run.js";
7
7
  import { readCatalogue } from "../catalogue.js";
8
8
  import { headings, openFence, prose } from "../markdown.js";
9
- import { CannotRun } from "./check.js";
9
+ import {} from "./check.js";
10
10
  import { catalogueFile } from "./primitives.js";
11
11
  import { matching, treeFiles } from "./tree.js";
12
12
  /** Either marker of a part: `/` before `README.md` closes it. */
@@ -111,41 +111,33 @@ function render(root, path, sources) {
111
111
  pieces.push(text.slice(copied));
112
112
  return { text: pieces.join(""), findings, rewritten };
113
113
  }
114
- /** The docs entries the catalogue names, each read from the root; the catalogue is read on the first call. */
115
- function listedDocs(root) {
114
+ /** The docs entries of the catalogue `gate` points at, each read from the root; it is read on the first call. */
115
+ function listedDocs(root, gate) {
116
116
  let listed;
117
117
  return () => {
118
118
  if (listed === undefined) {
119
- const file = catalogueFile(resolveConfig(root).config.gate.checks);
119
+ const file = catalogueFile(gate.checks);
120
120
  const read = readCatalogue(root, file);
121
121
  if (read.state === "missing") {
122
- throw new CannotRun({
123
- file,
124
- message: "the catalogue does not exist, and a skill holds a part",
125
- remedy: REMEDIES.missing
126
- });
122
+ throw new CannotRun("the catalogue does not exist, and a skill holds a part", REMEDIES.missing, { file });
127
123
  }
128
124
  if (read.state === "invalid") {
129
- throw new CannotRun({
130
- file,
131
- message: "the catalogue is not valid, and a skill holds a part",
132
- remedy: REMEDIES.invalid
133
- });
125
+ throw new CannotRun("the catalogue is not valid, and a skill holds a part", REMEDIES.invalid, { file });
134
126
  }
135
127
  listed = new Set(read.catalogue.primitives.flatMap(({ docs }) => docs).map((entry) => posix.join(".", entry)));
136
128
  }
137
129
  return listed;
138
130
  };
139
131
  }
140
- function renderAll(root, { files }) {
141
- const sources = { listed: listedDocs(root), readmes: new Map() };
132
+ function renderAll(root, { files }, gate) {
133
+ const sources = { listed: listedDocs(root, gate), readmes: new Map() };
142
134
  return matching(root, treeFiles(root), files).map((path) => ({ path, rendered: render(root, path, sources) }));
143
135
  }
144
136
  function plural(count, noun) {
145
137
  return `${count} ${noun}${count === 1 ? "" : "s"}`;
146
138
  }
147
- function write(root, options) {
148
- const changed = renderAll(root, options).filter(({ rendered }) => rendered.rewritten > 0);
139
+ function write(root, options, gate) {
140
+ const changed = renderAll(root, options, gate).filter(({ rendered }) => rendered.rewritten > 0);
149
141
  for (const { path, rendered } of changed) {
150
142
  writeFileSync(join(root, path), rendered.text);
151
143
  }
@@ -158,6 +150,6 @@ function write(root, options) {
158
150
  export const skillSections = {
159
151
  name: "skill-sections",
160
152
  defaults: { files: ["**/skills/**/*.md"] },
161
- run: (root, options) => renderAll(root, options).flatMap(({ rendered }) => rendered.findings),
153
+ run: (root, options, gate) => renderAll(root, options, gate).flatMap(({ rendered }) => rendered.findings),
162
154
  write
163
155
  };
@@ -2,18 +2,18 @@
2
2
  // pick out of them: what the Checks that judge many files read, so a file git ignores is never judged.
3
3
  import { existsSync, lstatSync, realpathSync } from "node:fs";
4
4
  import { join, matchesGlob, posix, relative } from "node:path";
5
- import { exec } from "../exec.js";
6
- import { CannotRun } from "./check.js";
5
+ import { CannotRun } from "../cannot-run.js";
6
+ import { git } from "../git.js";
7
7
  /** Every file under `pathspecs`, the whole tree when there are none, that git tracks or would track and that is on disk; sorted. */
8
8
  export function treeFiles(root, pathspecs = []) {
9
- const args = ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...pathspecs];
10
- const listed = exec("git", args);
11
- if (listed.status !== 0) {
9
+ const args = ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...pathspecs];
10
+ const listed = git(root, args, { raw: true });
11
+ if ("error" in listed) {
12
12
  const under = pathspecs.length === 0 ? "" : ` under ${pathspecs.join(", ")}`;
13
13
  const message = `cannot list the files${under}: not a git repository`;
14
- throw new CannotRun({ message, remedy: "run the Gate inside a git repository" });
14
+ throw new CannotRun(message, "run the Gate inside a git repository");
15
15
  }
16
- return [...new Set(listed.stdout.split("\0").filter((path) => path !== ""))]
16
+ return [...new Set(listed.out.split("\0").filter((path) => path !== ""))]
17
17
  .filter((path) => existsSync(join(root, path)))
18
18
  .toSorted();
19
19
  }
package/dist/cli.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ /** `dw-sdlc <argv>`: the command's exit code, or 2 with its line when it throws `CannotRun`. */
3
+ export declare function dispatch(argv: string[]): number | Promise<number>;
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // The `dw-sdlc` bin: one row per subcommand, each a module under `commands/`. A plugin's hook or skill
3
3
  // reaches it through the plugin's `dw-sdlc` link.
4
+ import { CannotRun, messageOf } from "./cannot-run.js";
4
5
  import * as check from "./commands/check.js";
5
6
  import * as config from "./commands/config.js";
6
7
  import * as doctor from "./commands/doctor.js";
@@ -40,7 +41,13 @@ function usage() {
40
41
  const rows = [...COMMANDS].map(([name, { summary }]) => ` ${name.padEnd(width)}${summary}`);
41
42
  return `Usage: dw-sdlc <command> [options]\n\nCommands:\n${rows.join("\n")}\n\nRun dw-sdlc <command> --help for its options.`;
42
43
  }
43
- async function main(argv) {
44
+ /** What the bin prints for `error`, which stopped `command`: the problem, its remedy, the lines after it. */
45
+ function stopText(command, { message, remedy, detail }) {
46
+ const line = `dw-sdlc ${command}: ${message}${remedy === undefined ? "" : ` — ${remedy}`}\n`;
47
+ return detail.after === undefined ? line : `${line}${detail.after}\n`;
48
+ }
49
+ /** `dw-sdlc <argv>`: the command's exit code, or 2 with its line when it throws `CannotRun`. */
50
+ export function dispatch(argv) {
44
51
  const [first, second] = argv;
45
52
  if (first === "--help" || first === "-h") {
46
53
  process.stdout.write(`${usage()}\n`);
@@ -48,21 +55,35 @@ async function main(argv) {
48
55
  }
49
56
  // A two-word name, as `references sync`, before its first word alone.
50
57
  const words = second !== undefined && COMMANDS.has(`${first} ${second}`) ? 2 : 1;
51
- const command = COMMANDS.get(argv.slice(0, words).join(" "));
58
+ const name = argv.slice(0, words).join(" ");
59
+ const command = COMMANDS.get(name);
52
60
  if (command === undefined) {
53
61
  const problem = first === undefined ? "no command given" : `unknown command ${JSON.stringify(first)}`;
54
62
  process.stderr.write(`dw-sdlc: ${problem}\n\n${usage()}\n`);
55
63
  return 2;
56
64
  }
57
- return command.run(argv.slice(words));
65
+ const stopped = (error) => {
66
+ if (!(error instanceof CannotRun)) {
67
+ throw error;
68
+ }
69
+ process.stderr.write(stopText(name, error));
70
+ return 2;
71
+ };
72
+ try {
73
+ const code = command.run(argv.slice(words));
74
+ return typeof code === "number" ? code : code.catch(stopped);
75
+ }
76
+ catch (error) {
77
+ return stopped(error);
78
+ }
58
79
  }
59
80
  if (import.meta.main) {
60
81
  try {
61
- process.exitCode = await main(process.argv.slice(2));
82
+ process.exitCode = await dispatch(process.argv.slice(2));
62
83
  }
63
84
  catch (error) {
64
85
  // What no command answered for stopped it before a verdict: exit 1 would read as one.
65
- process.stderr.write(`dw-sdlc: ${error instanceof Error ? error.message : String(error)}\n`);
86
+ process.stderr.write(`dw-sdlc: ${messageOf(error)}\n`);
66
87
  process.exitCode = 2;
67
88
  }
68
89
  }
@@ -1,7 +1,6 @@
1
1
  // `dw-sdlc check <name>`: one Check of the Gate by its name, without the Steps and the other Checks.
2
- import { parseArgs } from "node:util";
3
2
  import { repositoryRoot, resolveConfig } from "../agentic-config.js";
4
- import { CannotRun } from "../checks/check.js";
3
+ import { CannotRun, readArgs, rootDir, usageError } from "../cannot-run.js";
5
4
  import { CHECK_NAMES, shippedCheck } from "../checks/index.js";
6
5
  import { TAIL_LINES } from "../run-command.js";
7
6
  import { printReport, runGate } from "./gate.js";
@@ -23,49 +22,45 @@ skill-sections writes each part of a skill from its README section.
23
22
  A Ratchet's baseline is refreshed by dw-sdlc gate --update-baseline, which needs every other Check to
24
23
  pass.
25
24
 
26
- Exit 0 when the Check passes, 1 when it fails, 2 when it cannot run: no Check of that name, an
27
- invalid config layer, a command the shell cannot find or execute, or an input the Check reads
28
- missing.`;
25
+ Exit 0 when the Check passes, 1 when it fails, 2 when it cannot run: a --root that names no
26
+ directory, no Check of that name, an invalid config layer, a command the shell cannot find or
27
+ execute, or an input the Check reads missing.`;
29
28
  function parseCli(argv) {
30
- try {
31
- const { values, positionals } = parseArgs({
32
- args: argv,
33
- allowPositionals: true,
34
- options: {
35
- help: { type: "boolean", short: "h" },
36
- json: { type: "boolean" },
37
- write: { type: "boolean" },
38
- root: { type: "string" }
39
- }
40
- });
41
- if (positionals.length > 1) {
42
- return { error: `one Check at a time, not ${positionals.map((name) => JSON.stringify(name)).join(" and ")}` };
29
+ const { values, positionals } = readArgs({
30
+ args: argv,
31
+ allowPositionals: true,
32
+ options: {
33
+ help: { type: "boolean", short: "h" },
34
+ json: { type: "boolean" },
35
+ write: { type: "boolean" },
36
+ root: { type: "string" }
43
37
  }
44
- const flags = { help: values.help === true, json: values.json === true, write: values.write === true };
45
- return { ...flags, root: values.root, name: positionals[0] };
46
- }
47
- catch (error) {
48
- return { error: error instanceof Error ? error.message : String(error) };
38
+ }, USAGE);
39
+ if (positionals.length > 1) {
40
+ const names = positionals.map((name) => JSON.stringify(name)).join(" and ");
41
+ throw usageError(`one Check at a time, not ${names}`, USAGE);
49
42
  }
43
+ const flags = { help: values.help === true, json: values.json === true, write: values.write === true };
44
+ return { ...flags, root: values.root, name: positionals[0] };
50
45
  }
51
46
  /**
52
47
  * Writes what the shipped Check `name` derives, on the setting the config gives it, and prints what it
53
- * wrote; why it cannot when it takes no --write. An invalid config or a missing input writes nothing,
54
- * and the run after it reports why.
48
+ * wrote; stops the command when it takes no --write. An invalid config or a missing input writes
49
+ * nothing, and the run after it reports why.
55
50
  */
56
51
  function writeDerived(root, name, json) {
57
52
  const { config, problems } = resolveConfig(root);
58
53
  if (problems.length > 0) {
59
- return undefined;
54
+ return;
60
55
  }
61
- const shipped = shippedCheck(name, config.gate.checks[name]);
56
+ const shipped = shippedCheck(name, config.gate);
62
57
  if (shipped === undefined && !config.gate.checks.run.some((check) => check.name === name)) {
63
58
  // No Check of that name: the run after it says so, with the names there are.
64
- return undefined;
59
+ return;
65
60
  }
66
61
  const write = shipped?.write;
67
62
  if (write === undefined) {
68
- return `${JSON.stringify(name)} takes no --write: it derives no file`;
63
+ throw new CannotRun(`${JSON.stringify(name)} takes no --write: it derives no file`);
69
64
  }
70
65
  try {
71
66
  const out = json ? process.stderr : process.stdout;
@@ -76,30 +71,20 @@ function writeDerived(root, name, json) {
76
71
  throw error;
77
72
  }
78
73
  }
79
- return undefined;
80
74
  }
81
75
  export async function run(argv) {
82
76
  const cli = parseCli(argv);
83
- if ("error" in cli) {
84
- process.stderr.write(`dw-sdlc check: ${cli.error}\n\n${USAGE}\n`);
85
- return 2;
86
- }
87
77
  if (cli.help) {
88
78
  process.stdout.write(`${USAGE}\n`);
89
79
  return 0;
90
80
  }
91
81
  if (cli.name === undefined) {
92
- process.stderr.write(`dw-sdlc check: name the Check to run\n\n${USAGE}\n`);
93
- return 2;
82
+ throw usageError("name the Check to run", USAGE);
94
83
  }
95
84
  const { json, name } = cli;
96
- const root = repositoryRoot(cli.root ?? process.cwd());
85
+ const root = repositoryRoot(rootDir(cli.root));
97
86
  if (cli.write) {
98
- const refused = writeDerived(root, name, json);
99
- if (refused !== undefined) {
100
- process.stderr.write(`dw-sdlc check: ${refused}\n`);
101
- return 2;
102
- }
87
+ writeDerived(root, name, json);
103
88
  }
104
89
  const report = await runGate(root, { json, only: name });
105
90
  return printReport(report, json, `check ${name}`);
@@ -1,37 +1,31 @@
1
1
  // `dw-sdlc config`, the config check: resolves a repository's agentic config, or validates one file
2
2
  // against the schema, and prints what the guards see.
3
3
  import { readFileSync } from "node:fs";
4
- import { parseArgs } from "node:util";
5
- import { COMMITTED_FILE, findRepository, LOCAL_FILE, repositoryRoot, resolveConfig, validateDocument } from "../agentic-config.js";
4
+ import { COMMITTED_FILE, findRepository, invalidLayerEffect, LOCAL_FILE, repositoryRoot, resolveConfig, validateDocument } from "../agentic-config.js";
5
+ import { messageOf, readArgs, rootDir, usageError } from "../cannot-run.js";
6
6
  export const summary = "resolve the repository's agentic config, or validate one file against the schema";
7
7
  const USAGE = `Usage: dw-sdlc config [--root <dir>] [file]
8
8
  or: pnpm agentic:check [--root <dir>] [file], in a checkout of dw-sdlc
9
9
 
10
10
  Without a file: resolves the agentic config of the repository holding --root (default: the current
11
11
  directory) — local over committed over inferred — and prints what the guards see. With a file:
12
- validates that one document against the schema. Exit 1 on any problem.`;
13
- function messageOf(error) {
14
- return error instanceof Error ? error.message : String(error);
15
- }
16
- function fail(message, code) {
17
- console.error(`agentic:check: ${message}`);
18
- process.exit(code);
12
+ validates that one document against the schema. Exit 1 on any problem, 2 when --root names no
13
+ directory.`;
14
+ /** A problem the check reports beside the config's own: exit 1, as for an invalid layer. */
15
+ function failed(message) {
16
+ console.error(`dw-sdlc config: ${message}`);
17
+ return 1;
19
18
  }
20
19
  function parseCli(argv) {
21
- try {
22
- const { values, positionals } = parseArgs({
23
- args: argv,
24
- allowPositionals: true,
25
- options: { help: { type: "boolean", short: "h" }, root: { type: "string" } }
26
- });
27
- if (positionals.length > 1) {
28
- throw new Error("at most one file");
29
- }
30
- return { help: values.help === true, root: values.root, file: positionals[0] };
31
- }
32
- catch (error) {
33
- fail(`${messageOf(error)}\n\n${USAGE}`, 2);
20
+ const { values, positionals } = readArgs({
21
+ args: argv,
22
+ allowPositionals: true,
23
+ options: { help: { type: "boolean", short: "h" }, root: { type: "string" } }
24
+ }, USAGE);
25
+ if (positionals.length > 1) {
26
+ throw usageError("at most one file", USAGE);
34
27
  }
28
+ return { help: values.help === true, root: values.root, file: positionals[0] };
35
29
  }
36
30
  function report(problems) {
37
31
  for (const { file, path, message } of problems) {
@@ -50,7 +44,7 @@ export function run(argv) {
50
44
  text = readFileSync(cli.file, "utf8");
51
45
  }
52
46
  catch (error) {
53
- fail(messageOf(error), 1);
47
+ return failed(messageOf(error));
54
48
  }
55
49
  const result = validateDocument(cli.file, text);
56
50
  if ("problems" in result) {
@@ -60,9 +54,10 @@ export function run(argv) {
60
54
  console.log(`${cli.file}: valid`);
61
55
  return 0;
62
56
  }
63
- const root = cli.root === undefined
64
- ? (findRepository(process.cwd()) ?? fail("not inside a git repository; pass --root <dir>", 1))
65
- : repositoryRoot(cli.root);
57
+ const root = cli.root === undefined ? findRepository(process.cwd()) : repositoryRoot(rootDir(cli.root));
58
+ if (root === undefined) {
59
+ return failed("not inside a git repository; pass --root <dir>");
60
+ }
66
61
  const { config, problems, layers, sources } = resolveConfig(root);
67
62
  const from = (source) => source === undefined || source === root ? "" : `, from ${source}`;
68
63
  console.log(`committed ${COMMITTED_FILE} ${layers.committed}${from(sources.committed)}`);
@@ -70,7 +65,7 @@ export function run(argv) {
70
65
  console.log(JSON.stringify(config, null, 2));
71
66
  if (problems.length > 0) {
72
67
  report(problems);
73
- fail(`${problems.length} problem(s) above; the guards run on the strictest defaults until the file is fixed`, 1);
68
+ return failed(`${problems.length} problem(s) above; ${invalidLayerEffect(config)} until the file is fixed`);
74
69
  }
75
70
  return 0;
76
71
  }
@@ -20,6 +20,8 @@ export type RepositoryConfig = {
20
20
  config: {
21
21
  commit: {
22
22
  convention?: string;
23
+ pattern?: string;
24
+ trailer?: string;
23
25
  };
24
26
  };
25
27
  problems: string[];
@@ -4,10 +4,10 @@
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { homedir } from "node:os";
6
6
  import { join } from "node:path";
7
- import { parseArgs } from "node:util";
8
- import { findRepository, resolveConfig } from "../agentic-config.js";
7
+ import { findRepository, invalidLayerEffect, resolveConfig } from "../agentic-config.js";
8
+ import { readArgs, rootDir } from "../cannot-run.js";
9
9
  import { checkPackage, checkPlugin, checkSessions, parseJson, runningPluginRoot, sessionsEnabled, versionBefore } from "../doctor-plugins.js";
10
- import { exec, TimedOut } from "../exec.js";
10
+ import { exec, firstLine, TimedOut } from "../exec.js";
11
11
  import { isObject } from "../json-text.js";
12
12
  /** Claude Code's config dir: `$CLAUDE_CONFIG_DIR` when set, the way Claude Code reads it, else `~/.claude`. */
13
13
  export function claudeConfigDir(env, home) {
@@ -39,7 +39,7 @@ export const realProbe = {
39
39
  shell(script) {
40
40
  const shell = process.env.SHELL ?? "/bin/sh";
41
41
  const result = probed(shell, ["-l", "-c", script]);
42
- return (result?.stdout ?? "").trim().split("\n")[0] ?? "";
42
+ return firstLine(result?.stdout ?? "") ?? "";
43
43
  }
44
44
  };
45
45
  function checkJq(probe) {
@@ -213,7 +213,7 @@ function checkConfig(config, root) {
213
213
  return {
214
214
  name: "config",
215
215
  level: "FAIL",
216
- detail: `the guards run on the strictest defaults: ${config.problems.join("; ")}`
216
+ detail: `${invalidLayerEffect(config.config)}: ${config.problems.join("; ")}`
217
217
  };
218
218
  }
219
219
  // A worktree that lacks a layer reads the main checkout's; say so, since the file is not here.
@@ -250,7 +250,7 @@ const USAGE = `Usage: dw-sdlc doctor [--root <dir>] [--home <dir>]
250
250
  or: pnpm guardrails:doctor [--root <dir>] [--home <dir>], in a checkout of dw-sdlc
251
251
 
252
252
  One PASS / WARN / FAIL line per machine check the sdlc plugin relies on; read-only. Exit 1 when any
253
- check fails. Claude Code's settings are read from $CLAUDE_CONFIG_DIR, or ~/.claude without it; a
253
+ check fails, 2 when --root names no directory. Claude Code's settings are read from $CLAUDE_CONFIG_DIR, or ~/.claude without it; a
254
254
  --home reads <home>/.claude.`;
255
255
  function repositoryConfig(root) {
256
256
  const resolution = resolveConfig(root);
@@ -261,22 +261,15 @@ function repositoryConfig(root) {
261
261
  };
262
262
  }
263
263
  export function run(argv) {
264
- let values;
265
- try {
266
- values = parseArgs({
267
- args: argv,
268
- options: { help: { type: "boolean", short: "h" }, root: { type: "string" }, home: { type: "string" } }
269
- }).values;
270
- }
271
- catch (error) {
272
- console.error(`guardrails:doctor: ${error instanceof Error ? error.message : String(error)}\n\n${USAGE}`);
273
- return 2;
274
- }
264
+ const { values } = readArgs({
265
+ args: argv,
266
+ options: { help: { type: "boolean", short: "h" }, root: { type: "string" }, home: { type: "string" } }
267
+ }, USAGE);
275
268
  if (values.help === true) {
276
269
  console.log(USAGE);
277
270
  return 0;
278
271
  }
279
- const dir = values.root ?? process.cwd();
272
+ const dir = rootDir(values.root);
280
273
  const root = findRepository(dir);
281
274
  const checks = diagnose({
282
275
  root: root ?? dir,
@@ -1,8 +1,8 @@
1
1
  // `dw-sdlc gate`: the Gate of a repository, from the `gate` key of its resolved agentic config. The
2
2
  // run and the report serve `dw-sdlc check` too, which judges one Check of it.
3
- import { parseArgs } from "node:util";
4
3
  import { repositoryRoot, resolveConfig, stepCommand, stepHint } from "../agentic-config.js";
5
- import { CannotRun } from "../checks/check.js";
4
+ import { CannotRun, messageOf, readArgs, rootDir, usageError } from "../cannot-run.js";
5
+ import { findingOf } from "../checks/check.js";
6
6
  import { CHECK_NAMES, enabledChecks, shippedCheck } from "../checks/index.js";
7
7
  import { watchDrift } from "../drift.js";
8
8
  import { fingerprint, isGreen, stampGreen } from "../gate-cache.js";
@@ -29,10 +29,12 @@ changed or deleted is Drift, one failure per file, and the Steps after it still
29
29
  and edits made before the run never drift, and a Step a signal stopped is not compared. gate.drift
30
30
  "allow" turns the watch off.
31
31
 
32
- SIGINT or SIGTERM sent while a command runs stops it and everything after it, and the report still
33
- prints: the stopped command's last lines, its failure line and the last line. A command that SIGINT
34
- or SIGTERM ends stops everything after it the same way, as Ctrl-C reaches it and the Gate together. A
35
- second signal, or one sent while the Checks the package ships run, ends the Gate at once.
32
+ Each command runs in a process group of its own, which every process it starts joins. SIGINT or
33
+ SIGTERM sent while a command runs reaches that whole group and stops everything after it, and the
34
+ report still prints: the stopped command's last lines, its failure line and the last line. A command
35
+ that SIGINT or SIGTERM ends stops everything after it the same way. A second signal ends the Gate at
36
+ once and kills the command's group; one sent while the Checks the package ships run ends the Gate at
37
+ once. SIGHUP reaches the command's group and ends the Gate.
36
38
 
37
39
  --verbose passes each command's output through as it runs instead, under a line naming the command,
38
40
  and keeps no tail. --json prints the document alone; each Step and Check in it carries tail, the last
@@ -48,8 +50,9 @@ Untracked files count and ignored files do not. Each worktree keeps its own stam
48
50
  hash runs the whole Gate.
49
51
 
50
52
  Exit 0 when everything passes, 1 when a Step or a Check fails or a Step drifts, 2 when the Gate
51
- cannot run: an invalid config layer, a Step or Check whose command the shell cannot find or execute,
52
- a Check missing an input it reads, or a tree it cannot record for Drift.`;
53
+ cannot run: a --root that names no directory, an invalid config layer, a Step or Check whose command
54
+ the shell cannot find or execute, a Check missing an input it reads, or a tree it cannot record for
55
+ Drift.`;
53
56
  const EXIT_CODES = { pass: 0, fail: 1, error: 2 };
54
57
  const SKIPPED = { status: "skipped", exitCode: null, tail: null };
55
58
  const SILENT = { write: () => true };
@@ -72,30 +75,25 @@ function plural(count, noun) {
72
75
  return `${count} ${noun}${count === 1 ? "" : "s"}`;
73
76
  }
74
77
  function parseCli(argv) {
75
- try {
76
- const { values } = parseArgs({
77
- args: argv,
78
- options: {
79
- help: { type: "boolean", short: "h" },
80
- json: { type: "boolean" },
81
- verbose: { type: "boolean" },
82
- root: { type: "string" },
83
- "update-baseline": { type: "boolean" },
84
- cache: { type: "boolean" }
85
- }
86
- });
87
- const refresh = values["update-baseline"] === true;
88
- const cache = values.cache === true;
89
- if (refresh && cache) {
90
- return { error: "--cache and --update-baseline do not combine: a refresh needs the Gate to run" };
78
+ const { values } = readArgs({
79
+ args: argv,
80
+ options: {
81
+ help: { type: "boolean", short: "h" },
82
+ json: { type: "boolean" },
83
+ verbose: { type: "boolean" },
84
+ root: { type: "string" },
85
+ "update-baseline": { type: "boolean" },
86
+ cache: { type: "boolean" }
91
87
  }
92
- const mode = refresh ? "update-baseline" : cache ? "cache" : "plain";
93
- const flags = { help: values.help === true, json: values.json === true, verbose: values.verbose === true };
94
- return { ...flags, root: values.root, mode };
95
- }
96
- catch (error) {
97
- return { error: error instanceof Error ? error.message : String(error) };
88
+ }, USAGE);
89
+ const refresh = values["update-baseline"] === true;
90
+ const cache = values.cache === true;
91
+ if (refresh && cache) {
92
+ throw usageError("--cache and --update-baseline do not combine: a refresh needs the Gate to run", USAGE);
98
93
  }
94
+ const mode = refresh ? "update-baseline" : cache ? "cache" : "plain";
95
+ const flags = { help: values.help === true, json: values.json === true, verbose: values.verbose === true };
96
+ return { ...flags, root: values.root, mode };
99
97
  }
100
98
  function configFailure({ file, path, message }) {
101
99
  const where = path === "" ? message : `${path}: ${message}`;
@@ -164,8 +162,8 @@ function progressOf(options, log) {
164
162
  /** What a Check that threw reports: the input it lacks, or what stopped it. */
165
163
  function erred(check, error) {
166
164
  const finding = error instanceof CannotRun
167
- ? error.finding
168
- : { message: `stopped: ${error instanceof Error ? error.message : String(error)}`, remedy: REMEDIES.checkStopped };
165
+ ? findingOf(error, REMEDIES.checkStopped)
166
+ : { message: `stopped: ${messageOf(error)}`, remedy: REMEDIES.checkStopped };
169
167
  return {
170
168
  check,
171
169
  outcome: { status: "error", exitCode: EXIT_CODES.error, tail: null },
@@ -209,7 +207,8 @@ function refreshBaselines(judged, others, root) {
209
207
  }
210
208
  catch (error) {
211
209
  if (error instanceof CannotRun) {
212
- notes.push(`gate: baseline ${file} not refreshed — ${error.finding.message}; ${error.finding.remedy}`);
210
+ const { message, remedy } = findingOf(error, REMEDIES.checkStopped);
211
+ notes.push(`gate: baseline ${file} not refreshed — ${message}; ${remedy}`);
213
212
  }
214
213
  else {
215
214
  judged[index] = erred(check, error);
@@ -229,10 +228,10 @@ function worst(outcomes) {
229
228
  */
230
229
  function select(gate, only) {
231
230
  if (only === undefined) {
232
- return { steps: gate.steps, run: gate.checks.run, shipped: enabledChecks(gate.checks) };
231
+ return { steps: gate.steps, run: gate.checks.run, shipped: enabledChecks(gate) };
233
232
  }
234
233
  const scripts = gate.checks.run.filter((check) => check.name === only);
235
- const shipped = shippedCheck(only, gate.checks[only]);
234
+ const shipped = shippedCheck(only, gate);
236
235
  if (scripts.length === 0 && shipped === undefined) {
237
236
  const names = [...gate.checks.run.map((check) => check.name), ...CHECK_NAMES].join(", ");
238
237
  return { rule: "check", message: `no Check is named ${JSON.stringify(only)}`, remedy: `name one of ${names}` };
@@ -380,15 +379,11 @@ export function printReport(report, json, label) {
380
379
  }
381
380
  export async function run(argv) {
382
381
  const cli = parseCli(argv);
383
- if ("error" in cli) {
384
- process.stderr.write(`dw-sdlc gate: ${cli.error}\n\n${USAGE}\n`);
385
- return 2;
386
- }
387
382
  if (cli.help) {
388
383
  process.stdout.write(`${USAGE}\n`);
389
384
  return 0;
390
385
  }
391
386
  const { json, verbose, mode } = cli;
392
- const report = await runGate(repositoryRoot(cli.root ?? process.cwd()), { json, verbose, mode });
387
+ const report = await runGate(repositoryRoot(rootDir(cli.root)), { json, verbose, mode });
393
388
  return printReport(report, json, "gate");
394
389
  }