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
|
@@ -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,16 +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
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
|
|
33
|
-
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.
|
|
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.
|
|
35
37
|
Each Action the call runs is listed under performed. Its ok says whether the read after it shows the
|
|
36
38
|
Action took effect, whatever gh's exit status; gh's exit status and stderr stand beside it, so an ok
|
|
37
39
|
false with exit 0 is an Action gh accepted that the read does not show yet. An update counts when
|
|
@@ -46,58 +48,57 @@ Exit 0 done, 10 the agent's turn, 20 wait, 30 the dev's turn, 2 when it cannot r
|
|
|
46
48
|
/** The depths --review takes, strictest first, as a sentence lists them. */
|
|
47
49
|
const DEPTHS_SENTENCE = `${REVIEW_DEPTHS.slice(0, -1).join(", ")} or ${REVIEW_DEPTHS.at(-1)}`;
|
|
48
50
|
const EXIT = { done: 0, act: 10, wait: 20, park: 30 };
|
|
49
|
-
/**
|
|
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
|
+
*/
|
|
50
55
|
class Interrupted extends CannotRun {
|
|
51
56
|
performed;
|
|
52
57
|
constructor({ message, remedy }, performed) {
|
|
53
|
-
|
|
58
|
+
const ran = performed.map(({ action, exit }) => `${action} ran (gh exit ${exit ?? "unknown"})`);
|
|
59
|
+
super(message, remedy, { after: ran.join("\n") });
|
|
54
60
|
this.performed = performed;
|
|
55
61
|
}
|
|
56
62
|
}
|
|
57
63
|
function parseCli(argv) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return { error: `--review takes ${DEPTHS_SENTENCE}, not ${JSON.stringify(values.review)}` };
|
|
85
|
-
}
|
|
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);
|
|
86
90
|
}
|
|
87
|
-
return {
|
|
88
|
-
help: values.help === true,
|
|
89
|
-
json: values.json === true,
|
|
90
|
-
verbose: values.verbose === true,
|
|
91
|
-
merge: values.merge === true,
|
|
92
|
-
review,
|
|
93
|
-
dryRun: values["dry-run"] === true,
|
|
94
|
-
number: number === undefined ? undefined : Number(number),
|
|
95
|
-
repo
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
catch (error) {
|
|
99
|
-
return { error: error instanceof Error ? error.message : String(error) };
|
|
100
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
|
+
};
|
|
101
102
|
}
|
|
102
103
|
/** How a reason names each script Action: what the script would do, the act, and the state it acts on. */
|
|
103
104
|
const NAMED = {
|
|
@@ -116,7 +117,7 @@ const NAMED = {
|
|
|
116
117
|
*/
|
|
117
118
|
const TOOK = {
|
|
118
119
|
// A moved head alone may be a push: the update took effect when the head carries the base it read.
|
|
119
|
-
"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),
|
|
120
121
|
"mark-ready": (_, after) => !after.draft,
|
|
121
122
|
// The post names the head it read: a head moved since carries none of it.
|
|
122
123
|
"mark-reviewed": (before, after) => after.head === before.head && after.checks.some((check) => check.name === REVIEW && check.bucket === "pass"),
|
|
@@ -144,27 +145,17 @@ function handedOver(pr, policy, answer, action, reason) {
|
|
|
144
145
|
}
|
|
145
146
|
return { ...answer, status: "park", actor: "dev", reason, command: devCommand(pr, action, policy) };
|
|
146
147
|
}
|
|
147
|
-
/**
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
*/
|
|
152
|
-
function readTarget(cli, root, sized) {
|
|
153
|
-
const renamed = cli.number === undefined && cli.repo === undefined ? renamedUpstream(root) : undefined;
|
|
154
|
-
if (renamed === undefined) {
|
|
155
|
-
return read(cli, "pr ship", sized);
|
|
156
|
-
}
|
|
157
|
-
const { branch, head, upstream } = renamed;
|
|
158
|
-
const found = readBranch(sized, upstream);
|
|
159
|
-
const upstreamNamed = `${upstream}, the name its upstream gives`;
|
|
160
|
-
if (found === null) {
|
|
161
|
-
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");
|
|
162
152
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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);
|
|
166
157
|
}
|
|
167
|
-
return found
|
|
158
|
+
return found;
|
|
168
159
|
}
|
|
169
160
|
/**
|
|
170
161
|
* Reads and decides until the Action is not the script's. Each script Action runs at most once, then the
|
|
@@ -172,8 +163,8 @@ function readTarget(cli, root, sized) {
|
|
|
172
163
|
*/
|
|
173
164
|
function ship(cli, root, policy) {
|
|
174
165
|
// Only pr.review auto routes the depth from the changes, so only it reads them.
|
|
175
|
-
const
|
|
176
|
-
let pr = readTarget(cli, root,
|
|
166
|
+
const reads = ghReads(policy.review.depth === "auto");
|
|
167
|
+
let pr = readTarget(cli, root, reads);
|
|
177
168
|
const performed = [];
|
|
178
169
|
// The head each Action ran on.
|
|
179
170
|
const postedOn = new Map();
|
|
@@ -207,8 +198,8 @@ function ship(cli, root, policy) {
|
|
|
207
198
|
const record = { action, command, exit: status, ...(message === "" ? {} : { stderr: message }) };
|
|
208
199
|
postedOn.set(action, before.head);
|
|
209
200
|
try {
|
|
210
|
-
pr =
|
|
211
|
-
performed.push({ ...record, ok: TOOK[action](before, pr) });
|
|
201
|
+
pr = reads.again(pr);
|
|
202
|
+
performed.push({ ...record, ok: TOOK[action](before, pr, reads) });
|
|
212
203
|
}
|
|
213
204
|
catch (error) {
|
|
214
205
|
if (error instanceof CannotRun) {
|
|
@@ -225,10 +216,6 @@ function documentOf(pr, answer, performed, verbose) {
|
|
|
225
216
|
}
|
|
226
217
|
export function run(argv) {
|
|
227
218
|
const cli = parseCli(argv);
|
|
228
|
-
if ("error" in cli) {
|
|
229
|
-
process.stderr.write(`dw-sdlc pr ship: ${cli.error}\n\n${USAGE}\n`);
|
|
230
|
-
return 2;
|
|
231
|
-
}
|
|
232
219
|
if (cli.help) {
|
|
233
220
|
process.stdout.write(`${USAGE}\n`);
|
|
234
221
|
return 0;
|
|
@@ -246,14 +233,9 @@ export function run(argv) {
|
|
|
246
233
|
shipped = ship(cli, root, policy);
|
|
247
234
|
}
|
|
248
235
|
catch (error) {
|
|
249
|
-
if (error instanceof
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
process.stderr.write(`dw-sdlc pr ship: ${error.message} — ${error.remedy}\n${named.join("")}`);
|
|
253
|
-
if (cli.json && ran.length > 0) {
|
|
254
|
-
process.stdout.write(`${JSON.stringify({ error: error.message, remedy: error.remedy, performed: ran }, null, 2)}\n`);
|
|
255
|
-
}
|
|
256
|
-
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`);
|
|
257
239
|
}
|
|
258
240
|
throw error;
|
|
259
241
|
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// Primitives a change removes, adds or extends.
|
|
4
4
|
import { existsSync, realpathSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
6
|
-
import { parseArgs } from "node:util";
|
|
7
6
|
import { repositoryRoot, resolveConfig } from "../agentic-config.js";
|
|
7
|
+
import { CannotRun, readArgs, rootDir, usageError } from "../cannot-run.js";
|
|
8
8
|
import { primitivesNaming, readCatalogue } from "../catalogue.js";
|
|
9
9
|
import { catalogueFile } from "../checks/primitives.js";
|
|
10
10
|
import { blockOf, primitivesDiff } from "../primitives-diff.js";
|
|
@@ -25,44 +25,42 @@ for a pull request body, which a later run replaces. --json prints instead, per
|
|
|
25
25
|
class, file count, summary and docs, each doc with whether the change touches its file, and the count of
|
|
26
26
|
changed files outside every Primitive. It takes no --paths.
|
|
27
27
|
|
|
28
|
-
Exit 0 with an answer, 2 when there is no catalogue to read or it is
|
|
29
|
-
does not resolve.`;
|
|
28
|
+
Exit 0 with an answer, 2 when --root names no directory, when there is no catalogue to read or it is
|
|
29
|
+
not one, and for --diff when <base> does not resolve.`;
|
|
30
30
|
const REMEDIES = {
|
|
31
31
|
missing: "create it, or point gate.checks.primitives.file at the catalogue",
|
|
32
32
|
invalid: "fix the catalogue; dw-sdlc gate names each problem under the primitives Check"
|
|
33
33
|
};
|
|
34
34
|
function parseCli(argv) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
root: { type: "string" }
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
const cli = {
|
|
48
|
-
help: values.help === true,
|
|
49
|
-
json: values.json === true,
|
|
50
|
-
paths: values.paths === true,
|
|
51
|
-
diff: values.diff,
|
|
52
|
-
files: positionals,
|
|
53
|
-
root: values.root
|
|
54
|
-
};
|
|
55
|
-
if (cli.help) {
|
|
56
|
-
return cli;
|
|
35
|
+
const { values, positionals } = readArgs({
|
|
36
|
+
args: argv,
|
|
37
|
+
allowPositionals: true,
|
|
38
|
+
options: {
|
|
39
|
+
help: { type: "boolean", short: "h" },
|
|
40
|
+
json: { type: "boolean" },
|
|
41
|
+
paths: { type: "boolean" },
|
|
42
|
+
diff: { type: "string" },
|
|
43
|
+
root: { type: "string" }
|
|
57
44
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
45
|
+
}, USAGE);
|
|
46
|
+
const cli = {
|
|
47
|
+
help: values.help === true,
|
|
48
|
+
json: values.json === true,
|
|
49
|
+
paths: values.paths === true,
|
|
50
|
+
diff: values.diff,
|
|
51
|
+
files: positionals,
|
|
52
|
+
root: values.root
|
|
53
|
+
};
|
|
54
|
+
if (cli.help) {
|
|
55
|
+
return cli;
|
|
56
|
+
}
|
|
57
|
+
if (cli.diff !== undefined && (cli.paths || cli.files.length > 0)) {
|
|
58
|
+
throw usageError("--diff and --paths exclude each other — ask one question at a time", USAGE);
|
|
62
59
|
}
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
if (!cli.paths && cli.diff === undefined) {
|
|
61
|
+
throw usageError("name the paths with --paths, or a base with --diff", USAGE);
|
|
65
62
|
}
|
|
63
|
+
return cli;
|
|
66
64
|
}
|
|
67
65
|
/**
|
|
68
66
|
* `path` with every symbolic link in it resolved, as git reports the root: the nearest part on disk
|
|
@@ -87,26 +85,27 @@ function plain(path, primitives) {
|
|
|
87
85
|
` read: ${docs.join(", ")}`
|
|
88
86
|
]);
|
|
89
87
|
}
|
|
88
|
+
/** Stops the command on every one of `problems`, each on a line of its own, as the bin prints the first. */
|
|
89
|
+
function stopOn(problems) {
|
|
90
|
+
const [first = "the catalogue cannot be read", ...rest] = problems;
|
|
91
|
+
const after = rest.map((problem) => `dw-sdlc primitives: ${problem}`).join("\n");
|
|
92
|
+
throw new CannotRun(first, undefined, rest.length === 0 ? {} : { after });
|
|
93
|
+
}
|
|
90
94
|
function runDiff(root, base, json) {
|
|
91
95
|
const answer = primitivesDiff(root, base);
|
|
92
96
|
if ("problems" in answer) {
|
|
93
|
-
|
|
94
|
-
return 2;
|
|
97
|
+
stopOn(answer.problems);
|
|
95
98
|
}
|
|
96
99
|
process.stdout.write(json ? `${JSON.stringify(answer, null, 2)}\n` : `${blockOf(answer)}\n`);
|
|
97
100
|
return 0;
|
|
98
101
|
}
|
|
99
102
|
export function run(argv) {
|
|
100
103
|
const cli = parseCli(argv);
|
|
101
|
-
if ("error" in cli) {
|
|
102
|
-
process.stderr.write(`dw-sdlc primitives: ${cli.error}\n\n${USAGE}\n`);
|
|
103
|
-
return 2;
|
|
104
|
-
}
|
|
105
104
|
if (cli.help) {
|
|
106
105
|
process.stdout.write(`${USAGE}\n`);
|
|
107
106
|
return 0;
|
|
108
107
|
}
|
|
109
|
-
const root = repositoryRoot(cli.root
|
|
108
|
+
const root = repositoryRoot(rootDir(cli.root));
|
|
110
109
|
if (cli.diff !== undefined) {
|
|
111
110
|
return runDiff(root, cli.diff, cli.json);
|
|
112
111
|
}
|
|
@@ -116,8 +115,7 @@ export function run(argv) {
|
|
|
116
115
|
const problems = read.state === "missing"
|
|
117
116
|
? [`the catalogue does not exist — ${REMEDIES.missing}`]
|
|
118
117
|
: read.problems.map((problem) => `${problem} — ${REMEDIES.invalid}`);
|
|
119
|
-
|
|
120
|
-
return 2;
|
|
118
|
+
stopOn(problems.map((problem) => `${file}: ${problem}`));
|
|
121
119
|
}
|
|
122
120
|
const answers = cli.files.map((path) => {
|
|
123
121
|
const fromRoot = relative(root, realPath(resolve(path)));
|