@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 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 update --check # exit 1 when Dev would write anything different
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 update --check # exit 1 when Dev would write anything different
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.4";
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, api transitionIssue" },
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 asks Dev for what install asked for, unless told otherwise.
302
- const saved = action === "update" ? (state?.request ?? {}) : {};
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 === "update" && flags.check) {
309
- if (!state) throw new CliError("HARNESS_NOT_INSTALLED", "No nodii-dev harness here. Run `nodii-dev harness install` first.", 2);
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
- export async function laneCommand(action, args, flags, { request = devRequest, config = readConfig, gitContext = inferGitContext } = {}) {
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodii/dev",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "Agent CLI for Nodii Dev, reached through the platform edge",
5
5
  "type": "module",
6
6
  "bin": {