@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/README.md +26 -1
- package/dist/cli.js +18 -3
- package/dist/core.js +1 -1
- package/dist/devops-openapi.json +1 -1
- package/dist/figma-scope.js +205 -0
- package/dist/figma.js +16 -3
- package/dist/freeze.js +48 -16
- package/dist/gather.js +177 -12
- package/dist/hooks.js +22 -3
- package/dist/lanes.js +51 -12
- package/dist/nodii-dev.js +2 -2
- package/dist/runtime-launcher.js +2 -1
- package/dist/starting.js +28 -7
- package/dist/tune.js +1 -1
- package/dist/upload.js +10 -1
- package/package.json +1 -1
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
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 =
|
|
375
|
+
const figma = figmaScope.map((file) => ({
|
|
345
376
|
fileKey: file.fileKey,
|
|
346
|
-
...(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, ...(
|
|
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
|
-
|
|
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 =
|
|
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;
|
package/dist/runtime-launcher.js
CHANGED
|
@@ -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
|
-
|
|
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=
|
|
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 =
|
|
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
|
-
|
|
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
|
}
|