@nodii/dev 0.6.4 → 0.6.5

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
@@ -194,7 +196,33 @@ nodii-dev docs parked [CODE] [--status pending|acknowledged]
194
196
 
195
197
  nodii-dev harness install [--include-drafts] [--adopt PATH,...] [--dry-run]
196
198
  nodii-dev harness update [--include-drafts] [--adopt PATH,...] [--dry-run]
197
- nodii-dev harness update --check # exit 1 when Dev would write anything different
199
+ nodii-dev harness check [--json] # 0 current, 1 differs from Dev, 2 no harness
200
+ nodii-dev harness update --check # the same check
201
+ ```
202
+
203
+ ### Moving an issue: `issues transition`
204
+
205
+ `issues transition` moves an issue along Dev's status map (MW1-16) with
206
+ Dev's `transitionIssue` operation. It checks MW1-17 before sending anything,
207
+ and refuses with `USAGE` (exit 2) when a move lacks what it needs:
208
+
209
+ | `--to` | Needs |
210
+ |---|---|
211
+ | `blocked` | `--reason` naming the blocking condition and who or what clears it (`cleared by …`, `waiting on …`, `until …`) |
212
+ | `wont_do` | `--reason` saying why |
213
+ | `done` | `--dev-check REF` citing the lane's dev-check evidence (MW3-36); a merge alone never makes an issue done |
214
+
215
+ `--reason` and `--dev-check` are optional for the other moves, and
216
+ `--dev-check` goes only with `done`. Dev keeps one note per transition, so
217
+ the CLI sends the reason, then `Dev check: REF` on its own line, as that
218
+ note. A move Dev refuses (for example `ISSUE_ILLEGAL_TRANSITION`, with the
219
+ allowed set in its details, or `CLAIM_HELD_BY_OTHER`) exits 1 with Dev's
220
+ error code unchanged (MW1-61).
221
+
222
+ ```text
223
+ nodii-dev issues transition TKT-412 --to blocked \
224
+ --reason "edge route /tickets 404s on dev; cleared by the nodii-edge lane deploying it"
225
+ nodii-dev issues transition TKT-412 --to done --dev-check "lane:tkt-412 dev probe GET /tickets/412 200"
198
226
  ```
199
227
 
200
228
  ## AD documents
@@ -255,6 +283,24 @@ source documents moved, and each file created, updated or removed. It
255
283
  removes only files, blocks and hook entries it wrote, and it refuses (writing
256
284
  nothing) when one of its files was edited by hand.
257
285
 
286
+ `harness check` (MW5-4) compares the repository with what Dev would generate
287
+ now and writes nothing. `harness update --check` is the same check. It asks
288
+ Dev with the options install recorded, and exits:
289
+
290
+ - `0` when every file matches and the stamp names the methodology version Dev
291
+ serves: `Harness is current: REPO, methodology VERSION, N files match Dev.`
292
+ - `1` when anything differs. It prints one line per difference: the
293
+ methodology version, each source document that moved, each file that is
294
+ missing, differs from Dev's or is no longer generated, and each file edited
295
+ by hand since nodii-dev wrote it. `--json` gives the same as `current`,
296
+ `installed`, `available`, `sources`, `changes` and `conflicts`.
297
+ - `2` with `HARNESS_NOT_INSTALLED` when the repository has no harness. The
298
+ message lists the files `harness install` would write.
299
+
300
+ Inside a lane, a failed check is noted in the step, and the harness files
301
+ are left alone; the repository's orchestrator runs `harness update` in a
302
+ `chore` lane (MW5-8).
303
+
258
304
  ## Preflight: `doctor`
259
305
 
260
306
  Run `nodii-dev doctor` from the lane's worktree, with the lane agent's config,
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]
@@ -224,9 +227,44 @@ async function issues(action, args, f, json) {
224
227
  }),
225
228
  json,
226
229
  );
230
+ if (action === "transition")
231
+ return emit(
232
+ await devRequest("POST", `/api/v1/issues/${encode(ref)}/transition`, {
233
+ ...transitionBody(f),
234
+ onBehalfOf: config.person,
235
+ }),
236
+ json,
237
+ (p) => `${p.data.key} ${p.data.status} — ${p.data.title}`,
238
+ );
227
239
  throw new CliError("USAGE", `Unknown issues action: ${action}`, 2);
228
240
  }
229
241
 
242
+ const ISSUE_STATUSES = ["todo", "in_progress", "in_review", "blocked", "done", "wont_do"];
243
+ // A blocked reason names who or what clears it: "cleared by", "unblocks", "waiting on", "until".
244
+ const CLEARED_BY = /\b(clear(s|ed)?|unblock(s|ed)?|wait(s|ing)? (on|for)|until)\b/i;
245
+
246
+ /**
247
+ * transitionIssue's body ({ to, note }), with MW1-17 checked before anything
248
+ * is sent. Dev stores no separate reason or dev-check field: both go in the note.
249
+ */
250
+ export function transitionBody(f) {
251
+ const to = requireFlag(f, "to");
252
+ if (!ISSUE_STATUSES.includes(to))
253
+ throw new CliError("USAGE", `--to must be one of ${ISSUE_STATUSES.join(", ")}`, 2);
254
+ const reason = typeof f.reason === "string" ? f.reason.trim() : "";
255
+ const devCheck = typeof f["dev-check"] === "string" ? f["dev-check"].trim() : "";
256
+ if (f["dev-check"] !== undefined && to !== "done")
257
+ throw new CliError("USAGE", "--dev-check goes with --to done", 2);
258
+ if (to === "blocked" && !CLEARED_BY.test(reason))
259
+ 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);
260
+ if (to === "wont_do" && !reason)
261
+ throw new CliError("USAGE", "A move to wont_do needs --reason saying why (MW1-17)", 2);
262
+ if (to === "done" && !devCheck)
263
+ 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);
264
+ const note = [reason, devCheck && `Dev check: ${devCheck}`].filter(Boolean).join("\n");
265
+ return { to, ...(note ? { note } : {}) };
266
+ }
267
+
230
268
  export async function main(argv = process.argv.slice(2)) {
231
269
  if (argv.length === 1 && ["--version", "-v"].includes(argv[0]))
232
270
  return console.log(VERSION);
@@ -282,7 +320,7 @@ export async function main(argv = process.argv.slice(2)) {
282
320
  if (result.exitCode) process.exitCode = result.exitCode;
283
321
  return;
284
322
  }
285
- if (command === "harness" && ["install", "update"].includes(action)) {
323
+ if (command === "harness" && ["install", "update", "check"].includes(action)) {
286
324
  // `--include-drafts` is a switch: `--include-drafts false` would leave `false` here and turn drafts on.
287
325
  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
326
  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.5";
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodii/dev",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "Agent CLI for Nodii Dev, reached through the platform edge",
5
5
  "type": "module",
6
6
  "bin": {