@nodii/dev 0.6.4 → 0.6.6
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 +81 -1
- package/dist/cli.js +43 -2
- package/dist/core.js +1 -1
- package/dist/doctor.js +1 -1
- package/dist/harness.js +60 -15
- package/dist/lanes.js +57 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -78,6 +78,8 @@ nodii-dev issues create --service ID --title TEXT [--body TEXT|--body-file FILE]
|
|
|
78
78
|
nodii-dev issues update ISSUE [the same mutable fields]
|
|
79
79
|
nodii-dev issues claim ISSUE [--ttl MINUTES]
|
|
80
80
|
nodii-dev issues release ISSUE [--note TEXT]
|
|
81
|
+
nodii-dev issues transition ISSUE --to todo|in_progress|in_review|blocked|done|wont_do
|
|
82
|
+
[--reason TEXT] [--dev-check REF] [--json]
|
|
81
83
|
|
|
82
84
|
nodii-dev inbox list [--service ID] [--kind drift|request|question]
|
|
83
85
|
nodii-dev inbox show drift|request|question ID
|
|
@@ -162,6 +164,8 @@ nodii-dev lane list [--kind KIND] [--status STATUS] [--machine UUID]
|
|
|
162
164
|
nodii-dev lane show KEY
|
|
163
165
|
nodii-dev lane prioritize|deprioritize KEY --reason TEXT
|
|
164
166
|
nodii-dev lane stop KEY --reason TEXT
|
|
167
|
+
nodii-dev lane non-issue KEY --reason TEXT --evidence TEXT --reroute KEY[,KEY]|none
|
|
168
|
+
--branch carried:ISSUE|dropped[:REASON] [--commits SHA[,SHA]]
|
|
165
169
|
nodii-dev next KEY
|
|
166
170
|
|
|
167
171
|
nodii-dev built list [--cursor CURSOR] [--include-acknowledged true]
|
|
@@ -194,7 +198,33 @@ nodii-dev docs parked [CODE] [--status pending|acknowledged]
|
|
|
194
198
|
|
|
195
199
|
nodii-dev harness install [--include-drafts] [--adopt PATH,...] [--dry-run]
|
|
196
200
|
nodii-dev harness update [--include-drafts] [--adopt PATH,...] [--dry-run]
|
|
197
|
-
nodii-dev harness
|
|
201
|
+
nodii-dev harness check [--json] # 0 current, 1 differs from Dev, 2 no harness
|
|
202
|
+
nodii-dev harness update --check # the same check
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Moving an issue: `issues transition`
|
|
206
|
+
|
|
207
|
+
`issues transition` moves an issue along Dev's status map (MW1-16) with
|
|
208
|
+
Dev's `transitionIssue` operation. It checks MW1-17 before sending anything,
|
|
209
|
+
and refuses with `USAGE` (exit 2) when a move lacks what it needs:
|
|
210
|
+
|
|
211
|
+
| `--to` | Needs |
|
|
212
|
+
|---|---|
|
|
213
|
+
| `blocked` | `--reason` naming the blocking condition and who or what clears it (`cleared by …`, `waiting on …`, `until …`) |
|
|
214
|
+
| `wont_do` | `--reason` saying why |
|
|
215
|
+
| `done` | `--dev-check REF` citing the lane's dev-check evidence (MW3-36); a merge alone never makes an issue done |
|
|
216
|
+
|
|
217
|
+
`--reason` and `--dev-check` are optional for the other moves, and
|
|
218
|
+
`--dev-check` goes only with `done`. Dev keeps one note per transition, so
|
|
219
|
+
the CLI sends the reason, then `Dev check: REF` on its own line, as that
|
|
220
|
+
note. A move Dev refuses (for example `ISSUE_ILLEGAL_TRANSITION`, with the
|
|
221
|
+
allowed set in its details, or `CLAIM_HELD_BY_OTHER`) exits 1 with Dev's
|
|
222
|
+
error code unchanged (MW1-61).
|
|
223
|
+
|
|
224
|
+
```text
|
|
225
|
+
nodii-dev issues transition TKT-412 --to blocked \
|
|
226
|
+
--reason "edge route /tickets 404s on dev; cleared by the nodii-edge lane deploying it"
|
|
227
|
+
nodii-dev issues transition TKT-412 --to done --dev-check "lane:tkt-412 dev probe GET /tickets/412 200"
|
|
198
228
|
```
|
|
199
229
|
|
|
200
230
|
## AD documents
|
|
@@ -255,6 +285,24 @@ source documents moved, and each file created, updated or removed. It
|
|
|
255
285
|
removes only files, blocks and hook entries it wrote, and it refuses (writing
|
|
256
286
|
nothing) when one of its files was edited by hand.
|
|
257
287
|
|
|
288
|
+
`harness check` (MW5-4) compares the repository with what Dev would generate
|
|
289
|
+
now and writes nothing. `harness update --check` is the same check. It asks
|
|
290
|
+
Dev with the options install recorded, and exits:
|
|
291
|
+
|
|
292
|
+
- `0` when every file matches and the stamp names the methodology version Dev
|
|
293
|
+
serves: `Harness is current: REPO, methodology VERSION, N files match Dev.`
|
|
294
|
+
- `1` when anything differs. It prints one line per difference: the
|
|
295
|
+
methodology version, each source document that moved, each file that is
|
|
296
|
+
missing, differs from Dev's or is no longer generated, and each file edited
|
|
297
|
+
by hand since nodii-dev wrote it. `--json` gives the same as `current`,
|
|
298
|
+
`installed`, `available`, `sources`, `changes` and `conflicts`.
|
|
299
|
+
- `2` with `HARNESS_NOT_INSTALLED` when the repository has no harness. The
|
|
300
|
+
message lists the files `harness install` would write.
|
|
301
|
+
|
|
302
|
+
Inside a lane, a failed check is noted in the step, and the harness files
|
|
303
|
+
are left alone; the repository's orchestrator runs `harness update` in a
|
|
304
|
+
`chore` lane (MW5-8).
|
|
305
|
+
|
|
258
306
|
## Preflight: `doctor`
|
|
259
307
|
|
|
260
308
|
Run `nodii-dev doctor` from the lane's worktree, with the lane agent's config,
|
|
@@ -297,6 +345,38 @@ checks. Setup also checks the OAuth login (a token refresh, then the registered
|
|
|
297
345
|
agent) and prints `nodii-dev login` guidance without exposing credential
|
|
298
346
|
details.
|
|
299
347
|
|
|
348
|
+
`lane non-issue` ends a lane at its second terminal status, `non_issue`, when
|
|
349
|
+
its investigation proves the issue is not a defect in its repository (DEC-0011,
|
|
350
|
+
MW1-28.1). The lane's own agent or the machine's coordinator runs it; no staff
|
|
351
|
+
person is needed. It needs:
|
|
352
|
+
|
|
353
|
+
- `--reason`: why this is not a defect here;
|
|
354
|
+
- `--evidence`: what was checked, with `file:line` or data;
|
|
355
|
+
- `--reroute`: the issue keys the real cause was re-routed to, comma-separated,
|
|
356
|
+
or `none` only when the evidence shows no defect anywhere;
|
|
357
|
+
- `--branch`: what happens to the lane's branch work. `carried:ISSUE` carries it
|
|
358
|
+
into that open follow-up issue with its commits; `dropped:REASON` drops it
|
|
359
|
+
with a reason. A bare `dropped` is accepted only when the branch has no
|
|
360
|
+
commits beyond `origin/main`, and records that as the reason. Dev records the
|
|
361
|
+
outcome with the lane's branch.
|
|
362
|
+
- `--commits`: the commits (at most 200). Carried work needs at least one.
|
|
363
|
+
Without the flag the CLI reads the lane's branch commits that are not on
|
|
364
|
+
`origin/main` (as last fetched) from the lane's recorded worktree; it refuses
|
|
365
|
+
with `GIT_REQUIRED` when it cannot read them, and with `USAGE` when carried
|
|
366
|
+
work has none (drop it instead).
|
|
367
|
+
|
|
368
|
+
The CLI refuses with `USAGE` (exit 2) before sending anything when one of these
|
|
369
|
+
is missing. Dev then makes the lane `non_issue`, which frees its slot like
|
|
370
|
+
`on_dev`, and moves each linked open issue to `wont_do` with the reason. An
|
|
371
|
+
issue that another active lane still works on stays open. Dev refuses a lane
|
|
372
|
+
that is already terminal (409 `LANE_TERMINAL`), a caller that is neither the
|
|
373
|
+
lane's agent nor its coordinator (403 `LANE_ACTOR_REQUIRED`), a live lease held
|
|
374
|
+
by someone else (409 `CLAIM_HELD_BY_OTHER`), and a re-route or carried key that
|
|
375
|
+
names no issue, the lane's own issue, or (for carried) a closed one (422).
|
|
376
|
+
The command reaches Dev through the edge, so it needs the edge's route for
|
|
377
|
+
`/devops/api/v1/lanes/:id/non-issue`; until that is on an environment, the
|
|
378
|
+
command gets the edge's 404 there.
|
|
379
|
+
|
|
300
380
|
Heartbeat notes are optional and limited by Dev to 500 characters.
|
|
301
381
|
Prioritize and deprioritize require a 1–500 character reason, which the CLI
|
|
302
382
|
sends to Dev. `lane review` records the review gate.
|
package/dist/cli.js
CHANGED
|
@@ -58,6 +58,8 @@ Usage: nodii-dev <command> [options] [--json]
|
|
|
58
58
|
doctor [--json] # preflight: sign-in, agent, permissions, machine, repo, lane slots
|
|
59
59
|
agent register --name NAME | agent heartbeat
|
|
60
60
|
issues list|show|create|update|claim|release
|
|
61
|
+
issues transition ISSUE --to STATUS [--reason TEXT] [--dev-check REF]
|
|
62
|
+
# blocked and wont_do need --reason; done needs --dev-check
|
|
61
63
|
inbox list|show
|
|
62
64
|
drift file | request file
|
|
63
65
|
decisions list|get|search
|
|
@@ -69,7 +71,8 @@ Usage: nodii-dev <command> [options] [--json]
|
|
|
69
71
|
hook claude-figma-pre|claude-figma-post|print-settings
|
|
70
72
|
setup [--yes|--check] # dry-run by default
|
|
71
73
|
harness install|update [--include-drafts[=false]] [--adopt PATH,...] [--dry-run]
|
|
72
|
-
harness
|
|
74
|
+
harness check # 0 current, 1 differs from Dev, 2 not installed
|
|
75
|
+
harness update --check # the same check
|
|
73
76
|
docs check|validate|upload <path...> --class ruling|conformance|editorial
|
|
74
77
|
[--base CODE=VERSION] [--decision FILE] [--conformance-ref REF]
|
|
75
78
|
[--meaning-unchanged REASON] [--wait]
|
|
@@ -81,6 +84,9 @@ Usage: nodii-dev <command> [options] [--json]
|
|
|
81
84
|
lane step <key> --side SIDE --start|--done|--block <step>
|
|
82
85
|
lane evidence <key> --pr|--commit|--figma|--deploy|--dev-check REF
|
|
83
86
|
lane heartbeat|review|land|verify|prioritize|deprioritize|stop <key>
|
|
87
|
+
lane non-issue <key> --reason TEXT --evidence TEXT --reroute KEY[,KEY]|none
|
|
88
|
+
--branch carried:KEY|dropped[:REASON] [--commits SHA[,SHA]]
|
|
89
|
+
# DEC-0011: terminal non_issue; issue -> wont_do; slot frees
|
|
84
90
|
lane list [--kind KIND] [--status STATUS] [--mine] [--cursor CURSOR]
|
|
85
91
|
lane show <key> | next <key>
|
|
86
92
|
built list | built ack <key...>
|
|
@@ -224,9 +230,44 @@ async function issues(action, args, f, json) {
|
|
|
224
230
|
}),
|
|
225
231
|
json,
|
|
226
232
|
);
|
|
233
|
+
if (action === "transition")
|
|
234
|
+
return emit(
|
|
235
|
+
await devRequest("POST", `/api/v1/issues/${encode(ref)}/transition`, {
|
|
236
|
+
...transitionBody(f),
|
|
237
|
+
onBehalfOf: config.person,
|
|
238
|
+
}),
|
|
239
|
+
json,
|
|
240
|
+
(p) => `${p.data.key} ${p.data.status} — ${p.data.title}`,
|
|
241
|
+
);
|
|
227
242
|
throw new CliError("USAGE", `Unknown issues action: ${action}`, 2);
|
|
228
243
|
}
|
|
229
244
|
|
|
245
|
+
const ISSUE_STATUSES = ["todo", "in_progress", "in_review", "blocked", "done", "wont_do"];
|
|
246
|
+
// A blocked reason names who or what clears it: "cleared by", "unblocks", "waiting on", "until".
|
|
247
|
+
const CLEARED_BY = /\b(clear(s|ed)?|unblock(s|ed)?|wait(s|ing)? (on|for)|until)\b/i;
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* transitionIssue's body ({ to, note }), with MW1-17 checked before anything
|
|
251
|
+
* is sent. Dev stores no separate reason or dev-check field: both go in the note.
|
|
252
|
+
*/
|
|
253
|
+
export function transitionBody(f) {
|
|
254
|
+
const to = requireFlag(f, "to");
|
|
255
|
+
if (!ISSUE_STATUSES.includes(to))
|
|
256
|
+
throw new CliError("USAGE", `--to must be one of ${ISSUE_STATUSES.join(", ")}`, 2);
|
|
257
|
+
const reason = typeof f.reason === "string" ? f.reason.trim() : "";
|
|
258
|
+
const devCheck = typeof f["dev-check"] === "string" ? f["dev-check"].trim() : "";
|
|
259
|
+
if (f["dev-check"] !== undefined && to !== "done")
|
|
260
|
+
throw new CliError("USAGE", "--dev-check goes with --to done", 2);
|
|
261
|
+
if (to === "blocked" && !CLEARED_BY.test(reason))
|
|
262
|
+
throw new CliError("USAGE", 'A move to blocked needs --reason naming the blocking condition and who or what clears it (MW1-17), e.g. --reason "waiting on PR #12 in nodii-edge; cleared by the edge lane merging it"', 2);
|
|
263
|
+
if (to === "wont_do" && !reason)
|
|
264
|
+
throw new CliError("USAGE", "A move to wont_do needs --reason saying why (MW1-17)", 2);
|
|
265
|
+
if (to === "done" && !devCheck)
|
|
266
|
+
throw new CliError("USAGE", "A move to done needs --dev-check REF citing the lane's dev-check evidence (MW1-17, MW3-36). A merge alone never makes an issue done.", 2);
|
|
267
|
+
const note = [reason, devCheck && `Dev check: ${devCheck}`].filter(Boolean).join("\n");
|
|
268
|
+
return { to, ...(note ? { note } : {}) };
|
|
269
|
+
}
|
|
270
|
+
|
|
230
271
|
export async function main(argv = process.argv.slice(2)) {
|
|
231
272
|
if (argv.length === 1 && ["--version", "-v"].includes(argv[0]))
|
|
232
273
|
return console.log(VERSION);
|
|
@@ -282,7 +323,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
282
323
|
if (result.exitCode) process.exitCode = result.exitCode;
|
|
283
324
|
return;
|
|
284
325
|
}
|
|
285
|
-
if (command === "harness" && ["install", "update"].includes(action)) {
|
|
326
|
+
if (command === "harness" && ["install", "update", "check"].includes(action)) {
|
|
286
327
|
// `--include-drafts` is a switch: `--include-drafts false` would leave `false` here and turn drafts on.
|
|
287
328
|
if (rest.length) throw new CliError("USAGE", `harness ${action} takes no arguments, got: ${rest.join(" ")}. Write a flag value as --include-drafts=false.`, 2);
|
|
288
329
|
return emitCommand(await harnessCommand(action, f));
|
package/dist/core.js
CHANGED
|
@@ -4,7 +4,7 @@ import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import { homedir, platform } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
|
|
7
|
-
export const VERSION = "0.6.
|
|
7
|
+
export const VERSION = "0.6.6";
|
|
8
8
|
export const configPath = () =>
|
|
9
9
|
process.env.NODII_DEV_CONFIG_PATH ??
|
|
10
10
|
join(homedir(), ".config/nodii-dev/config.json");
|
package/dist/doctor.js
CHANGED
|
@@ -15,7 +15,7 @@ import { inferGitContext, repoName } from "./lanes.js";
|
|
|
15
15
|
*/
|
|
16
16
|
export const LANE_PERMISSIONS = [
|
|
17
17
|
{ key: "devops.issues.read", for: "issues list|show" },
|
|
18
|
-
{ key: "devops.issues.write", for: "issues create|update|claim|release
|
|
18
|
+
{ key: "devops.issues.write", for: "issues create|update|claim|release|transition" },
|
|
19
19
|
{ key: "devops.lanes.read", for: "lane show|list, next" },
|
|
20
20
|
{ key: "devops.lanes.write", for: "lane start|step|evidence|heartbeat|review|land|verify" },
|
|
21
21
|
{ key: "devops.agents.read", for: "whoami, reading this agent" },
|
package/dist/harness.js
CHANGED
|
@@ -290,33 +290,78 @@ export async function repoContext(cwd = process.cwd()) {
|
|
|
290
290
|
const flagList = (value) => (value === undefined || value === true ? [] : [value].flat().flatMap((x) => String(x).split(",")).filter(Boolean));
|
|
291
291
|
const changeLine = (c) => `${{ create: "+", remove: "-" }[c.action] ?? "~"} ${c.path}${["create", "update", "remove"].includes(c.action) ? "" : ` (${c.action})`}`;
|
|
292
292
|
|
|
293
|
+
// What each planned change means for a check: how the file on disk differs from Dev's.
|
|
294
|
+
const DIFFERENCE = {
|
|
295
|
+
create: "missing; Dev would create it",
|
|
296
|
+
update: "differs from Dev's",
|
|
297
|
+
"add block": "has no nodii-dev block",
|
|
298
|
+
merge: "hooks differ from Dev's",
|
|
299
|
+
remove: "Dev no longer generates it",
|
|
300
|
+
"remove block": "Dev no longer generates its block",
|
|
301
|
+
unmerge: "has hooks Dev no longer generates",
|
|
302
|
+
};
|
|
303
|
+
const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* `harness check` (and its alias `harness update --check`): compare the files
|
|
307
|
+
* on disk, and the methodology stamp, with what Dev would generate now (MW5-4,
|
|
308
|
+
* MW5-6). Writes nothing. Exit 0 when current, 1 when anything differs, 2
|
|
309
|
+
* when there is no harness.
|
|
310
|
+
*/
|
|
311
|
+
async function checkCommand(root, repo, state, fetchHarness) {
|
|
312
|
+
if (!state) {
|
|
313
|
+
let would;
|
|
314
|
+
try {
|
|
315
|
+
const plan = await planHarness(root, (await fetchHarness()).data, { install: true });
|
|
316
|
+
const paths = plan.changes.map((c) => c.path);
|
|
317
|
+
const taken = plan.conflicts.map((c) => c.path);
|
|
318
|
+
would = ` Dev would write ${plural(paths.length, "file")}: ${paths.join(", ")}.${taken.length ? ` ${plural(taken.length, "file")} ${taken.length === 1 ? "already exists and needs" : "already exist and need"} --adopt: ${taken.join(", ")}.` : ""}`;
|
|
319
|
+
} catch (error) {
|
|
320
|
+
would = ` Dev cannot say what install would write (${error?.code ?? "ERROR"}: ${error?.message ?? error}).`;
|
|
321
|
+
}
|
|
322
|
+
throw new CliError("HARNESS_NOT_INSTALLED", `No nodii-dev harness here.${would} Run \`nodii-dev harness install\` first.`, 2);
|
|
323
|
+
}
|
|
324
|
+
// Compare the files Dev would write now, not only the version: the
|
|
325
|
+
// CLAUDE.md block also changes when the service's domain becomes AD.
|
|
326
|
+
const harness = (await fetchHarness()).data;
|
|
327
|
+
const plan = await planHarness(root, harness, { install: false });
|
|
328
|
+
const moved = sourceChanges(state.sources, harness.sources);
|
|
329
|
+
const versionMoved = harness.methodologyVersion !== state.methodologyVersion;
|
|
330
|
+
const behind = versionMoved || moved.length > 0 || plan.changes.length > 0 || plan.conflicts.length > 0;
|
|
331
|
+
const payload = { data: { repo, current: !behind, installed: state.methodologyVersion, available: harness.methodologyVersion, sources: moved, changes: plan.changes.map(({ path, action: a, mode }) => ({ path, action: a, mode })), conflicts: plan.conflicts } };
|
|
332
|
+
const owned = Object.keys(state.files ?? {}).length;
|
|
333
|
+
if (!behind) return { payload, human: `Harness is current: ${repo}, methodology ${state.methodologyVersion}, ${plural(owned, "file")} ${owned === 1 ? "matches" : "match"} Dev.` };
|
|
334
|
+
process.exitCode = 1;
|
|
335
|
+
return {
|
|
336
|
+
payload,
|
|
337
|
+
human: [
|
|
338
|
+
`Harness differs from what Dev would generate for ${repo}:`,
|
|
339
|
+
...(versionMoved ? [` methodology: ${state.methodologyVersion} installed, ${harness.methodologyVersion} available`] : []),
|
|
340
|
+
...moved.map((x) => ` source ${x}`),
|
|
341
|
+
...plan.changes.map((c) => ` ${c.path}: ${DIFFERENCE[c.action] ?? c.action}`),
|
|
342
|
+
...plan.conflicts.map((c) => ` ${c.path}: ${c.reason}`),
|
|
343
|
+
"Fix with `nodii-dev harness update` (inside a lane: note this in the step and leave the harness files alone; MW5-8).",
|
|
344
|
+
].join("\n"),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
293
348
|
export async function harnessCommand(action, flags, { request = devRequest, context = repoContext } = {}) {
|
|
294
|
-
if (!["install", "update"].includes(action)) throw new CliError("USAGE", "harness install|update [--repo NAME] [--service ID] [--include-drafts[=false]] [--adopt PATH,...] [--dry-run] [--check]", 2);
|
|
349
|
+
if (!["install", "update", "check"].includes(action)) throw new CliError("USAGE", "harness install|update|check [--repo NAME] [--service ID] [--include-drafts[=false]] [--adopt PATH,...] [--dry-run] [--check]", 2);
|
|
295
350
|
if (flags.fleet || flags.all) throw new CliError("USAGE", "The harness is installed one service at a time, in that service's repository. There is no fleet mode.", 2);
|
|
296
351
|
if (flags.root === true || flags.repo === true) throw new CliError("USAGE", "--root and --repo need a value.", 2);
|
|
297
352
|
const ctx = flags.root ? { root: flags.root, repo: flags.repo } : await context();
|
|
298
353
|
const repo = flags.repo && flags.repo !== true ? String(flags.repo) : ctx.repo;
|
|
299
354
|
if (!repo) throw new CliError("USAGE", "Pass --repo NAME.", 2);
|
|
300
355
|
const state = await readState(ctx.root);
|
|
301
|
-
// Update
|
|
302
|
-
const saved = action === "
|
|
356
|
+
// Update and check ask Dev for what install asked for, unless told otherwise.
|
|
357
|
+
const saved = action === "install" ? {} : (state?.request ?? {});
|
|
303
358
|
const serviceId = flags.service && flags.service !== true ? String(flags.service) : (saved.serviceId ?? undefined);
|
|
304
359
|
const includeDrafts = flags["include-drafts"] !== undefined ? flags["include-drafts"] !== "false" : Boolean(saved.includeDrafts);
|
|
305
360
|
const params = { repo, serviceId, includeDrafts: includeDrafts ? "true" : undefined };
|
|
306
361
|
|
|
307
362
|
if (action === "install" && flags.check) throw new CliError("USAGE", "--check goes with `harness update`; to preview an install use --dry-run.", 2);
|
|
308
|
-
if (action === "
|
|
309
|
-
|
|
310
|
-
// Compare the files Dev would write now, not only the version: the
|
|
311
|
-
// CLAUDE.md block also changes when the service's domain becomes AD.
|
|
312
|
-
const harness = (await request("GET", `/api/v1/harness${query(params)}`)).data;
|
|
313
|
-
const plan = await planHarness(ctx.root, harness, { install: false });
|
|
314
|
-
const moved = sourceChanges(state.sources, harness.sources);
|
|
315
|
-
const behind = harness.methodologyVersion !== state.methodologyVersion || plan.changes.length > 0 || plan.conflicts.length > 0;
|
|
316
|
-
const payload = { data: { repo, current: !behind, installed: state.methodologyVersion, available: harness.methodologyVersion, sources: moved, changes: plan.changes.map(({ path, action: a, mode }) => ({ path, action: a, mode })), conflicts: plan.conflicts } };
|
|
317
|
-
if (behind) process.exitCode = 1;
|
|
318
|
-
return { payload, human: behind ? [`Harness is behind: ${state.methodologyVersion} → ${harness.methodologyVersion}`, ...moved.map((x) => ` source ${x}`), ...plan.changes.map(changeLine), ...plan.conflicts.map((c) => ` conflict ${c.path}: ${c.reason}`), "Run `nodii-dev harness update`."].join("\n") : `Harness is current (${state.methodologyVersion}).` };
|
|
319
|
-
}
|
|
363
|
+
if (action === "check" && (flags["dry-run"] || flags.adopt)) throw new CliError("USAGE", "harness check writes nothing; --dry-run and --adopt go with install and update.", 2);
|
|
364
|
+
if (action === "check" || (action === "update" && flags.check)) return checkCommand(ctx.root, repo, state, () => request("GET", `/api/v1/harness${query(params)}`));
|
|
320
365
|
|
|
321
366
|
const harness = (await request("GET", `/api/v1/harness${query(params)}`)).data;
|
|
322
367
|
const plan = await planHarness(ctx.root, harness, { install: action === "install", adopt: flagList(flags.adopt), request: { ...(serviceId ? { serviceId } : {}), includeDrafts } });
|
package/dist/lanes.js
CHANGED
|
@@ -48,7 +48,55 @@ function evidence(flags) {
|
|
|
48
48
|
return { kind: flag === "figma" ? "figma_frame" : flag.replace("-", "_"), ...(URL.canParse(value) ? { url: value } : { ref: value }), ...via };
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
const list = (value) => String(value).split(",").map((x) => x.trim()).filter(Boolean);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* DEC-0011 (MW1-28.1): the body for `lane non-issue`. A re-route needs at
|
|
55
|
+
* least one issue key or an explicit `none`, and the branch work is either
|
|
56
|
+
* carried into a named issue with its commits or dropped with a reason, never
|
|
57
|
+
* left unsaid. Without --commits, the commits are the lane's branch commits
|
|
58
|
+
* that are not on origin/main, read from the lane's recorded worktree.
|
|
59
|
+
*/
|
|
60
|
+
export async function nonIssueBody(flags, laneCommits) {
|
|
61
|
+
const reason = requireFlag(flags, "reason");
|
|
62
|
+
const evidence = requireFlag(flags, "evidence");
|
|
63
|
+
const rerouteFlag = requireFlag(flags, "reroute");
|
|
64
|
+
const reroute = rerouteFlag === "none" ? "none" : list(rerouteFlag);
|
|
65
|
+
if (reroute !== "none" && !reroute.length) throw new CliError("USAGE", "--reroute needs at least one issue key, or none when the evidence shows no defect anywhere.", 2);
|
|
66
|
+
const branchFlag = flags.branch;
|
|
67
|
+
const usage = "--branch must say what happens to the lane's branch work: carried:ISSUE_KEY or dropped[:REASON].";
|
|
68
|
+
if (typeof branchFlag !== "string" || !branchFlag) throw new CliError("USAGE", usage, 2);
|
|
69
|
+
const at = branchFlag.indexOf(":");
|
|
70
|
+
const disposition = at === -1 ? branchFlag : branchFlag.slice(0, at);
|
|
71
|
+
const detail = at === -1 ? "" : branchFlag.slice(at + 1).trim();
|
|
72
|
+
if (!(disposition === "carried" && detail) && disposition !== "dropped") throw new CliError("USAGE", usage, 2);
|
|
73
|
+
const given = flags.commits !== undefined ? list(requireFlag(flags, "commits")) : undefined;
|
|
74
|
+
// dropped:REASON states its own reason, so it needs no commit lookup.
|
|
75
|
+
const commits = given ?? (disposition === "dropped" && detail ? undefined : await laneCommits());
|
|
76
|
+
if (commits && commits.length > MAX_COMMITS) throw new CliError("USAGE", `That is ${commits.length} commits; Dev records at most ${MAX_COMMITS}. Pass --commits with the ones that matter, or --branch dropped:REASON.`, 2);
|
|
77
|
+
let branch;
|
|
78
|
+
if (disposition === "carried") {
|
|
79
|
+
if (!commits.length) throw new CliError("USAGE", "Carried work needs its commits: the lane's branch has none beyond origin/main. Pass --commits SHA[,SHA], or --branch dropped:REASON.", 2);
|
|
80
|
+
branch = { disposition, issueKey: detail, commits };
|
|
81
|
+
} else if (detail) branch = { disposition, reason: detail, ...(commits ? { commits } : {}) };
|
|
82
|
+
else if (!commits.length) branch = { disposition, reason: "no commits beyond origin/main" };
|
|
83
|
+
else throw new CliError("USAGE", `The lane's branch has ${commits.length} commits beyond origin/main. Say why they are dropped: --branch dropped:REASON.`, 2);
|
|
84
|
+
return { reason, evidence, reroute, branch, ...via };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const MAX_COMMITS = 200;
|
|
88
|
+
|
|
89
|
+
/** The lane's branch commits not on origin/main (as last fetched), from its recorded worktree. */
|
|
90
|
+
export async function branchCommits(lane) {
|
|
91
|
+
try {
|
|
92
|
+
const out = (await execFileAsync("git", ["-C", lane.worktree, "rev-list", `origin/main..${lane.branch}`], { encoding: "utf8" })).stdout.trim();
|
|
93
|
+
return out ? out.split("\n") : [];
|
|
94
|
+
} catch {
|
|
95
|
+
throw new CliError("GIT_REQUIRED", `Cannot read the commits of ${lane.branch} in ${lane.worktree}. Pass --commits SHA[,SHA], or --branch dropped:REASON.`, 2);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function laneCommand(action, args, flags, { request = devRequest, config = readConfig, gitContext = inferGitContext, commitsOf = branchCommits } = {}) {
|
|
52
100
|
if (action === "list") {
|
|
53
101
|
const c = flags.mine ? await config() : null;
|
|
54
102
|
const payload = await request("GET", `/api/v1/lanes${query({ kind: flags.kind, status: flags.status, machine: flags.machine, agent: flags.mine ? c.agentId : flags.agent, "waiting-on": flags["waiting-on"], stuck: flags.stuck, cursor: flags.cursor, limit: flags.limit })}`);
|
|
@@ -108,6 +156,14 @@ export async function laneCommand(action, args, flags, { request = devRequest, c
|
|
|
108
156
|
return { payload, human: `${ref}: ${action}d.` };
|
|
109
157
|
}
|
|
110
158
|
if (action === "stop") return { payload: await request("POST", `/api/v1/lanes/${encode(id)}/stop`, { reason: requireFlag(flags, "reason"), ...via }), human: `${ref}: stopped.` };
|
|
159
|
+
if (action === "non-issue") {
|
|
160
|
+
const body = await nonIssueBody(flags, async () => commitsOf((await request("GET", `/api/v1/lanes/${encode(id)}`)).data));
|
|
161
|
+
const payload = await request("POST", `/api/v1/lanes/${encode(id)}/non-issue`, body);
|
|
162
|
+
const { closedIssues = [], openIssues = [] } = payload?.data?.nonIssue ?? {};
|
|
163
|
+
const closed = closedIssues.length ? ` ${closedIssues.join(", ")} moved to wont_do.` : "";
|
|
164
|
+
const open = openIssues.length ? ` ${openIssues.join(", ")} left open: another active lane works on it.` : "";
|
|
165
|
+
return { payload, human: `${ref}: non_issue; slot freed.${closed}${open}` };
|
|
166
|
+
}
|
|
111
167
|
throw new CliError("USAGE", `Unknown lane action: ${action}`, 2);
|
|
112
168
|
}
|
|
113
169
|
|