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.
- package/dist/agentic-config.d.ts +16 -0
- package/dist/agentic-config.js +28 -23
- package/dist/cannot-run.d.ts +27 -0
- package/dist/cannot-run.js +48 -0
- package/dist/catalogue.js +2 -1
- package/dist/checks/agents-map.js +7 -8
- package/dist/checks/check.d.ts +10 -8
- package/dist/checks/check.js +9 -7
- package/dist/checks/decision-records.js +3 -2
- package/dist/checks/file-size-ratchet.js +13 -17
- package/dist/checks/index.d.ts +6 -5
- package/dist/checks/index.js +10 -9
- package/dist/checks/markdown-refs.js +1 -1
- package/dist/checks/primitives.js +5 -5
- package/dist/checks/routed-docs.js +1 -3
- package/dist/checks/skill-sections.js +12 -20
- package/dist/checks/tree.js +7 -7
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +26 -5
- package/dist/commands/check.js +27 -42
- package/dist/commands/config.js +22 -27
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +11 -18
- package/dist/commands/gate.js +35 -40
- package/dist/commands/lead-start.js +28 -42
- package/dist/commands/pr-reviewed.js +41 -59
- package/dist/commands/pr-ship.js +71 -89
- package/dist/commands/primitives.js +38 -40
- package/dist/commands/recap.js +23 -74
- package/dist/commands/references-setup.js +28 -76
- package/dist/commands/references-sync.js +29 -53
- package/dist/commands/setup.d.ts +2 -7
- package/dist/commands/setup.js +38 -90
- package/dist/commands/sweep.js +46 -157
- package/dist/commands/worker-ls.js +15 -31
- package/dist/commands/worker-report.js +36 -52
- package/dist/commands/worker-start.js +43 -57
- package/dist/drift.js +2 -1
- package/dist/exec.d.ts +20 -0
- package/dist/exec.js +37 -4
- package/dist/gate-cache.js +2 -1
- package/dist/git.d.ts +32 -0
- package/dist/git.js +60 -0
- package/dist/json-schema.js +1 -3
- package/dist/landed.js +11 -22
- package/dist/paused-operation.d.ts +2 -0
- package/dist/paused-operation.js +17 -0
- package/dist/pins.js +2 -1
- package/dist/pr/decide.d.ts +4 -16
- package/dist/pr/decide.js +48 -84
- package/dist/pr/delta.d.ts +3 -4
- package/dist/pr/delta.js +10 -19
- package/dist/pr/read.d.ts +27 -30
- package/dist/pr/read.js +94 -182
- package/dist/pr/reply.js +3 -5
- package/dist/pr/route.d.ts +11 -3
- package/dist/pr/route.js +5 -5
- package/dist/pr/standing.d.ts +138 -0
- package/dist/pr/standing.js +181 -0
- package/dist/pr/state.d.ts +10 -69
- package/dist/pr/state.js +42 -72
- package/dist/pr/upstream.d.ts +1 -6
- package/dist/pr/upstream.js +2 -2
- package/dist/primitives-diff.js +1 -1
- package/dist/recap-next.js +34 -47
- package/dist/recap-primitives.js +1 -1
- package/dist/references-key.d.ts +0 -1
- package/dist/references-key.js +3 -4
- package/dist/run-command.d.ts +4 -1
- package/dist/run-command.js +32 -11
- package/dist/setup-gate.d.ts +1 -7
- package/dist/setup-rules.js +2 -6
- package/dist/setup-writer.d.ts +32 -0
- package/dist/setup-writer.js +60 -0
- package/dist/shell-line.d.ts +2 -0
- package/dist/shell-line.js +5 -0
- package/dist/sweep-removal.d.ts +31 -0
- package/dist/sweep-removal.js +133 -0
- package/dist/sweep-terminals.d.ts +1 -1
- package/dist/sweep-terminals.js +21 -57
- package/dist/sweep-trees.d.ts +19 -3
- package/dist/sweep-trees.js +67 -31
- package/dist/worker/herdr.d.ts +67 -23
- package/dist/worker/herdr.js +126 -77
- package/dist/worker/launch.js +32 -45
- package/dist/worker/origin.d.ts +1 -1
- package/dist/worker/origin.js +9 -11
- package/dist/worker/tabs.js +11 -7
- package/dist/working-tree.d.ts +0 -8
- package/dist/working-tree.js +6 -22
- package/dist/worktree-lock.js +1 -1
- package/package.json +1 -1
- package/dist/git-env.d.ts +0 -2
- 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 {
|
|
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 {
|
|
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
|
|
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(
|
|
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
|
};
|
package/dist/checks/tree.js
CHANGED
|
@@ -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 {
|
|
6
|
-
import {
|
|
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 = ["
|
|
10
|
-
const listed =
|
|
11
|
-
if (
|
|
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(
|
|
14
|
+
throw new CannotRun(message, "run the Gate inside a git repository");
|
|
15
15
|
}
|
|
16
|
-
return [...new Set(listed.
|
|
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
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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: ${
|
|
86
|
+
process.stderr.write(`dw-sdlc: ${messageOf(error)}\n`);
|
|
66
87
|
process.exitCode = 2;
|
|
67
88
|
}
|
|
68
89
|
}
|
package/dist/commands/check.js
CHANGED
|
@@ -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 "../
|
|
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:
|
|
27
|
-
invalid config layer, a command the shell cannot find or
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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;
|
|
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
|
|
54
|
+
return;
|
|
60
55
|
}
|
|
61
|
-
const shipped = shippedCheck(name, config.gate
|
|
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
|
|
59
|
+
return;
|
|
65
60
|
}
|
|
66
61
|
const write = shipped?.write;
|
|
67
62
|
if (write === undefined) {
|
|
68
|
-
|
|
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
|
-
|
|
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
|
|
85
|
+
const root = repositoryRoot(rootDir(cli.root));
|
|
97
86
|
if (cli.write) {
|
|
98
|
-
|
|
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}`);
|
package/dist/commands/config.js
CHANGED
|
@@ -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 {
|
|
5
|
-
import {
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
+
return failed(`${problems.length} problem(s) above; ${invalidLayerEffect(config)} until the file is fixed`);
|
|
74
69
|
}
|
|
75
70
|
return 0;
|
|
76
71
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -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 {
|
|
8
|
-
import {
|
|
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 ?? "")
|
|
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:
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
272
|
+
const dir = rootDir(values.root);
|
|
280
273
|
const root = findRepository(dir);
|
|
281
274
|
const checks = diagnose({
|
|
282
275
|
root: root ?? dir,
|
package/dist/commands/gate.js
CHANGED
|
@@ -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 "../
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
52
|
-
a Check missing an input it reads, or a tree it cannot record for
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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.
|
|
168
|
-
: { message: `stopped: ${
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
387
|
+
const report = await runGate(repositoryRoot(rootDir(cli.root)), { json, verbose, mode });
|
|
393
388
|
return printReport(report, json, "gate");
|
|
394
389
|
}
|