dw-sdlc 0.12.1 → 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 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. It posts
72
- nothing and exits 2 once a push moved the head, so a new head needs a fresh review. Under `off` the
73
- script posts that status itself, its description saying why, and goes on.
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 clean review pinned to the head it read, as a commit status |
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]
@@ -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 second signal, or one
31
- sent while the Checks the package ships run, ends the Gate at once.
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 clean review to the head it read, as a commit status";
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 clean review to the head it read, as the commit status
2
- // `dw-sdlc/review` in state success, so a push after the review leaves the new head unreviewed.
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
- export const summary = "pin a clean review to the head it read, as a commit status";
7
- const USAGE = `Usage: dw-sdlc pr reviewed <n> [-R owner/name] --head <sha> [--json]
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. When
11
- a push moved the head since the review, it posts nothing. One line, or one document with --json.
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
- const DESCRIPTION = "a clean review of this head";
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
- return { json: values.json === true, number: Number(number), repo, head };
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, DESCRIPTION));
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: REVIEW_STATE }, null, 2)
89
- : `posted ${REVIEW} ${REVIEW_STATE} on ${head}`;
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
  }
@@ -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 reviewed commit under details.since, where the Review reads from and auto routes from. It
27
- carries that commit's success over a head whose only commits since are merges of the base, unless
28
- --review names a depth for the call.
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(read(cli, "pr ship", sized));
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 declared = resolveConfig(repositoryRoot(process.cwd())).config.pr;
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) {
@@ -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,23 +115,6 @@ function liveWorkers() {
119
115
  function workersText(workers) {
120
116
  return workers.map((worker) => `${worker.name} (${worker.status})`).join(", ");
121
117
  }
122
- /**
123
- * The remote branch name `branch` tracks, when it differs from `branch`. Read from the config, so it holds
124
- * after the remote deleted the branch.
125
- */
126
- function upstreamName(root, branch) {
127
- const remote = asked(root, ["config", "--get", `branch.${branch}.remote`]);
128
- const merge = asked(root, ["config", "--get", `branch.${branch}.merge`]);
129
- if (remote === undefined || remote === "." || merge === undefined || !merge.startsWith("refs/heads/")) {
130
- return undefined;
131
- }
132
- const name = merge.slice("refs/heads/".length);
133
- return name === branch ? undefined : name;
134
- }
135
- /** Whether `pr`'s commits hold `head`, so the head is on GitHub whatever became of the upstream. */
136
- function holds(pr, head) {
137
- return head !== null && pr.commits.includes(head);
138
- }
139
118
  /**
140
119
  * The current branch's pull request and the Action the Ship script's decision gives it, or null. The one
141
120
  * found by the upstream's name counts only when it holds the head: a branch started from another tracks
@@ -0,0 +1,2 @@
1
+ export declare const summary = "removes Landed branches and clean worktrees whose work has landed, names what it keeps";
2
+ export declare function run(argv: string[]): number;