dw-sdlc 0.12.0 → 0.13.0
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 +25 -16
- package/dist/cli.js +2 -0
- package/dist/commands/gate.js +3 -2
- package/dist/commands/pr-reviewed.d.ts +1 -1
- package/dist/commands/pr-reviewed.js +32 -13
- package/dist/commands/pr-ship.js +35 -8
- package/dist/commands/recap.js +15 -12
- package/dist/commands/sweep.d.ts +2 -0
- package/dist/commands/sweep.js +403 -0
- package/dist/landed.d.ts +7 -0
- package/dist/landed.js +86 -0
- package/dist/pr/decide.d.ts +9 -5
- package/dist/pr/decide.js +36 -9
- package/dist/pr/read.d.ts +10 -2
- package/dist/pr/read.js +32 -13
- package/dist/pr/state.d.ts +32 -6
- package/dist/pr/state.js +57 -16
- package/dist/pr/upstream.d.ts +16 -0
- package/dist/pr/upstream.js +31 -0
- package/dist/run-command.d.ts +1 -1
- package/dist/run-command.js +20 -4
- package/dist/working-tree.d.ts +2 -0
- package/dist/working-tree.js +5 -0
- package/dist/worktree-lock.d.ts +15 -0
- package/dist/worktree-lock.js +63 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,25 +68,34 @@ the `gh` account did not open is always the dev's to merge.
|
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
`pr ship` answers `review`, its depth under `details`, until the head carries the commit status
|
|
71
|
-
`dw-sdlc/review`, which `dw-sdlc pr reviewed <n> --head <sha>` posts after a clean review.
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
`dw-sdlc/review`, which `dw-sdlc pr reviewed <n> --head <sha>` posts after a clean review. A Review
|
|
72
|
+
that leaves a Finding open posts it with `--findings`, in state failure, "read, findings open": the
|
|
73
|
+
read mark. After the next push, `details.since` names the last head a Review read, clean or marked,
|
|
74
|
+
so the Review after a fix reads the fix alone. `pr reviewed` posts nothing and exits 2 once a push
|
|
75
|
+
moved the head, so a new head needs a fresh review. Under `off` the script posts that status
|
|
76
|
+
itself, its description saying why, and goes on.
|
|
77
|
+
|
|
78
|
+
Three Reviews with no success between them end the loop: `pr ship` answers `park review-limit`
|
|
79
|
+
instead of `review`, whose command, `dw-sdlc pr reviewed <n> -R <owner/name> --head <sha> --accept`,
|
|
80
|
+
is the dev's. `--accept` posts the success described "accepted by the dev, findings open"; with
|
|
81
|
+
`--findings` it exits 2. The limit is a constant, not a config key, and `--review <depth>` on a call
|
|
82
|
+
buys one more Review past it.
|
|
74
83
|
|
|
75
84
|
## Commands
|
|
76
85
|
|
|
77
|
-
| command | what it does
|
|
78
|
-
| ------------------ |
|
|
79
|
-
| `gate` | the Steps of `gate.steps` in order, then every Check under `gate.checks`
|
|
80
|
-
| `check <name>` | one Check of the Gate by its name, without the Steps and the other Checks
|
|
81
|
-
| `primitives` | the Primitives that own each path, with their summaries and documents
|
|
82
|
-
| `setup` | this machine's local config layer, the git excludes and the permission rules
|
|
83
|
-
| `doctor` | one PASS / WARN / FAIL line per machine check the guardrails rely on
|
|
84
|
-
| `config` | the resolved agentic config, or one file validated against the schema
|
|
85
|
-
| `references sync` | every declared Reference brought to its ref under `references/`
|
|
86
|
-
| `references setup` | the Router, its table and the ignore rules a repository needs to declare References
|
|
87
|
-
| `pr ship [<n>]` | the Action one pull request needs next, performing the script's own
|
|
88
|
-
| `pr reviewed <n>` | a
|
|
89
|
-
| `recap` | the facts a Recap rests on and whether the session can be closed
|
|
86
|
+
| command | what it does |
|
|
87
|
+
| ------------------ | ------------------------------------------------------------------------------------------ |
|
|
88
|
+
| `gate` | the Steps of `gate.steps` in order, then every Check under `gate.checks` |
|
|
89
|
+
| `check <name>` | one Check of the Gate by its name, without the Steps and the other Checks |
|
|
90
|
+
| `primitives` | the Primitives that own each path, with their summaries and documents |
|
|
91
|
+
| `setup` | this machine's local config layer, the git excludes and the permission rules |
|
|
92
|
+
| `doctor` | one PASS / WARN / FAIL line per machine check the guardrails rely on |
|
|
93
|
+
| `config` | the resolved agentic config, or one file validated against the schema |
|
|
94
|
+
| `references sync` | every declared Reference brought to its ref under `references/` |
|
|
95
|
+
| `references setup` | the Router, its table and the ignore rules a repository needs to declare References |
|
|
96
|
+
| `pr ship [<n>]` | the Action one pull request needs next, performing the script's own |
|
|
97
|
+
| `pr reviewed <n>` | a Review pinned to the head it read, as a commit status: clean, Findings open, or accepted |
|
|
98
|
+
| `recap` | the facts a Recap rests on and whether the session can be closed |
|
|
90
99
|
|
|
91
100
|
`dw-sdlc <command> --help` gives each one's options.
|
|
92
101
|
|
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import * as recap from "./commands/recap.js";
|
|
|
12
12
|
import * as referencesSetup from "./commands/references-setup.js";
|
|
13
13
|
import * as referencesSync from "./commands/references-sync.js";
|
|
14
14
|
import * as setup from "./commands/setup.js";
|
|
15
|
+
import * as sweep from "./commands/sweep.js";
|
|
15
16
|
import * as workerLs from "./commands/worker-ls.js";
|
|
16
17
|
import * as workerReport from "./commands/worker-report.js";
|
|
17
18
|
import * as workerStart from "./commands/worker-start.js";
|
|
@@ -27,6 +28,7 @@ const COMMANDS = new Map([
|
|
|
27
28
|
["pr ship", prShip],
|
|
28
29
|
["pr reviewed", prReviewed],
|
|
29
30
|
["recap", recap],
|
|
31
|
+
["sweep", sweep],
|
|
30
32
|
["worker start", workerStart],
|
|
31
33
|
["worker ls", workerLs],
|
|
32
34
|
["worker report", workerReport]
|
package/dist/commands/gate.js
CHANGED
|
@@ -27,8 +27,9 @@ and edits made before the run never drift, and a Step a signal stopped is not co
|
|
|
27
27
|
"allow" turns the watch off.
|
|
28
28
|
|
|
29
29
|
SIGINT or SIGTERM sent while a command runs stops it and everything after it, and the report still
|
|
30
|
-
prints: the stopped command's last lines, its failure line and the last line. A
|
|
31
|
-
|
|
30
|
+
prints: the stopped command's last lines, its failure line and the last line. A command that SIGINT
|
|
31
|
+
or SIGTERM ends stops everything after it the same way, as Ctrl-C reaches it and the Gate together. A
|
|
32
|
+
second signal, or one sent while the Checks the package ships run, ends the Gate at once.
|
|
32
33
|
|
|
33
34
|
--verbose passes each command's output through as it runs instead, under a line naming the command,
|
|
34
35
|
and keeps no tail. --json prints the document alone; each Step and Check in it carries tail, the last
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const summary = "pin a
|
|
1
|
+
export declare const summary = "pin a Review to the head it read, as a commit status";
|
|
2
2
|
export declare function run(argv: string[]): number;
|
|
@@ -1,18 +1,29 @@
|
|
|
1
|
-
// `dw-sdlc pr reviewed <n> --head <sha>`: pins a
|
|
2
|
-
// `dw-sdlc/review
|
|
1
|
+
// `dw-sdlc pr reviewed <n> --head <sha>`: pins a Review to the head it read, as the commit status
|
|
2
|
+
// `dw-sdlc/review`: success for a clean one, --findings the read mark of one that left a Finding open,
|
|
3
|
+
// --accept the dev's acceptance of a head as it stands, which the review-limit park names.
|
|
3
4
|
import { parseArgs } from "node:util";
|
|
4
5
|
import { REVIEW, REVIEW_STATE, reviewStatusArgs } from "../pr/decide.js";
|
|
5
6
|
import { CannotRun, gh, namedProblem, read, said } from "../pr/read.js";
|
|
6
|
-
|
|
7
|
-
const
|
|
7
|
+
import { ACCEPTED, MARK_STATE, READ_MARK } from "../pr/state.js";
|
|
8
|
+
export const summary = "pin a Review to the head it read, as a commit status";
|
|
9
|
+
const USAGE = `Usage: dw-sdlc pr reviewed <n> [-R owner/name] --head <sha> [--findings | --accept] [--json]
|
|
8
10
|
|
|
9
11
|
Reads pull request <n> through gh, the read pr ship makes, and when its head is still <sha> posts
|
|
10
|
-
the commit status dw-sdlc/review in state success on it, the pull request's URL as its target
|
|
11
|
-
|
|
12
|
+
the commit status dw-sdlc/review in state success on it, the pull request's URL as its target: a
|
|
13
|
+
clean Review of that head. --findings posts it in state failure, described "${READ_MARK}": the
|
|
14
|
+
read mark of a Review that left a Finding open, from which pr ship reads the fix that follows.
|
|
15
|
+
--accept posts it in state success, described "${ACCEPTED}": the dev takes the head as it stands, the
|
|
16
|
+
command the park review-limit names; only the dev runs it. --findings and --accept together exit 2.
|
|
17
|
+
When a push moved the head since the review, it posts nothing. One line, or one document with --json.
|
|
12
18
|
Needs an authenticated gh.
|
|
13
19
|
|
|
14
20
|
Exit 0 posted, 2 when it cannot run or the head moved.`;
|
|
15
|
-
|
|
21
|
+
/** What the status says: a clean Review, the read mark of one that left a Finding open, or the dev's acceptance. */
|
|
22
|
+
const STATUS = {
|
|
23
|
+
clean: { state: REVIEW_STATE, description: "a clean review of this head" },
|
|
24
|
+
findings: { state: MARK_STATE, description: READ_MARK },
|
|
25
|
+
accept: { state: REVIEW_STATE, description: ACCEPTED }
|
|
26
|
+
};
|
|
16
27
|
function parseCli(argv) {
|
|
17
28
|
try {
|
|
18
29
|
const { values, positionals } = parseArgs({
|
|
@@ -22,7 +33,9 @@ function parseCli(argv) {
|
|
|
22
33
|
help: { type: "boolean", short: "h" },
|
|
23
34
|
json: { type: "boolean" },
|
|
24
35
|
repo: { type: "string", short: "R" },
|
|
25
|
-
head: { type: "string" }
|
|
36
|
+
head: { type: "string" },
|
|
37
|
+
findings: { type: "boolean" },
|
|
38
|
+
accept: { type: "boolean" }
|
|
26
39
|
}
|
|
27
40
|
});
|
|
28
41
|
if (values.help === true) {
|
|
@@ -46,19 +59,24 @@ function parseCli(argv) {
|
|
|
46
59
|
if (!/^[0-9a-f]{40}$/.test(head)) {
|
|
47
60
|
return { error: `--head takes the full 40-character sha, not ${JSON.stringify(head)}` };
|
|
48
61
|
}
|
|
49
|
-
|
|
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 };
|
|
50
68
|
}
|
|
51
69
|
catch (error) {
|
|
52
70
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
53
71
|
}
|
|
54
72
|
}
|
|
55
73
|
/** Posts the review status on `head` once the read shows it is still the pull request's head. */
|
|
56
|
-
function post({ number, repo, head }) {
|
|
74
|
+
function post({ number, repo, head, status }) {
|
|
57
75
|
const pr = read({ number, repo }, "pr reviewed", false);
|
|
58
76
|
if (pr.head !== head) {
|
|
59
77
|
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");
|
|
60
78
|
}
|
|
61
|
-
const posted = gh(reviewStatusArgs(pr,
|
|
79
|
+
const posted = gh(reviewStatusArgs(pr, status.description, status.state));
|
|
62
80
|
if (posted.status !== 0) {
|
|
63
81
|
throw new CannotRun(`${pr.repo}#${number}: posting ${REVIEW} failed: ${said(posted.stderr)}`, `check that gh's token may write commit statuses to ${pr.repo}, then call again`);
|
|
64
82
|
}
|
|
@@ -84,9 +102,10 @@ export function run(argv) {
|
|
|
84
102
|
throw error;
|
|
85
103
|
}
|
|
86
104
|
const { head } = cli;
|
|
105
|
+
const { state } = cli.status;
|
|
87
106
|
const answer = cli.json
|
|
88
|
-
? JSON.stringify({ head, context: REVIEW, state
|
|
89
|
-
: `posted ${REVIEW} ${
|
|
107
|
+
? JSON.stringify({ head, context: REVIEW, state }, null, 2)
|
|
108
|
+
: `posted ${REVIEW} ${state} on ${head}`;
|
|
90
109
|
process.stdout.write(`${answer}\n`);
|
|
91
110
|
return 0;
|
|
92
111
|
}
|
package/dist/commands/pr-ship.js
CHANGED
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
import { repositoryRoot, resolveConfig, REVIEW_DEPTHS } from "../agentic-config.js";
|
|
6
6
|
import { decide, declaredPolicy, devCommand, hostArgs, isScriptAction, lineOf, readyToMerge, REVIEW, shellLine } from "../pr/decide.js";
|
|
7
|
-
import { CannotRun, carriesBase, gh, namedProblem, now, read, readAgain, said, withBehind } from "../pr/read.js";
|
|
7
|
+
import { CannotRun, carriesBase, gh, namedProblem, nameItRemedy, now, read, readAgain, readBranch, said, withBehind } from "../pr/read.js";
|
|
8
|
+
import { holds, renamedUpstream } from "../pr/upstream.js";
|
|
8
9
|
export const summary = "decide the Action one pull request needs next, as a line or JSON";
|
|
9
10
|
const USAGE = `Usage: dw-sdlc pr ship [<n>] [-R owner/name] [--merge] [--review full|light|off] [--dry-run] [--json] [--verbose]
|
|
10
11
|
|
|
11
12
|
Reads pull request <n> through gh, the one of the current branch as gh pr view resolves it when <n>
|
|
12
13
|
is omitted, and answers the Action it needs next: the status, the action, whose turn it is and why.
|
|
14
|
+
A branch pushed under another name has an upstream of that name. Where gh resolves no pull request
|
|
15
|
+
for it, the script asks for the upstream's, and reads it only when its commits hold HEAD.
|
|
13
16
|
The script performs its own Actions, each at most once and then reads again: it updates a branch
|
|
14
17
|
behind its base, marks a draft ready once nothing else holds it back, and squash merges a pull
|
|
15
18
|
request ready to merge under pr.merge auto in .ai/agentic.config.json or with --merge for this call;
|
|
@@ -23,9 +26,10 @@ why. Under auto, the default, the script routes the depth from the changes the R
|
|
|
23
26
|
lockfiles alone off; a file under .github/workflows/, more than 100 changed lines outside
|
|
24
27
|
lockfiles, or a change it cannot size, more files than GitHub's compare lists or a file outside
|
|
25
28
|
lockfiles with no patch, full; the rest light. After a push it names
|
|
26
|
-
the last
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
the last commit a Review read under details.since, where the Review reads from and auto routes from:
|
|
30
|
+
one with a ${REVIEW} success not posted under off, or the read mark pr reviewed --findings
|
|
31
|
+
posts. It carries a success over a head whose only commits since are merges of the base, unless
|
|
32
|
+
--review names a depth for the call; a read mark never carries.
|
|
29
33
|
Each Action the call runs is listed under performed. Its ok says whether the read after it shows the
|
|
30
34
|
Action took effect, whatever gh's exit status; gh's exit status and stderr stand beside it, so an ok
|
|
31
35
|
false with exit 0 is an Action gh accepted that the read does not show yet. An update counts when
|
|
@@ -138,14 +142,36 @@ function handedOver(pr, policy, answer, action, reason) {
|
|
|
138
142
|
}
|
|
139
143
|
return { ...answer, status: "park", actor: "dev", reason, command: devCommand(pr, action, policy) };
|
|
140
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* The pull request the call names, else the current branch's as `gh pr view` resolves it. Where gh finds
|
|
147
|
+
* none for a branch whose upstream has another name, the one of that name, taken only when its commits
|
|
148
|
+
* hold the local head: a branch started from another tracks that branch until its first push.
|
|
149
|
+
*/
|
|
150
|
+
function readTarget(cli, root, sized) {
|
|
151
|
+
const renamed = cli.number === undefined && cli.repo === undefined ? renamedUpstream(root) : undefined;
|
|
152
|
+
if (renamed === undefined) {
|
|
153
|
+
return read(cli, "pr ship", sized);
|
|
154
|
+
}
|
|
155
|
+
const { branch, head, upstream } = renamed;
|
|
156
|
+
const found = readBranch(sized, upstream);
|
|
157
|
+
const upstreamNamed = `${upstream}, the name its upstream gives`;
|
|
158
|
+
if (found === null) {
|
|
159
|
+
throw new CannotRun(`gh pr view found no pull request for ${branch}, nor for ${upstreamNamed}`, nameItRemedy("pr ship"));
|
|
160
|
+
}
|
|
161
|
+
if (found.byUpstream && !holds(found.pr, head)) {
|
|
162
|
+
const lacks = `${found.pr.url}, found by ${upstreamNamed}, lacks HEAD ${head.slice(0, 7)}`;
|
|
163
|
+
throw new CannotRun(lacks, `push HEAD if it is this branch's pull request, else ${nameItRemedy("pr ship")}`);
|
|
164
|
+
}
|
|
165
|
+
return found.pr;
|
|
166
|
+
}
|
|
141
167
|
/**
|
|
142
168
|
* Reads and decides until the Action is not the script's. Each script Action runs at most once, then the
|
|
143
169
|
* pull request is read again; under --dry-run, or when an Action comes round again, it is the dev's.
|
|
144
170
|
*/
|
|
145
|
-
function ship(cli, policy) {
|
|
171
|
+
function ship(cli, root, policy) {
|
|
146
172
|
// Only pr.review auto routes the depth from the changes, so only it reads them.
|
|
147
173
|
const sized = policy.review.depth === "auto";
|
|
148
|
-
let pr = withBehind(
|
|
174
|
+
let pr = withBehind(readTarget(cli, root, sized));
|
|
149
175
|
const performed = [];
|
|
150
176
|
// The head each Action ran on.
|
|
151
177
|
const postedOn = new Map();
|
|
@@ -206,7 +232,8 @@ export function run(argv) {
|
|
|
206
232
|
return 0;
|
|
207
233
|
}
|
|
208
234
|
// An invalid config layer resolves to ask and full, like every other strictest setting.
|
|
209
|
-
const
|
|
235
|
+
const root = repositoryRoot(process.cwd());
|
|
236
|
+
const declared = resolveConfig(root).config.pr;
|
|
210
237
|
const policy = {
|
|
211
238
|
...declaredPolicy(declared),
|
|
212
239
|
...(cli.merge ? { merge: "auto" } : {}),
|
|
@@ -214,7 +241,7 @@ export function run(argv) {
|
|
|
214
241
|
};
|
|
215
242
|
let shipped;
|
|
216
243
|
try {
|
|
217
|
-
shipped = ship(cli, policy);
|
|
244
|
+
shipped = ship(cli, root, policy);
|
|
218
245
|
}
|
|
219
246
|
catch (error) {
|
|
220
247
|
if (error instanceof CannotRun) {
|
package/dist/commands/recap.js
CHANGED
|
@@ -6,8 +6,9 @@ import { parseArgs } from "node:util";
|
|
|
6
6
|
import { findRepository, mainCheckout, resolveConfig } from "../agentic-config.js";
|
|
7
7
|
import { decide, declaredPolicy, labelOf, lineOf } from "../pr/decide.js";
|
|
8
8
|
import { CannotRun, defaultBranch, now, readBranch } from "../pr/read.js";
|
|
9
|
+
import { holds, upstreamName } from "../pr/upstream.js";
|
|
9
10
|
import { isLive, Refusal, workersOf, workingFolders } from "../worker/herdr.js";
|
|
10
|
-
import { git } from "../working-tree.js";
|
|
11
|
+
import { asked, git } from "../working-tree.js";
|
|
11
12
|
export const summary = "the facts a Recap rests on and whether the session can be closed";
|
|
12
13
|
const USAGE = `Usage: dw-sdlc recap [--json]
|
|
13
14
|
|
|
@@ -49,11 +50,6 @@ function must(root, args) {
|
|
|
49
50
|
}
|
|
50
51
|
return result.out;
|
|
51
52
|
}
|
|
52
|
-
/** `git <args>` at `root`, or undefined when it exits non-zero as a question answered no. */
|
|
53
|
-
function asked(root, args) {
|
|
54
|
-
const result = git(root, args);
|
|
55
|
-
return "out" in result ? result.out : undefined;
|
|
56
|
-
}
|
|
57
53
|
function count(root, args) {
|
|
58
54
|
return Number(must(root, ["rev-list", "--count", ...args]));
|
|
59
55
|
}
|
|
@@ -119,12 +115,19 @@ function liveWorkers() {
|
|
|
119
115
|
function workersText(workers) {
|
|
120
116
|
return workers.map((worker) => `${worker.name} (${worker.status})`).join(", ");
|
|
121
117
|
}
|
|
122
|
-
/**
|
|
123
|
-
|
|
118
|
+
/**
|
|
119
|
+
* The current branch's pull request and the Action the Ship script's decision gives it, or null. The one
|
|
120
|
+
* found by the upstream's name counts only when it holds the head: a branch started from another tracks
|
|
121
|
+
* that branch until its first push.
|
|
122
|
+
*/
|
|
123
|
+
function readShipped(root, branch, head) {
|
|
124
124
|
// An invalid config layer resolves to ask and full, as in the Ship script.
|
|
125
125
|
const policy = declaredPolicy(resolveConfig(root).config.pr);
|
|
126
|
-
const
|
|
127
|
-
|
|
126
|
+
const found = readBranch(policy.review.depth === "auto", head === null ? undefined : upstreamName(root, branch));
|
|
127
|
+
if (found === null || (found.byUpstream && !holds(found.pr, head))) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return { pr: found.pr, answer: decide(found.pr, now(), policy) };
|
|
128
131
|
}
|
|
129
132
|
/**
|
|
130
133
|
* The tracking refs, as `--remotes` names them, that the remote contradicts, which only a fetch would show:
|
|
@@ -302,8 +305,8 @@ function report(json) {
|
|
|
302
305
|
const { branch, head } = read;
|
|
303
306
|
// A detached HEAD names no branch for gh to look up, and a repository with no remote has no GitHub.
|
|
304
307
|
const asksGh = branch !== null && remotesOf(root).length > 0;
|
|
305
|
-
const shipped = asksGh ? readShipped(root) : null;
|
|
306
|
-
const held = shipped !== null &&
|
|
308
|
+
const shipped = asksGh ? readShipped(root, branch, head) : null;
|
|
309
|
+
const held = shipped !== null && holds(shipped.pr, head);
|
|
307
310
|
const local = standing(root, read, shipped, held);
|
|
308
311
|
// An open pull request can still take the head, and a commit-less repository has nothing to merge.
|
|
309
312
|
const nothingToMerge = held || shipped?.pr.state === "OPEN" || head === null;
|