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
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
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// `dw-sdlc lead start`: starts the next session as a Lead in a herdr terminal of its own, beside the caller and
|
|
2
2
|
// outliving it, on the command the config names for a Worker, without what makes it unsafe or isolated.
|
|
3
|
-
import { parseArgs } from "node:util";
|
|
4
3
|
import { mainCheckout, repositoryRoot, resolveConfig } from "../agentic-config.js";
|
|
5
|
-
import {
|
|
4
|
+
import { CannotRun, readArgs, usageError } from "../cannot-run.js";
|
|
5
|
+
import { lead, wholeNumber } from "../worker/herdr.js";
|
|
6
6
|
import { launch, NAME, spaceOf, TAB_WAIT, undelivered } from "../worker/launch.js";
|
|
7
7
|
export const summary = "start the next session as a Lead in a herdr terminal of its own";
|
|
8
8
|
const USAGE = `Usage: dw-sdlc lead start --name <name> --prompt <first message> [--wait <seconds>] [--json]
|
|
@@ -33,36 +33,31 @@ const UNSAFE = "--dangerously-skip-permissions";
|
|
|
33
33
|
/** The flags that put a Claude Code session in a worktree of its own, each taking an optional name. */
|
|
34
34
|
const WORKTREE = new Set(["--worktree", "-w"]);
|
|
35
35
|
function parseCli(argv) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
wait: { type: "string" }
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
if (values.help === true) {
|
|
48
|
-
return "help";
|
|
49
|
-
}
|
|
50
|
-
const { name, prompt } = values;
|
|
51
|
-
if (name === undefined || !NAME.test(name)) {
|
|
52
|
-
return { error: `--name takes 1-32 of a-z, 0-9, - and _, not ${JSON.stringify(name ?? "")}` };
|
|
53
|
-
}
|
|
54
|
-
if (prompt === undefined || prompt.trim() === "") {
|
|
55
|
-
return { error: "give the Lead its first message: --prompt <first message>" };
|
|
36
|
+
const { values } = readArgs({
|
|
37
|
+
args: argv,
|
|
38
|
+
options: {
|
|
39
|
+
help: { type: "boolean", short: "h" },
|
|
40
|
+
json: { type: "boolean" },
|
|
41
|
+
name: { type: "string" },
|
|
42
|
+
prompt: { type: "string" },
|
|
43
|
+
wait: { type: "string" }
|
|
56
44
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
45
|
+
}, USAGE);
|
|
46
|
+
if (values.help === true) {
|
|
47
|
+
return { help: true };
|
|
48
|
+
}
|
|
49
|
+
const { name, prompt } = values;
|
|
50
|
+
if (name === undefined || !NAME.test(name)) {
|
|
51
|
+
throw usageError(`--name takes 1-32 of a-z, 0-9, - and _, not ${JSON.stringify(name ?? "")}`, USAGE);
|
|
62
52
|
}
|
|
63
|
-
|
|
64
|
-
|
|
53
|
+
if (prompt === undefined || prompt.trim() === "") {
|
|
54
|
+
throw usageError("give the Lead its first message: --prompt <first message>", USAGE);
|
|
65
55
|
}
|
|
56
|
+
const wait = wholeNumber("--wait", values.wait, DEFAULT_WAIT, 0);
|
|
57
|
+
if (typeof wait === "object") {
|
|
58
|
+
throw usageError(wait.error, USAGE);
|
|
59
|
+
}
|
|
60
|
+
return { help: false, name, prompt, wait, json: values.json === true };
|
|
66
61
|
}
|
|
67
62
|
/** `words` without the flag that skips permissions and without a worktree flag, its name with it when it has one. */
|
|
68
63
|
function leadCommand(words) {
|
|
@@ -88,11 +83,11 @@ async function start(cli) {
|
|
|
88
83
|
const checkout = repositoryRoot(process.cwd());
|
|
89
84
|
const main = mainCheckout(checkout);
|
|
90
85
|
if (main !== undefined) {
|
|
91
|
-
throw new
|
|
86
|
+
throw new CannotRun(`in a linked worktree, ${checkout}: a Lead works in the main checkout, and this session cannot start one there`, `run it from the main checkout, ${main}, or start the Lead there by hand`);
|
|
92
87
|
}
|
|
93
88
|
const command = leadCommand(resolveConfig(checkout).config.worker.command.split(/\s+/).filter(Boolean));
|
|
94
89
|
if (command.length === 0) {
|
|
95
|
-
throw new
|
|
90
|
+
throw new CannotRun("worker.command holds no command", "set it in .ai/agentic.config.local.json, or remove the key");
|
|
96
91
|
}
|
|
97
92
|
const { address, tab, reason } = await launch("lead start", {
|
|
98
93
|
name: cli.name,
|
|
@@ -118,18 +113,9 @@ async function start(cli) {
|
|
|
118
113
|
}
|
|
119
114
|
export async function run(argv) {
|
|
120
115
|
const cli = parseCli(argv);
|
|
121
|
-
if (cli
|
|
116
|
+
if (cli.help) {
|
|
122
117
|
process.stdout.write(`${USAGE}\n`);
|
|
123
118
|
return 0;
|
|
124
119
|
}
|
|
125
|
-
|
|
126
|
-
process.stderr.write(`dw-sdlc lead start: ${cli.error}\n\n${USAGE}\n`);
|
|
127
|
-
return 2;
|
|
128
|
-
}
|
|
129
|
-
try {
|
|
130
|
-
return await start(cli);
|
|
131
|
-
}
|
|
132
|
-
catch (error) {
|
|
133
|
-
return refuse("lead start", error);
|
|
134
|
-
}
|
|
120
|
+
return start(cli);
|
|
135
121
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// `dw-sdlc pr reviewed <n> --head <sha>`: pins a Review to the head it read, as the commit status
|
|
2
2
|
// `dw-sdlc/review`: success for a clean one, --findings the read mark of one that left a Finding open,
|
|
3
3
|
// --accept the dev's acceptance of a head as it stands, which the review-limit park names.
|
|
4
|
-
import {
|
|
4
|
+
import { CannotRun, readArgs, usageError } from "../cannot-run.js";
|
|
5
5
|
import { REVIEW, REVIEW_STATE, reviewStatusArgs } from "../pr/decide.js";
|
|
6
|
-
import {
|
|
6
|
+
import { gh, namedProblem, pullRequestNamed, said } from "../pr/read.js";
|
|
7
7
|
import { ACCEPTED, MARK_STATE, READ_MARK } from "../pr/state.js";
|
|
8
8
|
export const summary = "pin a Review to the head it read, as a commit status";
|
|
9
9
|
const USAGE = `Usage: dw-sdlc pr reviewed <n> [-R owner/name] --head <sha> [--findings | --accept] [--json]
|
|
@@ -25,54 +25,49 @@ const STATUS = {
|
|
|
25
25
|
accept: { state: REVIEW_STATE, description: ACCEPTED }
|
|
26
26
|
};
|
|
27
27
|
function parseCli(argv) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
accept: { type: "boolean" }
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
if (values.help === true) {
|
|
42
|
-
return "help";
|
|
28
|
+
const { values, positionals } = readArgs({
|
|
29
|
+
args: argv,
|
|
30
|
+
allowPositionals: true,
|
|
31
|
+
options: {
|
|
32
|
+
help: { type: "boolean", short: "h" },
|
|
33
|
+
json: { type: "boolean" },
|
|
34
|
+
repo: { type: "string", short: "R" },
|
|
35
|
+
head: { type: "string" },
|
|
36
|
+
findings: { type: "boolean" },
|
|
37
|
+
accept: { type: "boolean" }
|
|
43
38
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
if (head === undefined) {
|
|
57
|
-
return { error: "name the head the review read: --head <sha>" };
|
|
58
|
-
}
|
|
59
|
-
if (!/^[0-9a-f]{40}$/.test(head)) {
|
|
60
|
-
return { error: `--head takes the full 40-character sha, not ${JSON.stringify(head)}` };
|
|
61
|
-
}
|
|
62
|
-
if (values.findings === true && values.accept === true) {
|
|
63
|
-
return { error: "--findings and --accept contradict: name one" };
|
|
64
|
-
}
|
|
65
|
-
const kind = values.findings === true ? "findings" : "clean";
|
|
66
|
-
const status = STATUS[values.accept === true ? "accept" : kind];
|
|
67
|
-
return { json: values.json === true, number: Number(number), repo, head, status };
|
|
39
|
+
}, USAGE);
|
|
40
|
+
if (values.help === true) {
|
|
41
|
+
return { help: true };
|
|
42
|
+
}
|
|
43
|
+
const [number, ...more] = positionals;
|
|
44
|
+
const { repo, head } = values;
|
|
45
|
+
if (number === undefined) {
|
|
46
|
+
throw usageError("name the pull request: dw-sdlc pr reviewed <n> --head <sha>", USAGE);
|
|
47
|
+
}
|
|
48
|
+
if (more.length > 0) {
|
|
49
|
+
throw usageError(`name one pull request, not ${positionals.length}`, USAGE);
|
|
68
50
|
}
|
|
69
|
-
|
|
70
|
-
|
|
51
|
+
const problem = namedProblem(number, repo);
|
|
52
|
+
if (problem !== undefined) {
|
|
53
|
+
throw usageError(problem, USAGE);
|
|
71
54
|
}
|
|
55
|
+
if (head === undefined) {
|
|
56
|
+
throw usageError("name the head the review read: --head <sha>", USAGE);
|
|
57
|
+
}
|
|
58
|
+
if (!/^[0-9a-f]{40}$/.test(head)) {
|
|
59
|
+
throw usageError(`--head takes the full 40-character sha, not ${JSON.stringify(head)}`, USAGE);
|
|
60
|
+
}
|
|
61
|
+
if (values.findings === true && values.accept === true) {
|
|
62
|
+
throw usageError("--findings and --accept contradict: name one", USAGE);
|
|
63
|
+
}
|
|
64
|
+
const kind = values.findings === true ? "findings" : "clean";
|
|
65
|
+
const status = STATUS[values.accept === true ? "accept" : kind];
|
|
66
|
+
return { help: false, json: values.json === true, number: Number(number), repo, head, status };
|
|
72
67
|
}
|
|
73
68
|
/** Posts the review status on `head` once the read shows it is still the pull request's head. */
|
|
74
69
|
function post({ number, repo, head, status }) {
|
|
75
|
-
const pr =
|
|
70
|
+
const pr = pullRequestNamed({ number, repo }, "pr reviewed");
|
|
76
71
|
if (pr.head !== head) {
|
|
77
72
|
throw new CannotRun(`${pr.repo}#${number}: the head is ${pr.head}, not ${head}: a push landed since the review`, "review the new head, then call again with its sha");
|
|
78
73
|
}
|
|
@@ -83,24 +78,11 @@ function post({ number, repo, head, status }) {
|
|
|
83
78
|
}
|
|
84
79
|
export function run(argv) {
|
|
85
80
|
const cli = parseCli(argv);
|
|
86
|
-
if (cli
|
|
81
|
+
if (cli.help) {
|
|
87
82
|
process.stdout.write(`${USAGE}\n`);
|
|
88
83
|
return 0;
|
|
89
84
|
}
|
|
90
|
-
|
|
91
|
-
process.stderr.write(`dw-sdlc pr reviewed: ${cli.error}\n\n${USAGE}\n`);
|
|
92
|
-
return 2;
|
|
93
|
-
}
|
|
94
|
-
try {
|
|
95
|
-
post(cli);
|
|
96
|
-
}
|
|
97
|
-
catch (error) {
|
|
98
|
-
if (error instanceof CannotRun) {
|
|
99
|
-
process.stderr.write(`dw-sdlc pr reviewed: ${error.message} — ${error.remedy}\n`);
|
|
100
|
-
return 2;
|
|
101
|
-
}
|
|
102
|
-
throw error;
|
|
103
|
-
}
|
|
85
|
+
post(cli);
|
|
104
86
|
const { head } = cli;
|
|
105
87
|
const { state } = cli.status;
|
|
106
88
|
const answer = cli.json
|
package/dist/commands/pr-ship.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// `dw-sdlc pr ship [<n>]`, the Ship script: reads one pull request through gh, decides the Action it
|
|
2
2
|
// needs next, performs it when it is the script's and reads again, then answers the first Action that
|
|
3
3
|
// is not, as a line or one JSON document, with an exit code that names whose turn it is.
|
|
4
|
-
import { parseArgs } from "node:util";
|
|
5
4
|
import { repositoryRoot, resolveConfig, REVIEW_DEPTHS } from "../agentic-config.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import { CannotRun, readArgs, usageError } from "../cannot-run.js";
|
|
6
|
+
import { decide, declaredPolicy, devCommand, hostArgs, isScriptAction, lineOf, readyToMerge, REVIEW } from "../pr/decide.js";
|
|
7
|
+
import { gh, ghReads, namedProblem, nameItRemedy, now, said } from "../pr/read.js";
|
|
8
|
+
import { shellLine } from "../shell-line.js";
|
|
9
9
|
export const summary = "decide the Action one pull request needs next, as a line or JSON";
|
|
10
10
|
const USAGE = `Usage: dw-sdlc pr ship [<n>] [-R owner/name] [--merge] [--review full|light|off] [--dry-run] [--json] [--verbose]
|
|
11
11
|
|
|
@@ -22,13 +22,18 @@ ${REVIEW} success gets the Review depth --review names for this call, else pr.re
|
|
|
22
22
|
the agent reviews it with the command pr.reviewCommand names, /code-review by default; under light
|
|
23
23
|
one sdlc-reviewer subagent reads the diff; under off the script posts the success itself, its
|
|
24
24
|
description saying why. Under auto, the default, the script routes the depth from the changes the
|
|
25
|
-
Review would read: lockfiles alone off; a file under .github/workflows/,
|
|
26
|
-
lines outside lockfiles, or a change it cannot size, more files than GitHub's
|
|
27
|
-
file outside lockfiles with no patch, full; the rest light. After a push it names
|
|
25
|
+
Review would read: lockfiles alone off, unless a Finding is open; a file under .github/workflows/,
|
|
26
|
+
more than 100 changed lines outside lockfiles, or a change it cannot size, more files than GitHub's
|
|
27
|
+
compare lists or a file outside lockfiles with no patch, full; the rest light. After a push it names
|
|
28
28
|
the last commit a Review read under details.since, where the Review reads from and auto routes from:
|
|
29
29
|
one with a ${REVIEW} success not posted under off, or the read mark pr reviewed --findings
|
|
30
30
|
posts. It carries a success over a head whose only commits since are merges of the base, unless
|
|
31
|
-
--review names a depth for the call; a read mark never carries.
|
|
31
|
+
--review names a depth for the call; a read mark never carries. A read mark no later commit answers,
|
|
32
|
+
on the head or before commits that leave the own changes as it read them, is named under
|
|
33
|
+
details.open: the Review then reads the whole pull request, and nothing carries over it. After a merge or a rebase, details.paths names
|
|
34
|
+
the files whose own change moved, the only ones the Review reads. Three Reviews that left a Finding
|
|
35
|
+
open with no success between them park as review-limit at every depth, pr reviewed --accept the
|
|
36
|
+
dev's command: the dev's --review <depth> buys one more Review, and their --review off posts past it.
|
|
32
37
|
Each Action the call runs is listed under performed. Its ok says whether the read after it shows the
|
|
33
38
|
Action took effect, whatever gh's exit status; gh's exit status and stderr stand beside it, so an ok
|
|
34
39
|
false with exit 0 is an Action gh accepted that the read does not show yet. An update counts when
|
|
@@ -43,58 +48,57 @@ Exit 0 done, 10 the agent's turn, 20 wait, 30 the dev's turn, 2 when it cannot r
|
|
|
43
48
|
/** The depths --review takes, strictest first, as a sentence lists them. */
|
|
44
49
|
const DEPTHS_SENTENCE = `${REVIEW_DEPTHS.slice(0, -1).join(", ")} or ${REVIEW_DEPTHS.at(-1)}`;
|
|
45
50
|
const EXIT = { done: 0, act: 10, wait: 20, park: 30 };
|
|
46
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* A call that stopped after it ran Actions: what it ran stands in `performed`, `ok` null where no read
|
|
53
|
+
* followed, and each Action is named with gh's exit under the problem.
|
|
54
|
+
*/
|
|
47
55
|
class Interrupted extends CannotRun {
|
|
48
56
|
performed;
|
|
49
57
|
constructor({ message, remedy }, performed) {
|
|
50
|
-
|
|
58
|
+
const ran = performed.map(({ action, exit }) => `${action} ran (gh exit ${exit ?? "unknown"})`);
|
|
59
|
+
super(message, remedy, { after: ran.join("\n") });
|
|
51
60
|
this.performed = performed;
|
|
52
61
|
}
|
|
53
62
|
}
|
|
54
63
|
function parseCli(argv) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return { error: `--review takes ${DEPTHS_SENTENCE}, not ${JSON.stringify(values.review)}` };
|
|
82
|
-
}
|
|
64
|
+
const { values, positionals } = readArgs({
|
|
65
|
+
args: argv,
|
|
66
|
+
allowPositionals: true,
|
|
67
|
+
options: {
|
|
68
|
+
help: { type: "boolean", short: "h" },
|
|
69
|
+
json: { type: "boolean" },
|
|
70
|
+
verbose: { type: "boolean" },
|
|
71
|
+
merge: { type: "boolean" },
|
|
72
|
+
review: { type: "string" },
|
|
73
|
+
"dry-run": { type: "boolean" },
|
|
74
|
+
repo: { type: "string", short: "R" }
|
|
75
|
+
}
|
|
76
|
+
}, USAGE);
|
|
77
|
+
const [number, ...more] = positionals;
|
|
78
|
+
const { repo } = values;
|
|
79
|
+
const review = REVIEW_DEPTHS.find((depth) => depth === values.review);
|
|
80
|
+
if (values.help !== true) {
|
|
81
|
+
if (more.length > 0) {
|
|
82
|
+
throw usageError(`name one pull request, not ${positionals.length}`, USAGE);
|
|
83
|
+
}
|
|
84
|
+
const problem = namedProblem(number, repo);
|
|
85
|
+
if (problem !== undefined) {
|
|
86
|
+
throw usageError(problem, USAGE);
|
|
87
|
+
}
|
|
88
|
+
if (values.review !== undefined && review === undefined) {
|
|
89
|
+
throw usageError(`--review takes ${DEPTHS_SENTENCE}, not ${JSON.stringify(values.review)}`, USAGE);
|
|
83
90
|
}
|
|
84
|
-
return {
|
|
85
|
-
help: values.help === true,
|
|
86
|
-
json: values.json === true,
|
|
87
|
-
verbose: values.verbose === true,
|
|
88
|
-
merge: values.merge === true,
|
|
89
|
-
review,
|
|
90
|
-
dryRun: values["dry-run"] === true,
|
|
91
|
-
number: number === undefined ? undefined : Number(number),
|
|
92
|
-
repo
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
return { error: error instanceof Error ? error.message : String(error) };
|
|
97
91
|
}
|
|
92
|
+
return {
|
|
93
|
+
help: values.help === true,
|
|
94
|
+
json: values.json === true,
|
|
95
|
+
verbose: values.verbose === true,
|
|
96
|
+
merge: values.merge === true,
|
|
97
|
+
review,
|
|
98
|
+
dryRun: values["dry-run"] === true,
|
|
99
|
+
number: number === undefined ? undefined : Number(number),
|
|
100
|
+
repo
|
|
101
|
+
};
|
|
98
102
|
}
|
|
99
103
|
/** How a reason names each script Action: what the script would do, the act, and the state it acts on. */
|
|
100
104
|
const NAMED = {
|
|
@@ -113,7 +117,7 @@ const NAMED = {
|
|
|
113
117
|
*/
|
|
114
118
|
const TOOK = {
|
|
115
119
|
// A moved head alone may be a push: the update took effect when the head carries the base it read.
|
|
116
|
-
"update-branch": (before, after) => after.head !== before.head && carriesBase(before, after),
|
|
120
|
+
"update-branch": (before, after, reads) => after.head !== before.head && reads.carriesBase(before, after),
|
|
117
121
|
"mark-ready": (_, after) => !after.draft,
|
|
118
122
|
// The post names the head it read: a head moved since carries none of it.
|
|
119
123
|
"mark-reviewed": (before, after) => after.head === before.head && after.checks.some((check) => check.name === REVIEW && check.bucket === "pass"),
|
|
@@ -141,27 +145,17 @@ function handedOver(pr, policy, answer, action, reason) {
|
|
|
141
145
|
}
|
|
142
146
|
return { ...answer, status: "park", actor: "dev", reason, command: devCommand(pr, action, policy) };
|
|
143
147
|
}
|
|
144
|
-
/**
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
*/
|
|
149
|
-
function readTarget(cli, root, sized) {
|
|
150
|
-
const renamed = cli.number === undefined && cli.repo === undefined ? renamedUpstream(root) : undefined;
|
|
151
|
-
if (renamed === undefined) {
|
|
152
|
-
return read(cli, "pr ship", sized);
|
|
153
|
-
}
|
|
154
|
-
const { branch, head, upstream } = renamed;
|
|
155
|
-
const found = readBranch(sized, upstream);
|
|
156
|
-
const upstreamNamed = `${upstream}, the name its upstream gives`;
|
|
157
|
-
if (found === null) {
|
|
158
|
-
throw new CannotRun(`gh pr view found no pull request for ${branch}, nor for ${upstreamNamed}`, nameItRemedy("pr ship"));
|
|
148
|
+
/** The pull request the call names, else this checkout's; none found stops the call with what to name. */
|
|
149
|
+
function readTarget(cli, root, reads) {
|
|
150
|
+
if (cli.number !== undefined || cli.repo !== undefined) {
|
|
151
|
+
return reads.named(cli, "pr ship");
|
|
159
152
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
153
|
+
const found = reads.checkout(root);
|
|
154
|
+
if ("problem" in found) {
|
|
155
|
+
const nameIt = nameItRemedy("pr ship");
|
|
156
|
+
throw new CannotRun(found.problem, found.lacksHead ? `push HEAD if it is this branch's pull request, else ${nameIt}` : nameIt);
|
|
163
157
|
}
|
|
164
|
-
return found
|
|
158
|
+
return found;
|
|
165
159
|
}
|
|
166
160
|
/**
|
|
167
161
|
* Reads and decides until the Action is not the script's. Each script Action runs at most once, then the
|
|
@@ -169,8 +163,8 @@ function readTarget(cli, root, sized) {
|
|
|
169
163
|
*/
|
|
170
164
|
function ship(cli, root, policy) {
|
|
171
165
|
// Only pr.review auto routes the depth from the changes, so only it reads them.
|
|
172
|
-
const
|
|
173
|
-
let pr = readTarget(cli, root,
|
|
166
|
+
const reads = ghReads(policy.review.depth === "auto");
|
|
167
|
+
let pr = readTarget(cli, root, reads);
|
|
174
168
|
const performed = [];
|
|
175
169
|
// The head each Action ran on.
|
|
176
170
|
const postedOn = new Map();
|
|
@@ -204,8 +198,8 @@ function ship(cli, root, policy) {
|
|
|
204
198
|
const record = { action, command, exit: status, ...(message === "" ? {} : { stderr: message }) };
|
|
205
199
|
postedOn.set(action, before.head);
|
|
206
200
|
try {
|
|
207
|
-
pr =
|
|
208
|
-
performed.push({ ...record, ok: TOOK[action](before, pr) });
|
|
201
|
+
pr = reads.again(pr);
|
|
202
|
+
performed.push({ ...record, ok: TOOK[action](before, pr, reads) });
|
|
209
203
|
}
|
|
210
204
|
catch (error) {
|
|
211
205
|
if (error instanceof CannotRun) {
|
|
@@ -222,10 +216,6 @@ function documentOf(pr, answer, performed, verbose) {
|
|
|
222
216
|
}
|
|
223
217
|
export function run(argv) {
|
|
224
218
|
const cli = parseCli(argv);
|
|
225
|
-
if ("error" in cli) {
|
|
226
|
-
process.stderr.write(`dw-sdlc pr ship: ${cli.error}\n\n${USAGE}\n`);
|
|
227
|
-
return 2;
|
|
228
|
-
}
|
|
229
219
|
if (cli.help) {
|
|
230
220
|
process.stdout.write(`${USAGE}\n`);
|
|
231
221
|
return 0;
|
|
@@ -243,14 +233,9 @@ export function run(argv) {
|
|
|
243
233
|
shipped = ship(cli, root, policy);
|
|
244
234
|
}
|
|
245
235
|
catch (error) {
|
|
246
|
-
if (error instanceof
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
process.stderr.write(`dw-sdlc pr ship: ${error.message} — ${error.remedy}\n${named.join("")}`);
|
|
250
|
-
if (cli.json && ran.length > 0) {
|
|
251
|
-
process.stdout.write(`${JSON.stringify({ error: error.message, remedy: error.remedy, performed: ran }, null, 2)}\n`);
|
|
252
|
-
}
|
|
253
|
-
return 2;
|
|
236
|
+
if (cli.json && error instanceof Interrupted) {
|
|
237
|
+
const { message, remedy, performed } = error;
|
|
238
|
+
process.stdout.write(`${JSON.stringify({ error: message, remedy, performed }, null, 2)}\n`);
|
|
254
239
|
}
|
|
255
240
|
throw error;
|
|
256
241
|
}
|