@nodii/dev 0.6.33 → 0.6.35

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/dist/lanes.js CHANGED
@@ -4,8 +4,9 @@ import { promisify } from "node:util";
4
4
  import { CliError, encode, devRequest, query, readConfig, requireFlag } from "./core.js";
5
5
  import { launchRecovery } from "./runtime-launcher.js";
6
6
  import { recordReviewReceipt, reviewCommitContext } from "./safety.js";
7
- import { GATHER_TIMEOUT_MS, PICKER_MODEL, gatherText, pickDocuments } from "./gather.js";
7
+ import { GATHER_TIMEOUT_MS, PICKER_MODEL, gatherText, pickDocuments, withPickLock } from "./gather.js";
8
8
  import { storeGatherFreeze } from "./freeze.js";
9
+ import { SWAP_BOUNDARIES, componentsForScope, guidelineLookup, laneFigmaScope, loadDsCatalog } from "./figma-scope.js";
9
10
 
10
11
  const execFileAsync = promisify(execFile);
11
12
  const via = { via: "nodii-dev" };
@@ -206,7 +207,7 @@ async function currentGatherLane(request, config, gitContext) {
206
207
  return matches[0].key;
207
208
  }
208
209
 
209
- export async function laneCommand(action, args, flags, { request = devRequest, config = readConfig, gitContext = inferGitContext, commitsOf = branchCommits, read = (path) => readFile(path, "utf8"), reviewContext = reviewCommitContext, reviewReceipt = recordReviewReceipt, picker = pickDocuments, changedPaths = laneChangedPaths, gatherTimeoutMs = GATHER_TIMEOUT_MS, gatherSignal, storeFreeze = storeGatherFreeze, launch = launchRecovery } = {}) {
210
+ export async function laneCommand(action, args, flags, { request = devRequest, config = readConfig, gitContext = inferGitContext, commitsOf = branchCommits, read = (path) => readFile(path, "utf8"), reviewContext = reviewCommitContext, reviewReceipt = recordReviewReceipt, picker = pickDocuments, changedPaths = laneChangedPaths, gatherTimeoutMs = GATHER_TIMEOUT_MS, gatherSignal, storeFreeze = storeGatherFreeze, launch = launchRecovery, catalog = loadDsCatalog, pickLock = withPickLock } = {}) {
210
211
  if (action === "list") {
211
212
  const c = flags.mine ? await config() : null;
212
213
  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 })}`);
@@ -230,6 +231,16 @@ export async function laneCommand(action, args, flags, { request = devRequest, c
230
231
  const payload = await request("POST", `/api/v1/lanes/${encode(key)}/start`, via);
231
232
  return { payload, human: (p) => `Started ${laneLine(p.data)}` };
232
233
  }
234
+ // The Freeze references the Clause map rather than holding it (hub-58).
235
+ if (action === "gather" && args[0] === "show") {
236
+ // A Gather flag here means a Gather was meant: never show silently instead.
237
+ const gatherFlags = ["picker", "shortlist", "documents", "delta", "objective", "done-check", "paths", "components", "limit", "section-limit-per-document"].filter((flag) => flags[flag] !== undefined);
238
+ if (gatherFlags.length)
239
+ throw new CliError("USAGE", `lane gather show takes only [<key>] [--gather ID]; --${gatherFlags.join(", --")} belong to lane gather.`, 2);
240
+ const key = need(args[1] ?? await currentGatherLane(request, config, gitContext), "lane gather show needs <key>");
241
+ const payload = await request("GET", `/api/v1/lanes/${encode(key)}/gather${query({ gatherId: flags.gather })}`);
242
+ return { payload, human: (p) => `Gather ${p.data.gatherId}\n${gatherText(p, p.data.objective ?? "")}` };
243
+ }
233
244
  const ref = need(args[0] ?? (action === "gather" ? await currentGatherLane(request, config, gitContext) : undefined), `lane ${action} needs <key>`);
234
245
  const id = ref;
235
246
  if (action === "show") {
@@ -302,11 +313,29 @@ export async function laneCommand(action, args, flags, { request = devRequest, c
302
313
  ...(Array.isArray(lane.repos) ? lane.repos.map((repo) => typeof repo === "string" ? repo : repo.repo) : []),
303
314
  ];
304
315
  const repos = [...new Set(repoRefs.filter(Boolean).map(repoName))];
305
- const components = flags.components ? list(flags.components) : [
306
- ...(lane.freeze?.boundaries?.figma ?? []).flatMap((file) => file.componentSetKeys ?? []),
307
- ...(lane.launch?.figmaScopes ?? []).flatMap((file) => file.componentSets ?? []),
316
+ // The Figma scope (published keys) pulls in the DS components its
317
+ // Surfaces are built from, so their guidelines and U-docs apply.
318
+ // null = no declared Figma scope (kept undeclared); [] = declared "no Figma".
319
+ // A first Gather on a launch scope stored before keys were enforced
320
+ // stores "no Figma" (fails closed) so the lane can swap boundaries.
321
+ let declaredFigma;
322
+ let invalidScope = null;
323
+ try {
324
+ declaredFigma = laneFigmaScope(lane);
325
+ } catch (error) {
326
+ if (error?.code !== "FIGMA_SCOPE_INVALID" || lane.freeze) throw error;
327
+ declaredFigma = [];
328
+ invalidScope = error.reason ?? error.message;
329
+ }
330
+ const figmaScope = declaredFigma ?? [];
331
+ const scoped = figmaScope.some((file) => file.componentSets.length)
332
+ ? componentsForScope(await catalog({ signal }), figmaScope)
333
+ : { surfaces: [], components: [], unknownKeys: [] };
334
+ const components = flags.components ? list(flags.components) : [...new Set([
335
+ ...scoped.components,
336
+ ...scoped.surfaces,
308
337
  ...(lane.components ?? []),
309
- ];
338
+ ])];
310
339
  const shortlistBody = {
311
340
  objective,
312
341
  paths,
@@ -322,7 +351,9 @@ export async function laneCommand(action, args, flags, { request = devRequest, c
322
351
  const triggers = shortlist.data?.triggers ?? [];
323
352
  if (byAgent && !byAgent.shortlist)
324
353
  return { payload: { data: { recorded: false, picker: "agent", shortlistId, objective, triggers } }, human: (p) => agentShortlistText(ref, p.data, flags) };
325
- const picked = byAgent ? { documents: byAgent.documents } : await picker({
354
+ // Only the local model pick takes the per-lane pick lock; an agent pick
355
+ // records a choice already made (AGENT-HARNESS-19 + GATHER-NA).
356
+ const picked = byAgent ? { documents: byAgent.documents } : await pickLock(lane.id ?? id, () => picker({
326
357
  objective,
327
358
  files: paths,
328
359
  triggers: triggers.map((trigger) => ({
@@ -330,7 +361,7 @@ export async function laneCommand(action, args, flags, { request = devRequest, c
330
361
  trigger: trigger.sentence,
331
362
  ...(trigger.forcedBy ? { forcedBy: trigger.forcedBy } : {}),
332
363
  })),
333
- }, { signal });
364
+ }, { signal }));
334
365
  const payload = await request("POST", `/api/v1/lanes/${encode(id)}/gather`, {
335
366
  objective,
336
367
  documentCodes: picked.documents,
@@ -341,10 +372,11 @@ export async function laneCommand(action, args, flags, { request = devRequest, c
341
372
  shortlistId,
342
373
  picker: byAgent ? "agent" : picked.model ?? PICKER_MODEL,
343
374
  }, { signal });
344
- const figma = lane.freeze?.boundaries?.figma ?? (lane.launch?.figmaScopes ?? []).map((file) => ({
375
+ const figma = figmaScope.map((file) => ({
345
376
  fileKey: file.fileKey,
346
- ...(file.componentSets?.length ? { componentSetKeys: file.componentSets } : {}),
377
+ ...(file.componentSets.length ? { componentSetKeys: file.componentSets } : {}),
347
378
  }));
379
+ const lookup = guidelineLookup(scoped.components);
348
380
  const issue = lane.issue ? { key: lane.issue.key, title: lane.issue.title } : null;
349
381
  const laneNotes = [
350
382
  lane.launch?.notes,
@@ -354,16 +386,23 @@ export async function laneCommand(action, args, flags, { request = devRequest, c
354
386
  const freezeRequest = (method, path, body, options = {}) => request(method, path, body, { ...options, signal });
355
387
  await storeFreeze(id, {
356
388
  gather: payload.data,
357
- boundaries: { repos: repositories, ...(figma.length ? { figma } : {}) },
389
+ boundaries: { repos: repositories, ...(declaredFigma ? { figma } : {}) },
358
390
  issue,
359
391
  laneNotes,
360
392
  doneCheck,
393
+ lookups: lookup ? [lookup] : [],
361
394
  }, { request: freezeRequest });
362
395
  const reasons = Object.fromEntries(triggers.map((trigger) => [
363
396
  trigger.code,
364
397
  Array.isArray(trigger.forcedBy) && trigger.forcedBy.length ? `forced by ${trigger.forcedBy.join(", ")}` : trigger.sentence,
365
398
  ]));
366
- return { payload, human: (p) => gatherText(p, objective, reasons) };
399
+ const scopeNotice = [
400
+ ...(scoped.unknownKeys.length ? [`Notice: no DS catalog Surface has component-set key ${scoped.unknownKeys.join(", ")}; no component guidelines were pulled for ${scoped.unknownKeys.length === 1 ? "it" : "them"}.`] : []),
401
+ ...(invalidScope ? [`Notice: the launch Figma scope is not published keys (${invalidScope.replace(/\.$/, "")}), so the Freeze was stored with no Figma scope and every Figma lock and write is refused. Set the published keys with ${SWAP_BOUNDARIES(id)}.`] : []),
402
+ ].map((line) => `\n${line}`).join("");
403
+ // The pick's model and latency ride along in --json (morning check records them).
404
+ const result = { ...payload, picker: { model: picked.model ?? null, latency: picked.latency ?? null } };
405
+ return { payload: result, human: (p) => `${gatherText(p, objective, reasons)}${scopeNotice}` };
367
406
  } catch (error) {
368
407
  if (signal.aborted) throw new CliError("GATHER_TIMEOUT", `Gather exceeded its ${Math.round(gatherTimeoutMs / 1000)}-second deadline; nothing was retried.`, 1);
369
408
  throw error;
package/dist/nodii-dev.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { fail, main } from "./cli.js";
3
3
  import { regroundCurrentLane, shouldReground } from "./freeze.js";
4
+ import { isHookDenial } from "./hooks.js";
4
5
 
5
6
  // Hooks run in the agent's environment, which may be incomplete or may not be
6
7
  // a git worktree. An unavailable hook must never turn into a refused tool call.
@@ -16,8 +17,7 @@ async function run() {
16
17
  }
17
18
 
18
19
  run().catch((error) => {
19
- const denial = String(error?.code ?? "").startsWith("SAFETY_") ||
20
- ["FIGMA_FILE_KEY_MISSING", "FIGMA_LOCK_DENIED", "WRITE_OUT_OF_SCOPE"].includes(error?.code);
20
+ const denial = isHookDenial(error?.code);
21
21
  if (argv[0] === "hook" && !denial) {
22
22
  process.exitCode = 0;
23
23
  return;
@@ -108,7 +108,8 @@ function buildInvocation(options, { env = process.env, home = homedir() } = {})
108
108
  const { runId, laneKey, runtime, model, cwd, addDirs, profileHome, devConfigPath } = options;
109
109
  const args = runtime === "codex"
110
110
  ? ["exec", "--json", "--cd", cwd, "--model", model, ...addDirs.flatMap((path) => ["--add-dir", path]), "-"]
111
- : ["--print", "--output-format", "stream-json", "--model", model, ...addDirs.flatMap((path) => ["--add-dir", path])];
111
+ // Claude Code refuses --print with stream-json unless --verbose is set.
112
+ : ["--print", "--output-format", "stream-json", "--verbose", "--model", model, ...addDirs.flatMap((path) => ["--add-dir", path])];
112
113
  return {
113
114
  command: runtime,
114
115
  args,
package/dist/starting.js CHANGED
@@ -4,6 +4,7 @@ import { setTimeout as delay } from "node:timers/promises";
4
4
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { CliError, configPath, devRequest, encode, query, readConfig, requireFlag } from "./core.js";
7
+ import { assertKnownSets, checkComponentSetKey, checkFileKey, loadDsCatalog, mergeScopes, scopesFromRoutes } from "./figma-scope.js";
7
8
  import { GATHER_TIMEOUT_MS } from "./gather.js";
8
9
  import { laneCommand, nextCommand } from "./lanes.js";
9
10
 
@@ -79,18 +80,30 @@ export function launchRepositories(key, flags) {
79
80
  }
80
81
 
81
82
  export function figmaScopes(value) {
82
- return values(value).map((raw) => {
83
+ return mergeScopes(values(value).map((raw) => {
83
84
  const scope = String(raw).trim();
84
85
  const at = scope.indexOf("=");
85
86
  const fileKey = (at < 0 ? scope : scope.slice(0, at)).trim();
86
87
  const componentSets = at < 0 ? [] : scope.slice(at + 1).split(",").map((item) => item.trim()).filter(Boolean);
87
88
  if (!fileKey || (at >= 0 && componentSets.length === 0))
88
- throw new CliError("USAGE", "--figma must be FILE_KEY or FILE_KEY=COMPONENT_SET[,COMPONENT_SET] and may be repeated.", 2);
89
+ throw new CliError("USAGE", "--figma must be FILE_KEY or FILE_KEY=COMPONENT_SET_KEY[,COMPONENT_SET_KEY] (published keys) and may be repeated.", 2);
89
90
  return {
90
- fileKey,
91
- componentSets,
91
+ fileKey: checkFileKey(fileKey),
92
+ componentSets: componentSets.map(checkComponentSetKey),
92
93
  };
93
- });
94
+ }));
95
+ }
96
+
97
+ /** --figma as given, plus what each --route derives from the DS catalog. */
98
+ export async function launchFigmaScopes(flags, { catalog = loadDsCatalog } = {}) {
99
+ const explicit = figmaScopes(flags.figma);
100
+ const routes = values(flags.route).map((route) => String(route).trim());
101
+ if (routes.some((route) => !route.startsWith("/")))
102
+ throw new CliError("USAGE", "--route must be a frontend route such as /app/dev/building and may be repeated.", 2);
103
+ if (!routes.length && !explicit.some((scope) => scope.componentSets.length)) return explicit;
104
+ const surfaces = await catalog();
105
+ assertKnownSets(surfaces, explicit);
106
+ return routes.length ? mergeScopes(explicit, scopesFromRoutes(surfaces, routes)) : explicit;
94
107
  }
95
108
 
96
109
  const stateFile = (machine) => join(dirname(configPath()), `coordinator-${encodeURIComponent(machine)}.json`);
@@ -273,6 +286,7 @@ export async function launchLaneCommand(key, flags, {
273
286
  head = primaryHead,
274
287
  launcher = defaultLaunchRuntime,
275
288
  figmaSeat = verifyFigmaSeat,
289
+ catalog = loadDsCatalog,
276
290
  laneConfig = loadJson,
277
291
  identityPath = laneConfigPath,
278
292
  } = {}) {
@@ -294,7 +308,7 @@ export async function launchLaneCommand(key, flags, {
294
308
  const identity = await laneConfig(devConfigPath);
295
309
  if (!identity?.agentId)
296
310
  throw new CliError("LANE_IDENTITY_REQUIRED", `Lane ${key} needs its provisioned Dev identity at ${devConfigPath}.`, 1);
297
- const scopes = figmaScopes(flags.figma);
311
+ const scopes = await launchFigmaScopes(flags, { catalog });
298
312
  const figmaSeatVerified = scopes.length ? await figmaSeat() : false;
299
313
  const profile = requireFlag(flags, "profile");
300
314
  if (!isAbsolute(profile)) throw new CliError("USAGE", "--profile must be an absolute runtime profile path.", 2);
@@ -356,7 +370,11 @@ export async function launchLaneCommand(key, flags, {
356
370
  }
357
371
  throw new CliError("LANE_LAUNCH_FAILED", `Lane ${key} is registered but its runtime did not start: ${error.message}. Dev recorded it for lane resume.`, 1);
358
372
  }
359
- return { payload, human: `Launched ${key}: ${payload.data.brief}` };
373
+ const figmaLines = body.figmaScopes.map((scope) => `Figma scope: ${scope.fileKey} ${scope.componentSets.length ? `component sets ${scope.componentSets.join(", ")}` : "(whole file)"}`);
374
+ return {
375
+ payload: { ...payload, figmaScopes: body.figmaScopes },
376
+ human: [`Launched ${key}: ${payload.data.brief}`, ...(figmaLines.length ? figmaLines : ["Figma scope: none declared"])].join("\n"),
377
+ };
360
378
  }
361
379
 
362
380
  export async function amendLaneCommand(key, flags, { request = devRequest } = {}) {
@@ -394,6 +412,9 @@ export async function amendmentGuard(argv, { request = devRequest, marker = curr
394
412
  throw new CliError("LANE_AMENDMENT_ACK_REQUIRED", `Lane ${lane.laneKey} changed:\n${detail}\nDelta gather required: run nodii-dev lane gather ${lane.laneKey} --delta.`, 1, { amendments: pending.data });
395
413
  }
396
414
 
415
+ // Gather's human output is a renderer of its payload; Delta gather prints the
416
+ // Clause map too, never only "Gather complete.".
417
+
397
418
  const runGather = (key, flags, { request, signal }) => laneCommand("gather", [key], flags, { request, gatherSignal: signal });
398
419
 
399
420
  // Gather's human text is its Clause map (a function of the payload).
package/dist/tune.js CHANGED
@@ -345,7 +345,7 @@ export async function tuneCommand(action, args, flags, { request = devRequest, p
345
345
  } catch (error) {
346
346
  // A Gemma picker this Mac cannot run is reported, not filled in; any
347
347
  // other failure is a real error and stops the run.
348
- if (!(error instanceof CliError) || !["PICKER_UNAVAILABLE", "PICKER_MODEL_MISSING", "PICKER_MODEL_VERSION_MISMATCH", "OLLAMA_URL_INVALID"].includes(error.code)) throw error;
348
+ if (!(error instanceof CliError) || !["PICKER_UNAVAILABLE", "PICKER_TIMEOUT", "PICKER_MODEL_MISSING", "PICKER_MODEL_VERSION_MISMATCH", "OLLAMA_URL_INVALID"].includes(error.code)) throw error;
349
349
  Object.assign(result, { status: "unavailable", code: error.code, reason: error.message });
350
350
  }
351
351
  }
package/dist/upload.js CHANGED
@@ -317,6 +317,15 @@ export async function rulesCommand(action, args, flags, { request = devRequest }
317
317
  const payload = await request("GET", `/api/v1/rules/find${query({ question, limit })}`);
318
318
  return { payload, human: rulesFindText };
319
319
  }
320
+ if (action === "show") {
321
+ const id = args[0]?.trim();
322
+ if (!id) throw new CliError("USAGE", "rules show needs a clause id, for example: nodii-dev rules show S04-7", 2);
323
+ const payload = await request("GET", `/api/v1/rules/clauses/${encode(id)}`);
324
+ return {
325
+ payload,
326
+ human: (p) => p.data.map((c) => `${c.clauseId}${c.counterInstinct ? " ⚑" : ""} · ${c.documentCode}@${c.documentVersion} › ${c.section}\n${c.text}`).join("\n\n"),
327
+ };
328
+ }
320
329
  if (action === "core") {
321
330
  // Dev refuses a core over the cap with CORE_OVER_CAP; that error is the output.
322
331
  const payload = await request("GET", "/api/v1/rules/core");
@@ -347,5 +356,5 @@ export async function rulesCommand(action, args, flags, { request = devRequest }
347
356
  const payload = await request("PUT", "/api/v1/rules/core/cap", { cap });
348
357
  return { payload, human: (p) => `Core cap: ${p.data.previous} -> ${p.data.cap} tokens.` };
349
358
  }
350
- throw new CliError("USAGE", 'rules find "QUESTION" [--limit N] | rules core | rules report [--section rulebook|services|all] [--lane KEY] | rules cap TOKENS', 2);
359
+ throw new CliError("USAGE", 'rules find "QUESTION" [--limit N] | rules show CLAUSE_ID | rules core | rules report [--section rulebook|services|all] [--lane KEY] | rules cap TOKENS', 2);
351
360
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodii/dev",
3
- "version": "0.6.33",
3
+ "version": "0.6.35",
4
4
  "description": "Agent CLI for Nodii Dev, reached through the platform edge",
5
5
  "type": "module",
6
6
  "bin": {