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.
- package/README.md +13 -13
- package/config/agentic.config.schema.json +1 -1
- 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 +58 -6
- 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 +72 -87
- 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 -83
- package/dist/pr/delta.d.ts +3 -4
- package/dist/pr/delta.js +11 -20
- package/dist/pr/read.d.ts +27 -30
- package/dist/pr/read.js +96 -158
- package/dist/pr/reply.d.ts +34 -0
- package/dist/pr/reply.js +112 -0
- 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 +14 -69
- package/dist/pr/state.js +75 -148
- 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
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
// `markdown-refs`: every relative link and image
|
|
2
|
-
// directory git tracks or would track and its
|
|
3
|
-
// link to an ignored file or to another case of a file's name fails here as it fails in CI.
|
|
1
|
+
// `markdown-refs`: every relative link and image, and every URL into origin's default branch even in
|
|
2
|
+
// code, resolves to a file or directory git tracks or would track and its anchor to a heading there, so
|
|
3
|
+
// a link to an ignored file or to another case of a file's name fails here as it fails in CI.
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
5
|
import { join, posix } from "node:path";
|
|
6
|
+
import { asked } from "../git.js";
|
|
6
7
|
import { anchorsOf, links } from "../markdown.js";
|
|
7
8
|
import { directoriesOf, matching, treeFiles } from "./tree.js";
|
|
8
9
|
const REMEDIES = {
|
|
@@ -12,6 +13,51 @@ const REMEDIES = {
|
|
|
12
13
|
};
|
|
13
14
|
/** A target with a scheme, such as `https:` or `mailto:`, or a host: no file of the tree answers it. */
|
|
14
15
|
const EXTERNAL = /^([a-z][a-z\d+.-]*:|\/\/)/i;
|
|
16
|
+
/** `owner/name` in the URL of a GitHub remote, over SSH or HTTPS, with a user, a token or a port, `.git` or not. */
|
|
17
|
+
const GITHUB_REMOTE = /^(?:git@github\.com:|(?:ssh|https?):\/\/(?:[^@/]+@)?github\.com(?::\d+)?\/)([^/]+\/[^/]+?)(?:\.git)?\/?$/i;
|
|
18
|
+
const REMOTE_HEAD = "refs/remotes/origin/HEAD";
|
|
19
|
+
/** The branch a URL is judged at when origin/HEAD names none, as in a CI checkout. */
|
|
20
|
+
const FALLBACK_BRANCH = "main";
|
|
21
|
+
/** A URL into a GitHub repository: a file page, a directory page or a raw file, up to a space, a quote or a bracket. */
|
|
22
|
+
const REPOSITORY_URL = /https:\/\/(?:github\.com\/[^/\s]+\/[^/\s]+\/(?:blob|tree)|raw\.githubusercontent\.com\/[^/\s]+\/[^/\s]+)\/[^\s"'`<>()[\]]+/gi;
|
|
23
|
+
/** Closing punctuation and emphasis markers after a URL, which GitHub leaves out of it. */
|
|
24
|
+
const TRAILING = /[.,;:!?*_~]+$/;
|
|
25
|
+
/** A GitHub anchor to a line or a range of lines, which names no heading. */
|
|
26
|
+
const LINE_ANCHOR = /#L\d+(C\d+)?(-L\d+(C\d+)?)?$/;
|
|
27
|
+
/** The prefixes of URLs into origin's default branch; none when origin is not on GitHub. */
|
|
28
|
+
function prefixesOf(root) {
|
|
29
|
+
const slug = GITHUB_REMOTE.exec(asked(root, ["remote", "get-url", "origin"]) ?? "")?.[1]?.toLowerCase();
|
|
30
|
+
if (slug === undefined) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const head = asked(root, ["symbolic-ref", "-q", REMOTE_HEAD])?.replace(/^refs\/remotes\/origin\//, "");
|
|
34
|
+
const branch = head === undefined || head === "" ? FALLBACK_BRANCH : head;
|
|
35
|
+
return [
|
|
36
|
+
`https://github.com/${slug}/blob/`,
|
|
37
|
+
`https://github.com/${slug}/tree/`,
|
|
38
|
+
`https://raw.githubusercontent.com/${slug}/`
|
|
39
|
+
].map((repository) => ({ repository, branch }));
|
|
40
|
+
}
|
|
41
|
+
/** The path from the root `url` names under one of `prefixes`, its line anchor dropped; undefined under none. */
|
|
42
|
+
function pathOf(url, prefixes) {
|
|
43
|
+
const prefix = prefixes.find(({ repository, branch }) => url.toLowerCase().startsWith(repository) && url.slice(repository.length).startsWith(`${branch}/`));
|
|
44
|
+
return prefix === undefined
|
|
45
|
+
? undefined
|
|
46
|
+
: `/${url.slice(prefix.repository.length + prefix.branch.length + 1).replace(LINE_ANCHOR, "")}`;
|
|
47
|
+
}
|
|
48
|
+
/** Every URL of `text` into the repository's default branch, a fenced line's included, once per line. */
|
|
49
|
+
function repositoryLinks(text, prefixes) {
|
|
50
|
+
if (prefixes.length === 0) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
return text.split("\n").flatMap((line, index) => {
|
|
54
|
+
const urls = new Set(Array.from(line.matchAll(REPOSITORY_URL), ([url]) => url.replace(TRAILING, "")));
|
|
55
|
+
return [...urls].flatMap((url) => {
|
|
56
|
+
const target = pathOf(url, prefixes);
|
|
57
|
+
return target === undefined ? [] : [{ target, line: index + 1, shown: url }];
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
15
61
|
function decode(text) {
|
|
16
62
|
try {
|
|
17
63
|
return decodeURIComponent(text);
|
|
@@ -30,13 +76,13 @@ function resolve(file, path) {
|
|
|
30
76
|
return normal.length > 1 ? normal.replace(/\/$/, "") : normal;
|
|
31
77
|
}
|
|
32
78
|
/** What is wrong with one link of `file`: a path out of the repository or not in it, or an anchor with no heading. */
|
|
33
|
-
function judge(repository, file, { target, line }) {
|
|
79
|
+
function judge(repository, file, { target, line, shown = target }) {
|
|
34
80
|
if (EXTERNAL.test(target)) {
|
|
35
81
|
return [];
|
|
36
82
|
}
|
|
37
83
|
const [beforeAnchor = "", anchor = ""] = target.split("#", 2);
|
|
38
84
|
const resolved = resolve(file, decode(beforeAnchor.split("?", 1)[0] ?? ""));
|
|
39
|
-
const quoted = JSON.stringify(
|
|
85
|
+
const quoted = JSON.stringify(shown);
|
|
40
86
|
if (resolved === ".." || resolved.startsWith("../")) {
|
|
41
87
|
return [{ file, line, message: `links to ${quoted}, which is outside the repository`, remedy: REMEDIES.outside }];
|
|
42
88
|
}
|
|
@@ -53,8 +99,14 @@ function judge(repository, file, { target, line }) {
|
|
|
53
99
|
function run(root, { files, exempt }) {
|
|
54
100
|
const tree = treeFiles(root);
|
|
55
101
|
const known = new Set(tree);
|
|
102
|
+
const prefixes = prefixesOf(root);
|
|
56
103
|
const repository = { root, files: known, directories: directoriesOf(known), anchors: new Map() };
|
|
57
|
-
return matching(root, tree, files, exempt).flatMap((file) =>
|
|
104
|
+
return matching(root, tree, files, exempt).flatMap((file) => {
|
|
105
|
+
const text = readFileSync(join(root, file), "utf8");
|
|
106
|
+
return [...links(text), ...repositoryLinks(text, prefixes)]
|
|
107
|
+
.toSorted((left, right) => left.line - right.line)
|
|
108
|
+
.flatMap((link) => judge(repository, file, link));
|
|
109
|
+
});
|
|
58
110
|
}
|
|
59
111
|
export const markdownRefs = {
|
|
60
112
|
name: "markdown-refs",
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// one-line summary, real code and docs paths, no decision record out of force, every file named.
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { CannotRun } from "../cannot-run.js";
|
|
6
6
|
import { CATALOGUE_FILE, primitivesNaming, readCatalogue } from "../catalogue.js";
|
|
7
7
|
import { anchorsOf } from "../markdown.js";
|
|
8
|
-
import {
|
|
8
|
+
import { optionsOf } from "./check.js";
|
|
9
9
|
import { decisionRecords, OUT_OF_FORCE, recordStatus } from "./decision-records.js";
|
|
10
10
|
import { treeFiles } from "./tree.js";
|
|
11
11
|
const MAX_SUMMARY = 120;
|
|
@@ -68,16 +68,16 @@ function judge(root, catalogue, primitive, at, cache, records) {
|
|
|
68
68
|
}
|
|
69
69
|
return findings;
|
|
70
70
|
}
|
|
71
|
-
function run(root, { file, roots }) {
|
|
71
|
+
function run(root, { file, roots }, gate) {
|
|
72
72
|
const read = readCatalogue(root, file);
|
|
73
73
|
if (read.state === "missing") {
|
|
74
|
-
throw new CannotRun(
|
|
74
|
+
throw new CannotRun("the catalogue does not exist", REMEDIES.missing, { file });
|
|
75
75
|
}
|
|
76
76
|
if (read.state === "invalid") {
|
|
77
77
|
return read.problems.map((message) => ({ file, message, remedy: REMEDIES.shape }));
|
|
78
78
|
}
|
|
79
79
|
const { catalogue, text } = read;
|
|
80
|
-
const records = (optionsOf(decisionRecords,
|
|
80
|
+
const records = (optionsOf(decisionRecords, gate.checks["decision-records"]) ?? decisionRecords.defaults).dirs;
|
|
81
81
|
const findings = [];
|
|
82
82
|
const seen = new Map();
|
|
83
83
|
const cache = new Map();
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// a Task Router row of the agent map, through a path, a directory above it or a glob, or by the docs of
|
|
3
3
|
// a Primitive in the catalogue. It reads the file list, the Task Router and the catalogue, never prose.
|
|
4
4
|
import { matchesGlob, posix } from "node:path";
|
|
5
|
-
import { resolveConfig } from "../agentic-config.js";
|
|
6
5
|
import { readCatalogue } from "../catalogue.js";
|
|
7
6
|
import { mapFile, readMap, routes } from "./agents-map.js";
|
|
8
7
|
import { catalogueFile } from "./primitives.js";
|
|
@@ -25,8 +24,7 @@ function catalogueDocs(root, file) {
|
|
|
25
24
|
const read = readCatalogue(root, file);
|
|
26
25
|
return read.state === "valid" ? read.catalogue.primitives.flatMap(({ docs }) => docs) : [];
|
|
27
26
|
}
|
|
28
|
-
function run(root, { files, exempt }) {
|
|
29
|
-
const { checks } = resolveConfig(root).config.gate;
|
|
27
|
+
function run(root, { files, exempt }, { checks }) {
|
|
30
28
|
const map = mapFile(checks);
|
|
31
29
|
const rows = routes(readMap(root, map));
|
|
32
30
|
const findings = [];
|
|
@@ -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,
|