@nodii/dev 0.6.35 → 0.6.36
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 +59 -7
- package/dist/api.js +6 -0
- package/dist/cli.js +55 -9
- package/dist/core.js +1 -1
- package/dist/devops-openapi.json +1 -1
- package/dist/gather.js +105 -9
- package/dist/lanes.js +63 -11
- package/dist/remote.js +281 -0
- package/dist/runs.js +41 -7
- package/dist/starting.js +24 -11
- package/dist/upload.js +5 -1
- package/package.json +1 -1
package/dist/runs.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
2
4
|
import { basename } from "node:path";
|
|
3
5
|
import { CliError, devRequest, encode } from "./core.js";
|
|
4
6
|
|
|
@@ -36,7 +38,7 @@ function writeSafe(write, value) {
|
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
function currentRunId(runId = process.env[RUN_ID_ENV]) {
|
|
41
|
+
export function currentRunId(runId = process.env[RUN_ID_ENV]) {
|
|
40
42
|
if (runId === undefined || runId === "") return null;
|
|
41
43
|
if (!UUID.test(runId))
|
|
42
44
|
throw new CliError(
|
|
@@ -113,7 +115,7 @@ function commandLabel(command) {
|
|
|
113
115
|
return basename(command[0] ?? "command").slice(0, 240) || "command";
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
function runKind(value) {
|
|
118
|
+
export function runKind(value) {
|
|
117
119
|
const kind = value ?? "build";
|
|
118
120
|
if (!RUN_KINDS.has(kind))
|
|
119
121
|
throw new CliError(
|
|
@@ -138,7 +140,7 @@ function runEvidence(command, exitCode, signal, tail) {
|
|
|
138
140
|
return evidence;
|
|
139
141
|
}
|
|
140
142
|
|
|
141
|
-
async function endRun(request, runId, body, attempts = 3) {
|
|
143
|
+
export async function endRun(request, runId, body, attempts = 3) {
|
|
142
144
|
let error;
|
|
143
145
|
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
144
146
|
try {
|
|
@@ -152,6 +154,25 @@ async function endRun(request, runId, body, attempts = 3) {
|
|
|
152
154
|
throw error;
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
// The Result artifact contract (AGENT-HARNESS-41): a run that succeeds can
|
|
158
|
+
// attach one text artifact (e.g. a benchmark table as JSON). It rides in the
|
|
159
|
+
// Result as an `artifact` evidence item, delivered whole to the waiter: the
|
|
160
|
+
// note is the content, the ref names it with its size and sha256. Dev holds at
|
|
161
|
+
// most 10,000 characters in a note, so a larger artifact fails the run rather
|
|
162
|
+
// than arriving cut.
|
|
163
|
+
export const ARTIFACT_MAX_CHARS = 10_000;
|
|
164
|
+
|
|
165
|
+
export function artifactEvidence(name, content) {
|
|
166
|
+
const text = redactRunText(content);
|
|
167
|
+
if (!text.trim()) throw new CliError("ARTIFACT_EMPTY", `Artifact ${basename(name)} is empty.`, 1);
|
|
168
|
+
if (text.length > ARTIFACT_MAX_CHARS)
|
|
169
|
+
throw new CliError("ARTIFACT_TOO_LARGE", `Artifact ${basename(name)} is ${text.length} characters; a Result carries at most ${ARTIFACT_MAX_CHARS}.`, 1);
|
|
170
|
+
const sha256 = createHash("sha256").update(text).digest("hex");
|
|
171
|
+
return { kind: "artifact", ref: `${basename(name)} · ${Buffer.byteLength(text)} bytes · sha256:${sha256}`, note: text };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const artifactFailure = (error) => `Artifact not attached: ${redactRunText(error?.code === "ENOENT" ? "the file does not exist" : error?.message ?? error)}`;
|
|
175
|
+
|
|
155
176
|
export async function superviseRun({
|
|
156
177
|
command,
|
|
157
178
|
waiterRunId = process.env[RUN_ID_ENV],
|
|
@@ -160,6 +181,8 @@ export async function superviseRun({
|
|
|
160
181
|
expectedHeartbeatSeconds = 60,
|
|
161
182
|
heartbeatIntervalMs = DEFAULT_HEARTBEAT_MS,
|
|
162
183
|
tailBytes = DEFAULT_TAIL_BYTES,
|
|
184
|
+
artifact,
|
|
185
|
+
readArtifact = (path) => readFile(path, "utf8"),
|
|
163
186
|
request = devRequest,
|
|
164
187
|
spawn = nodeSpawn,
|
|
165
188
|
stdoutWrite = (value) => process.stdout.write(value),
|
|
@@ -268,12 +291,21 @@ export async function superviseRun({
|
|
|
268
291
|
outcome.signal,
|
|
269
292
|
tail,
|
|
270
293
|
);
|
|
271
|
-
|
|
272
|
-
|
|
294
|
+
let succeeded = outcome.exitCode === 0 && !outcome.signal && !spawnError;
|
|
295
|
+
let artifactError = null;
|
|
296
|
+
if (succeeded && artifact) {
|
|
297
|
+
try {
|
|
298
|
+
evidence.push(artifactEvidence(artifact, await readArtifact(artifact)));
|
|
299
|
+
} catch (error) {
|
|
300
|
+
succeeded = false;
|
|
301
|
+
artifactError = artifactFailure(error);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const reason = artifactError ?? (spawnError
|
|
273
305
|
? `Command could not start: ${redactRunText(spawnError.code ?? spawnError.message)}`
|
|
274
306
|
: outcome.signal
|
|
275
307
|
? `Command ended by signal ${outcome.signal}`
|
|
276
|
-
: `Command exited with code ${outcome.exitCode ?? -1}
|
|
308
|
+
: `Command exited with code ${outcome.exitCode ?? -1}`);
|
|
277
309
|
await endRun(
|
|
278
310
|
request,
|
|
279
311
|
runId,
|
|
@@ -283,7 +315,7 @@ export async function superviseRun({
|
|
|
283
315
|
);
|
|
284
316
|
return {
|
|
285
317
|
runId,
|
|
286
|
-
exitCode: succeeded ? 0 : (outcome.exitCode
|
|
318
|
+
exitCode: succeeded ? 0 : (outcome.exitCode || 1),
|
|
287
319
|
signal: outcome.signal,
|
|
288
320
|
tail: redactRunText(tail.toString("utf8")),
|
|
289
321
|
};
|
|
@@ -302,6 +334,7 @@ export async function launchRunSupervisor(
|
|
|
302
334
|
env = process.env,
|
|
303
335
|
kind,
|
|
304
336
|
name,
|
|
337
|
+
artifact,
|
|
305
338
|
} = {},
|
|
306
339
|
) {
|
|
307
340
|
if (!entrypoint)
|
|
@@ -309,6 +342,7 @@ export async function launchRunSupervisor(
|
|
|
309
342
|
const args = [entrypoint, "__run-supervisor"];
|
|
310
343
|
if (kind) args.push("--kind", kind);
|
|
311
344
|
if (name) args.push("--name", name);
|
|
345
|
+
if (artifact) args.push("--artifact", artifact);
|
|
312
346
|
args.push("--", ...command);
|
|
313
347
|
const supervisor = spawn(process.execPath, args, {
|
|
314
348
|
detached: true,
|
package/dist/starting.js
CHANGED
|
@@ -5,7 +5,7 @@ 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
7
|
import { assertKnownSets, checkComponentSetKey, checkFileKey, loadDsCatalog, mergeScopes, scopesFromRoutes } from "./figma-scope.js";
|
|
8
|
-
import { GATHER_TIMEOUT_MS } from "./gather.js";
|
|
8
|
+
import { GATHER_TIMEOUT_MS, gatherDeadline } from "./gather.js";
|
|
9
9
|
import { laneCommand, nextCommand } from "./lanes.js";
|
|
10
10
|
|
|
11
11
|
const execFile = promisify(execFileCallback);
|
|
@@ -346,6 +346,19 @@ export async function launchLaneCommand(key, flags, {
|
|
|
346
346
|
} catch (error) {
|
|
347
347
|
throw new CliError(error?.code ?? "LANE_REGISTER_FAILED", `${error?.message ?? error}. Worktrees were created and were not removed; reconcile them before retrying.`, 1);
|
|
348
348
|
}
|
|
349
|
+
// An agent is dispatched only onto a lane whose start Dev recorded: building,
|
|
350
|
+
// with its first steps current (LANE-RECORD-GAP-APPLY-VERDICTS-1).
|
|
351
|
+
const status = payload.data?.lane?.status;
|
|
352
|
+
if (status !== "building") {
|
|
353
|
+
const reason = `Dev registered lane ${key} as ${status ?? "unknown"}, not started (no lane start recorded)`;
|
|
354
|
+
let reported = "Dev recorded it for lane resume";
|
|
355
|
+
try {
|
|
356
|
+
await request("POST", `/api/v1/lanes/${encode(payload.data.lane.id)}/resume/${encode(payload.data.run.id)}/launch-failed`, { reason, ...via });
|
|
357
|
+
} catch (reportError) {
|
|
358
|
+
reported = `Dev could not record the launch failure (${reportError.message})`;
|
|
359
|
+
}
|
|
360
|
+
throw new CliError("LANE_NOT_STARTED", `${reason}, so its agent was not dispatched; ${reported}. Worktrees were created and were not removed.`, 1);
|
|
361
|
+
}
|
|
349
362
|
await markers(repositories, { laneKey: key, laneId: payload.data.lane.id, runId: payload.data.run.id });
|
|
350
363
|
try {
|
|
351
364
|
await launcher({
|
|
@@ -415,31 +428,31 @@ export async function amendmentGuard(argv, { request = devRequest, marker = curr
|
|
|
415
428
|
// Gather's human output is a renderer of its payload; Delta gather prints the
|
|
416
429
|
// Clause map too, never only "Gather complete.".
|
|
417
430
|
|
|
418
|
-
const runGather = (key, flags, { request,
|
|
431
|
+
const runGather = (key, flags, { request, deadline }) => laneCommand("gather", [key], flags, { request, deadline });
|
|
419
432
|
|
|
420
433
|
// Gather's human text is its Clause map (a function of the payload).
|
|
421
434
|
const gatheredText = (gathered) => typeof gathered.human === "function" ? gathered.human(gathered.payload) : gathered.human ?? "Gather complete.";
|
|
422
435
|
|
|
423
436
|
export async function gatherLaneCommand(key, flags, { request = devRequest, marker = currentMarker, gather = runGather, acknowledge = nextCommand, gatherTimeoutMs = GATHER_TIMEOUT_MS } = {}) {
|
|
424
|
-
|
|
425
|
-
const deadline =
|
|
426
|
-
const signal =
|
|
437
|
+
// The lane's local pick holds this deadline while it runs on its own budget.
|
|
438
|
+
const deadline = gatherDeadline(gatherTimeoutMs);
|
|
439
|
+
const signal = deadline.signal;
|
|
427
440
|
const timedRequest = (method, path, body, options = {}) => withSignal(request(method, path, body, { ...options, signal }), signal);
|
|
428
441
|
try {
|
|
429
442
|
const lane = await withSignal(marker(), signal);
|
|
430
443
|
key ??= lane?.laneKey;
|
|
431
444
|
if (!key) throw new CliError("USAGE", "lane gather needs <key> outside a launched lane worktree.", 2);
|
|
432
|
-
if (!flags.delta) return await withSignal(gather(key, flags, { request: timedRequest,
|
|
445
|
+
if (!flags.delta) return await withSignal(gather(key, flags, { request: timedRequest, deadline }), signal);
|
|
433
446
|
if (lane && lane.laneKey !== key) throw new CliError("LANE_RUN_REQUIRED", "Delta gather for another lane cannot run inside this lane's registered worktree.", 1);
|
|
434
|
-
if (!lane) return await withSignal(gather(key, flags, { request: timedRequest,
|
|
447
|
+
if (!lane) return await withSignal(gather(key, flags, { request: timedRequest, deadline }), signal);
|
|
435
448
|
const pending = await timedRequest("GET", `/api/v1/lanes/${encode(key)}/amendments${query({ runId: lane.runId })}`);
|
|
436
449
|
if (!pending.data.length) {
|
|
437
|
-
const gathered = await withSignal(gather(key, flags, { request: timedRequest,
|
|
450
|
+
const gathered = await withSignal(gather(key, flags, { request: timedRequest, deadline }), signal);
|
|
438
451
|
if (gathered.payload?.data?.recorded === false) return gathered;
|
|
439
452
|
return { ...gathered, human: `${gatheredText(gathered)}\nNo pending amendments.` };
|
|
440
453
|
}
|
|
441
454
|
const version = Math.max(...pending.data.map((item) => item.version));
|
|
442
|
-
const gathered = await withSignal(gather(key, flags, { request: timedRequest,
|
|
455
|
+
const gathered = await withSignal(gather(key, flags, { request: timedRequest, deadline }), signal);
|
|
443
456
|
// An agent picker's shortlist step records nothing, so it acknowledges nothing.
|
|
444
457
|
if (gathered.payload?.data?.recorded === false) return gathered;
|
|
445
458
|
const acknowledgement = await withSignal(acknowledge(key, {
|
|
@@ -454,9 +467,9 @@ export async function gatherLaneCommand(key, flags, { request = devRequest, mark
|
|
|
454
467
|
human: `${pending.data.map((item) => `v${item.version} ${item.reason}: ${JSON.stringify(item.change)}`).join("\n")}\n${gatheredText(gathered)}\nAcknowledged amendment version ${version}.`,
|
|
455
468
|
};
|
|
456
469
|
} catch (error) {
|
|
457
|
-
if (signal.aborted) throw new CliError("GATHER_TIMEOUT", `Gather exceeded
|
|
470
|
+
if (signal.aborted) throw new CliError("GATHER_TIMEOUT", `Gather's Dev requests exceeded their ${Math.round(gatherTimeoutMs / 1000)}-second deadline (the local pick runs on its own budget); nothing was retried.`, 1);
|
|
458
471
|
throw error;
|
|
459
472
|
} finally {
|
|
460
|
-
|
|
473
|
+
deadline.clear();
|
|
461
474
|
}
|
|
462
475
|
}
|
package/dist/upload.js
CHANGED
|
@@ -169,9 +169,12 @@ const uploadBody = (docs, cls, flags, decision) => ({
|
|
|
169
169
|
...(flags["meaning-unchanged"] && cls === "editorial" ? { meaningUnchangedReason: String(flags["meaning-unchanged"]) } : {}),
|
|
170
170
|
})),
|
|
171
171
|
...(decision ? { decision } : {}),
|
|
172
|
-
...(flags.pending ? { pending: true } : {}),
|
|
173
172
|
});
|
|
174
173
|
|
|
174
|
+
// Dev locks a ruling's decision as its change lands (DEV-DEC-AS-COMMIT-1,
|
|
175
|
+
// hub #406); there is no pending decision to draft any more.
|
|
176
|
+
export const PENDING_NO_OP = "--pending does nothing now: decisions lock on upload.";
|
|
177
|
+
|
|
175
178
|
export async function docsCommand(action, args, flags, { request = devRequest, log = console.error, cwd = process.cwd(), waitInterval } = {}) {
|
|
176
179
|
const json = Boolean(flags.json);
|
|
177
180
|
if (action === "check") {
|
|
@@ -193,6 +196,7 @@ export async function docsCommand(action, args, flags, { request = devRequest, l
|
|
|
193
196
|
throw new CliError("USAGE", "--pending is valid only with docs upload.", 2);
|
|
194
197
|
if (flags.pending && cls !== "ruling")
|
|
195
198
|
throw new CliError("USAGE", "--pending requires --class ruling and --decision FILE.", 2);
|
|
199
|
+
if (flags.pending) log(PENDING_NO_OP);
|
|
196
200
|
const decision = await readJsonFile(flags.decision, "decision");
|
|
197
201
|
if (cls === "ruling" && !decision) throw new CliError("RULING_NEEDS_DECISION", "A ruling write needs its decision: --decision DECISION.json (title, context, options, ruling, consequences, provenance, amends).", 2);
|
|
198
202
|
if (cls !== "ruling" && decision) throw new CliError("DECISION_NEEDS_RULING", "--decision goes with --class ruling only.", 2);
|