@yagni-app/code-staging 1.2.6-staging.1952.1 → 1.2.6-staging.1958.1
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/extension/branding.d.ts +21 -1
- package/dist/extension/branding.js +53 -12
- package/dist/extension/index.js +5 -0
- package/dist/extension/pipeline/changeScan.d.ts +62 -0
- package/dist/extension/pipeline/changeScan.js +307 -0
- package/dist/extension/pipeline/cloud.d.ts +205 -0
- package/dist/extension/pipeline/cloud.js +382 -0
- package/dist/extension/pipeline/continuation.d.ts +7 -3
- package/dist/extension/pipeline/continuation.js +29 -13
- package/dist/extension/pipeline/findings.d.ts +9 -4
- package/dist/extension/pipeline/findings.js +25 -10
- package/dist/extension/pipeline/finish.d.ts +11 -1
- package/dist/extension/pipeline/finish.js +51 -5
- package/dist/extension/pipeline/headlessGo.d.ts +20 -1
- package/dist/extension/pipeline/headlessGo.js +205 -33
- package/dist/extension/pipeline/orchestrator.d.ts +8 -2
- package/dist/extension/pipeline/orchestrator.js +249 -68
- package/dist/extension/pipeline/personas.d.ts +8 -3
- package/dist/extension/pipeline/personas.js +42 -7
- package/dist/extension/pipeline/planOutput.d.ts +42 -0
- package/dist/extension/pipeline/planOutput.js +94 -0
- package/dist/extension/pipeline/stages.d.ts +34 -5
- package/dist/extension/pipeline/stages.js +136 -7
- package/dist/extension/pipeline/tierCap.d.ts +22 -1
- package/dist/extension/pipeline/tierCap.js +48 -1
- package/dist/extension/pipeline/types.d.ts +31 -0
- package/dist/extension/pipeline/types.js +2 -0
- package/dist/extension/pipeline/verify.d.ts +33 -0
- package/dist/extension/pipeline/verify.js +101 -9
- package/package.json +2 -2
|
@@ -24,6 +24,17 @@
|
|
|
24
24
|
* run ends at the reviewed candidate and never commits, pushes, opens a PR, or
|
|
25
25
|
* reports one. The mission's own prHandoff owns all of that.
|
|
26
26
|
*
|
|
27
|
+
* CLOUD (`--cloud`, the one-attempt lane; cloud.ts): the session runs the
|
|
28
|
+
* cloud lane (stages.ts `cloudLane`: plan, implement, review, fix, then verify
|
|
29
|
+
* and finish; one writer, one reviewer, four rounds) on the host's input
|
|
30
|
+
* folder beside the ticket and its env (`YAGNI_ATTEMPT_ID`, `YAGNI_EXECUTION_ID`,
|
|
31
|
+
* `YAGNI_GENERATION`, `YAGNI_MISSION_OUT_DIR`, `YAGNI_GO_STAGE_TIERS`). A fresh
|
|
32
|
+
* attempt plans and stops, writing `plan.md` and `plan.json`; a plan file
|
|
33
|
+
* starts implement; a continue file names its stage. Every execution but a
|
|
34
|
+
* plan commits its work locally on the branch the host checked out, whatever
|
|
35
|
+
* its stop, and never pushes. Its last act is `result.json`, mirrored on the
|
|
36
|
+
* last `pipeline_result` line. Mission mode below is untouched by it.
|
|
37
|
+
*
|
|
27
38
|
* CONTINUATION (`--continue-file`, mission mode only): the run continues an
|
|
28
39
|
* earlier attempt whose change is already in the tree as uncommitted edits. The
|
|
29
40
|
* build baselines the clean base, and an attempt that stopped in review, fix or
|
|
@@ -60,8 +71,12 @@
|
|
|
60
71
|
* ceilings (`SANDBOX_RUN_BUDGET`), not interactive /go's.
|
|
61
72
|
*/
|
|
62
73
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
63
|
-
import { dirname, join } from "node:path";
|
|
64
|
-
import {
|
|
74
|
+
import { dirname, join, relative } from "node:path";
|
|
75
|
+
import { CLOUD_SESSION_ENV, completeCloudSession, prepareCloudLane, readCloudEnv, } from "./cloud.js";
|
|
76
|
+
import { scanChange } from "./changeScan.js";
|
|
77
|
+
import { continuationResumePlan, parseContinuation, } from "./continuation.js";
|
|
78
|
+
import { finishExec } from "./finish.js";
|
|
79
|
+
import { snapshotWorkspace } from "./workspace.js";
|
|
65
80
|
import { resolveFanoutMode } from "./fanout.js";
|
|
66
81
|
import { makeFanoutBeats } from "./fanoutBeats.js";
|
|
67
82
|
import { isBlockingFinding } from "./findings.js";
|
|
@@ -69,14 +84,16 @@ import { MISSION_FIX_ROUND_FILE_NAME, readFixRoundFile } from "./fixRound.js";
|
|
|
69
84
|
import { normalizeMission } from "./mission.js";
|
|
70
85
|
import { MISSION_CHECKS_FILE_NAME, MISSION_CI_RESULTS_FILE_NAME, readMissionChecksFile, renderCiResults, } from "./missionChecks.js";
|
|
71
86
|
import { startTreeSnapshots } from "./treeSnapshot.js";
|
|
72
|
-
import { runPipeline as defaultRunPipeline } from "./orchestrator.js";
|
|
73
|
-
import { baselineDirs, makeRunVerify, makeTakeVerifyBaseline, makeWorkstreamCheck, planPaths, } from "./verify.js";
|
|
87
|
+
import { PipelineStageError, runPipeline as defaultRunPipeline, } from "./orchestrator.js";
|
|
88
|
+
import { baselineDirs, makeRunVerify, makeTakeVerifyBaseline, makeWorkstreamCheck, parseChangedPaths, planPaths, } from "./verify.js";
|
|
74
89
|
import { loadGroundingEnabled } from "../grounding.js";
|
|
75
90
|
import { codeStateHome } from "../stateHome.js";
|
|
76
91
|
import { parseTierCap, TIER_CAP_ENV } from "./tierCap.js";
|
|
77
|
-
import { parsePipelineTimeLeft, PIPELINE_TIME_LEFT_ENV, SANDBOX_RUN_BUDGET } from "./budget.js";
|
|
92
|
+
import { parsePipelineTimeLeft, PIPELINE_TIME_LEFT_ENV, SANDBOX_RUN_BUDGET, } from "./budget.js";
|
|
78
93
|
/** One-line usage copy, shared by every argument error. */
|
|
79
|
-
export const HEADLESS_GO_USAGE = "Usage: yagni go --headless --ticket-file <path> [--plan-file <path>] [--memo-file <path>] [--continue-file <path>] [--run-id <id>] [--cwd <path>] [--json]";
|
|
94
|
+
export const HEADLESS_GO_USAGE = "Usage: yagni go --headless [--cloud] --ticket-file <path> [--plan-file <path>] [--memo-file <path>] [--continue-file <path>] [--run-id <id>] [--cwd <path>] [--json]";
|
|
95
|
+
/** With a deadline, the final verify of a cloud session needs at least this much of it left. */
|
|
96
|
+
const FINAL_VERIFY_MIN_LEFT_MS = 5 * 60_000;
|
|
80
97
|
/**
|
|
81
98
|
* Exit codes. `verified` is deliberately narrow: only a `clean` stop means the
|
|
82
99
|
* run produced a reviewed candidate, so a round_cap / no_changes / failed run
|
|
@@ -105,6 +122,7 @@ export function parseHeadlessGoArgs(argv) {
|
|
|
105
122
|
const args = {
|
|
106
123
|
headless: false,
|
|
107
124
|
json: false,
|
|
125
|
+
cloud: false,
|
|
108
126
|
unknownFlags: [],
|
|
109
127
|
positionals: [],
|
|
110
128
|
missingValues: [],
|
|
@@ -124,6 +142,10 @@ export function parseHeadlessGoArgs(argv) {
|
|
|
124
142
|
args.json = true;
|
|
125
143
|
continue;
|
|
126
144
|
}
|
|
145
|
+
if (name === "--cloud") {
|
|
146
|
+
args.cloud = true;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
127
149
|
const key = VALUE_FLAGS[name];
|
|
128
150
|
if (key) {
|
|
129
151
|
const next = inlineValue ?? argv[i + 1];
|
|
@@ -199,7 +221,9 @@ function buildResult(result, ctx) {
|
|
|
199
221
|
rounds: result.rounds.length,
|
|
200
222
|
findings: result.findings.length,
|
|
201
223
|
blocking: result.findings.filter(isBlocking).length,
|
|
202
|
-
finalFindings: [...result.findings]
|
|
224
|
+
finalFindings: [...result.findings]
|
|
225
|
+
.sort((a, b) => Number(isBlocking(b)) - Number(isBlocking(a)))
|
|
226
|
+
.slice(0, 10),
|
|
203
227
|
stages: stageRows(result),
|
|
204
228
|
...(ctx.runId ? { runId: ctx.runId } : {}),
|
|
205
229
|
...(ctx.tierCap ? { tierCap: ctx.tierCap } : {}),
|
|
@@ -250,7 +274,9 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
250
274
|
return readFile(path);
|
|
251
275
|
}
|
|
252
276
|
catch (err) {
|
|
253
|
-
return {
|
|
277
|
+
return {
|
|
278
|
+
error: `Could not read the ${label} at ${path}: ${err instanceof Error ? err.message : String(err)}`,
|
|
279
|
+
};
|
|
254
280
|
}
|
|
255
281
|
};
|
|
256
282
|
const files = {
|
|
@@ -294,22 +320,35 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
294
320
|
const deadlineMs = timeLeftMs === undefined ? undefined : (deps.now ?? Date.now)() + timeLeftMs;
|
|
295
321
|
const budget = deadlineMs === undefined
|
|
296
322
|
? deps.budget
|
|
297
|
-
: {
|
|
323
|
+
: {
|
|
324
|
+
...(deps.budget ?? SANDBOX_RUN_BUDGET),
|
|
325
|
+
deadlineMs: deps.budget?.deadlineMs ?? deadlineMs,
|
|
326
|
+
};
|
|
298
327
|
const cwd = args.cwd ?? deps.cwd ?? process.cwd();
|
|
299
328
|
const emit = (obj) => write(JSON.stringify(obj));
|
|
300
329
|
const stream = args.json;
|
|
330
|
+
// A blank plan or memo file is NOT mission context: it must never read as an
|
|
331
|
+
// approved plan (which would skip the pipeline's planning on nothing at all).
|
|
332
|
+
const mission = normalizeMission({ plan: files.plan, memo: files.memo });
|
|
333
|
+
// The cloud session (cloud.ts): its identity and output folder from the
|
|
334
|
+
// host's env, read before anything runs, since its result must carry them.
|
|
335
|
+
const cloudEnv = args.cloud ? readCloudEnv(env) : undefined;
|
|
336
|
+
if (cloudEnv && "problem" in cloudEnv) {
|
|
337
|
+
writeErr(cloudEnv.problem);
|
|
338
|
+
writeErr(HEADLESS_GO_USAGE);
|
|
339
|
+
return { exitCode: HEADLESS_GO_EXIT.usage, error: cloudEnv.problem };
|
|
340
|
+
}
|
|
301
341
|
const childEnv = {
|
|
302
342
|
...(deps.childEnv ?? env),
|
|
303
343
|
...(tierCap ? { [TIER_CAP_ENV]: tierCap } : {}),
|
|
344
|
+
...(cloudEnv ? { [CLOUD_SESSION_ENV]: "1" } : {}),
|
|
304
345
|
};
|
|
305
|
-
// A blank plan or memo file is NOT mission context: it must never read as an
|
|
306
|
-
// approved plan (which would skip the pipeline's planning on nothing at all).
|
|
307
|
-
const mission = normalizeMission({ plan: files.plan, memo: files.memo });
|
|
308
346
|
// The continue file fails soft on every path: a continuation the run cannot
|
|
309
347
|
// read still has the saved change in its tree, and a normal mission on it beats
|
|
310
|
-
// no run at all.
|
|
348
|
+
// no run at all. A cloud session's continue file needs no plan: a fix or
|
|
349
|
+
// verify execution resumes from the attempt's branch.
|
|
311
350
|
let continuation;
|
|
312
|
-
if (args.continueFile && !mission) {
|
|
351
|
+
if (args.continueFile && !mission && !cloudEnv) {
|
|
313
352
|
writeErr(`Ignoring --continue-file ${args.continueFile}: a continuation needs a mission (--plan-file or --memo-file).`);
|
|
314
353
|
}
|
|
315
354
|
else if (args.continueFile) {
|
|
@@ -322,9 +361,27 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
322
361
|
else if (parsed)
|
|
323
362
|
writeErr(`Ignoring --continue-file ${args.continueFile}: ${parsed.reason}. Running as a normal mission.`);
|
|
324
363
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
364
|
+
// The cloud lane and the stage this execution starts at.
|
|
365
|
+
let cloud;
|
|
366
|
+
let lane;
|
|
367
|
+
if (cloudEnv) {
|
|
368
|
+
const prepared = prepareCloudLane({
|
|
369
|
+
inputDir: dirname(args.ticketFile ?? ""),
|
|
370
|
+
env,
|
|
371
|
+
...(args.continueFile ? { continueFile: args.continueFile } : {}),
|
|
372
|
+
...(continuation ? { continuation } : {}),
|
|
373
|
+
hasPlan: Boolean(mission?.plan),
|
|
374
|
+
readFile,
|
|
375
|
+
writeErr,
|
|
376
|
+
});
|
|
377
|
+
if ("problem" in prepared) {
|
|
378
|
+
writeErr(prepared.problem);
|
|
379
|
+
return { exitCode: HEADLESS_GO_EXIT.usage, error: prepared.problem };
|
|
380
|
+
}
|
|
381
|
+
cloud = { ...cloudEnv, stage: prepared.stage };
|
|
382
|
+
lane = prepared.lane;
|
|
383
|
+
}
|
|
384
|
+
const cloudStage = cloud?.stage;
|
|
328
385
|
// The implement diamond's recording thread, per RUN. The beats ride the
|
|
329
386
|
// NDJSON stream as `pipeline_fanout` records, and the mission collector
|
|
330
387
|
// folds them onto the Run.
|
|
@@ -333,15 +390,15 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
333
390
|
// host beside the ticket: the verify gate runs them inside the loop. No file
|
|
334
391
|
// (a host that predates it) leaves the gate as it was.
|
|
335
392
|
let gateOpts;
|
|
336
|
-
if (mission && args.ticketFile) {
|
|
393
|
+
if ((mission || cloud) && args.ticketFile) {
|
|
337
394
|
const checksPath = join(dirname(args.ticketFile), MISSION_CHECKS_FILE_NAME);
|
|
338
395
|
const read = readMissionChecksFile(checksPath, readFile);
|
|
339
396
|
if (read.file) {
|
|
340
397
|
const checks = read.file;
|
|
341
398
|
// How the failed CI checks' jobs went, for the host, beside the checks
|
|
342
|
-
// file. A write that fails is said
|
|
343
|
-
// round's own account, as before.
|
|
344
|
-
const resultsPath = join(dirname(args.ticketFile), MISSION_CI_RESULTS_FILE_NAME);
|
|
399
|
+
// file (a cloud session's outputs folder). A write that fails is said
|
|
400
|
+
// once; the host then trusts the round's own account, as before.
|
|
401
|
+
const resultsPath = join(cloud?.outDir ?? dirname(args.ticketFile), MISSION_CI_RESULTS_FILE_NAME);
|
|
345
402
|
let resultsWriteFailed = false;
|
|
346
403
|
const onFailedChecks = (results) => {
|
|
347
404
|
try {
|
|
@@ -366,7 +423,7 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
366
423
|
// is an ordinary mission; an unreadable one is reported and the run goes on
|
|
367
424
|
// the full path, which is slower but never wrong.
|
|
368
425
|
let fixRound;
|
|
369
|
-
if (mission && args.ticketFile) {
|
|
426
|
+
if ((cloud ? cloudStage === "fix" : mission) && args.ticketFile) {
|
|
370
427
|
const fixPath = join(dirname(args.ticketFile), MISSION_FIX_ROUND_FILE_NAME);
|
|
371
428
|
const read = readFixRoundFile(fixPath, readFile);
|
|
372
429
|
if (read.file)
|
|
@@ -376,10 +433,23 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
376
433
|
writeErr(`Ignoring ${fixPath}: ${why}. The fix round runs the full pipeline.`);
|
|
377
434
|
}
|
|
378
435
|
}
|
|
436
|
+
// A continuation past implement re-enters the review loop. A cloud fix
|
|
437
|
+
// execution with the round's findings in `fix-round.json` runs fix mode
|
|
438
|
+
// instead, which the loop's re-entry would otherwise override.
|
|
439
|
+
const resumeFrom = continuation && !(cloud && fixRound)
|
|
440
|
+
? continuationResumePlan(continuation, {
|
|
441
|
+
ticket: files.ticket,
|
|
442
|
+
...(args.runId ? { runId: args.runId } : {}),
|
|
443
|
+
...(mission ? { mission } : {}),
|
|
444
|
+
}, lane ? { maxRounds: lane.maxReviewRounds, committed: true } : {})
|
|
445
|
+
: undefined;
|
|
379
446
|
// The gate's own checks, once on the clean tree before the build (YAG-1003),
|
|
380
447
|
// in the packages the approved plan names: what already failed there is a
|
|
381
448
|
// note, never a finding. Only a fresh build's tree is the clean base; a
|
|
382
|
-
// continuation or a fix round already carries a change, so it takes none
|
|
449
|
+
// continuation or a fix round already carries a change, so it takes none (a
|
|
450
|
+
// cloud continuation's earlier work is committed on its branch, and taking
|
|
451
|
+
// the baseline there would excuse the attempt's own breakage). It then leans
|
|
452
|
+
// on `checks.json`, whose base standing the host took on the real base.
|
|
383
453
|
let baseline;
|
|
384
454
|
const logBaseline = (data) => {
|
|
385
455
|
deps.logger?.("verify_baseline", data);
|
|
@@ -389,11 +459,19 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
389
459
|
// With a deadline, the baseline gets at most a sixth of the run's time left,
|
|
390
460
|
// and none below a minute: the build itself must still fit.
|
|
391
461
|
const deadlineLeftMs = budget?.deadlineMs === undefined ? undefined : budget.deadlineMs - (deps.now ?? Date.now)();
|
|
392
|
-
const baselineBudgetMs = deadlineLeftMs === undefined
|
|
462
|
+
const baselineBudgetMs = deadlineLeftMs === undefined
|
|
463
|
+
? undefined
|
|
464
|
+
: Math.floor(deadlineLeftMs / BASELINE_SHARE_OF_TIME_LEFT);
|
|
393
465
|
if (mission?.plan && !continuation && !fixRound && !deps.signal?.aborted) {
|
|
394
466
|
const dirs = baselineDirs(cwd, planPaths(mission.plan), readFile);
|
|
395
|
-
if (dirs.length > 0 &&
|
|
396
|
-
|
|
467
|
+
if (dirs.length > 0 &&
|
|
468
|
+
baselineBudgetMs !== undefined &&
|
|
469
|
+
baselineBudgetMs < MIN_BASELINE_BUDGET_MS) {
|
|
470
|
+
logBaseline({
|
|
471
|
+
skipped: "too little of the run's time left",
|
|
472
|
+
deadlineLeftMs,
|
|
473
|
+
baselineBudgetMs,
|
|
474
|
+
});
|
|
397
475
|
}
|
|
398
476
|
else if (dirs.length > 0) {
|
|
399
477
|
try {
|
|
@@ -412,12 +490,31 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
412
490
|
// The last gate verdict's count of failures the clean tree already had, for the result.
|
|
413
491
|
let preexistingFailures = 0;
|
|
414
492
|
let runVerify;
|
|
415
|
-
|
|
416
|
-
|
|
493
|
+
// A cloud session reports the checks of the tree it commits: the gate's last
|
|
494
|
+
// verdict, with the tree it judged.
|
|
495
|
+
const snapshot = deps.snapshotWorkspace ?? snapshotWorkspace;
|
|
496
|
+
let gate;
|
|
497
|
+
let lastVerify;
|
|
498
|
+
// A cloud session's gate also runs the host's secret and binary scans on
|
|
499
|
+
// what it added (YAG-1000), its own run token among the secrets it looks for.
|
|
500
|
+
const exec = deps.exec ?? finishExec;
|
|
501
|
+
const sessionSecrets = [env.YAGNI_TOKEN, env.GH_ENTERPRISE_TOKEN].filter((value) => Boolean(value));
|
|
502
|
+
const scan = cloud
|
|
503
|
+
? (scanCwd, signal, skip) => scanChange(scanCwd, { exec, sessionSecrets }, skip, signal)
|
|
504
|
+
: undefined;
|
|
505
|
+
if (gateOpts || baseline || cloud) {
|
|
506
|
+
const made = (deps.makeRunVerify ?? makeRunVerify)({
|
|
507
|
+
...gateOpts,
|
|
508
|
+
...(baseline ? { baseline } : {}),
|
|
509
|
+
...(scan ? { scanChange: scan } : {}),
|
|
510
|
+
});
|
|
511
|
+
gate = made;
|
|
417
512
|
runVerify = async (verifyCwd, signal, baselinePaths) => {
|
|
418
|
-
const outcome = await
|
|
513
|
+
const outcome = await made(verifyCwd, signal, baselinePaths);
|
|
419
514
|
if (outcome.ran)
|
|
420
515
|
preexistingFailures = outcome.preexisting ?? 0;
|
|
516
|
+
if (cloud)
|
|
517
|
+
lastVerify = { outcome, tree: treeKey(await snapshot(verifyCwd, signal)) };
|
|
421
518
|
return outcome;
|
|
422
519
|
};
|
|
423
520
|
}
|
|
@@ -439,10 +536,13 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
439
536
|
...(budget ? { budget } : {}),
|
|
440
537
|
...(deps.stages ? { stages: deps.stages } : {}),
|
|
441
538
|
grounded: deps.grounded ?? grounding,
|
|
539
|
+
...(lane ? { lane } : {}),
|
|
442
540
|
...(mission ? { mission } : {}),
|
|
443
541
|
...(runVerify ? { runVerify } : {}),
|
|
444
542
|
...(baseline ? { runWorkstreamCheck: makeWorkstreamCheck({ baseline }) } : {}),
|
|
445
|
-
|
|
543
|
+
// A cloud execution never starts on an earlier attempt's uncommitted
|
|
544
|
+
// edits: that work is on the branch, so its tree baselines as it is.
|
|
545
|
+
...(continuation && !cloud ? { continuation: { stageAtStop: continuation.stageAtStop } } : {}),
|
|
446
546
|
...(resumeFrom ? { resumeFrom } : {}),
|
|
447
547
|
onEvent: (ev, tag) => {
|
|
448
548
|
deps.onEvent?.(ev, tag);
|
|
@@ -475,15 +575,78 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
475
575
|
emit({ type: "pipeline_log", event, ...(data === undefined ? {} : { data }) });
|
|
476
576
|
},
|
|
477
577
|
};
|
|
578
|
+
// A cloud session's start: the commit it builds on (its result's `baseSha`)
|
|
579
|
+
// and the tree's pre-run dirt (an install's lockfile churn), which never
|
|
580
|
+
// lands in its commit.
|
|
581
|
+
const startTree = cloud ? await snapshot(cwd, deps.signal) : undefined;
|
|
582
|
+
const baseSha = startTree?.head ?? "";
|
|
583
|
+
const preRunDirt = startTree ? parseChangedPaths(startTree.status) : [];
|
|
584
|
+
/** The cloud session's last act (cloud.ts `completeCloudSession`), mirrored on the stream. */
|
|
585
|
+
const completeCloud = async (session, ran) => {
|
|
586
|
+
const verifyGate = gate;
|
|
587
|
+
const done = await completeCloudSession({
|
|
588
|
+
identity: session.identity,
|
|
589
|
+
outDir: session.outDir,
|
|
590
|
+
stage: session.stage,
|
|
591
|
+
cwd,
|
|
592
|
+
baseSha,
|
|
593
|
+
preRunDirt,
|
|
594
|
+
...(mission?.plan ? { plan: mission.plan } : {}),
|
|
595
|
+
roundsDone: continuation?.reviewRoundsDone ?? 0,
|
|
596
|
+
...(lastVerify ? { lastVerify } : {}),
|
|
597
|
+
tree: async () => treeKey(await snapshot(cwd, deps.signal)),
|
|
598
|
+
// The checks run commands and may take minutes: with a deadline they
|
|
599
|
+
// need some of it left. The scan below runs no model and always runs.
|
|
600
|
+
...(verifyGate
|
|
601
|
+
? {
|
|
602
|
+
reverify: async () => {
|
|
603
|
+
const left = budget?.deadlineMs === undefined
|
|
604
|
+
? undefined
|
|
605
|
+
: budget.deadlineMs - (deps.now ?? Date.now)();
|
|
606
|
+
if (deps.signal?.aborted || (left !== undefined && left < FINAL_VERIFY_MIN_LEFT_MS))
|
|
607
|
+
return null;
|
|
608
|
+
return verifyGate(cwd, deps.signal, preRunDirt);
|
|
609
|
+
},
|
|
610
|
+
}
|
|
611
|
+
: {}),
|
|
612
|
+
scan: () => scanChange(cwd, { exec, sessionSecrets }, preRunDirt),
|
|
613
|
+
planPaths: (planText) => baselineDirs(cwd, planPaths(planText), readFile).map((dir) => relative(cwd, dir) || "."),
|
|
614
|
+
exec,
|
|
615
|
+
writeFile: deps.writeFile ?? writeFileSync,
|
|
616
|
+
}, ran);
|
|
617
|
+
const result = "result" in ran ? ran.result : undefined;
|
|
618
|
+
const headless = result
|
|
619
|
+
? buildResult(result, {
|
|
620
|
+
...(args.runId ? { runId: args.runId } : {}),
|
|
621
|
+
...(tierCap ? { tierCap } : {}),
|
|
622
|
+
preexistingFailures,
|
|
623
|
+
})
|
|
624
|
+
: undefined;
|
|
625
|
+
emit({ type: "pipeline_result", ...headless, ...done.attempt });
|
|
626
|
+
return {
|
|
627
|
+
exitCode: done.ok ? HEADLESS_GO_EXIT.verified : HEADLESS_GO_EXIT.unverified,
|
|
628
|
+
...(headless ? { result: headless } : {}),
|
|
629
|
+
...("error" in ran ? { error: ran.error } : {}),
|
|
630
|
+
};
|
|
631
|
+
};
|
|
478
632
|
// A mission's tree is the only copy of its work until the host commits it,
|
|
479
|
-
// so it is snapshotted for the host to recover from (treeSnapshot.ts).
|
|
480
|
-
|
|
633
|
+
// so it is snapshotted for the host to recover from (treeSnapshot.ts). A
|
|
634
|
+
// cloud execution that builds is snapshotted the same way until it commits.
|
|
635
|
+
// It stops before a cloud session commits, so its last snapshot is the
|
|
636
|
+
// tree being committed, and only once.
|
|
637
|
+
const snapshots = (cloud ? cloudStage !== "plan" : mission)
|
|
481
638
|
? (deps.startTreeSnapshots ?? startTreeSnapshots)(cwd, {
|
|
482
639
|
onError: (message) => writeErr(`Working-tree checkpoint failed (the run continues without it): ${message}`),
|
|
483
640
|
})
|
|
484
641
|
: null;
|
|
642
|
+
let stopping;
|
|
643
|
+
const stopSnapshots = () => (stopping ??= snapshots?.() ?? Promise.resolve());
|
|
485
644
|
try {
|
|
486
645
|
const result = await runPipeline(files.ticket, pipelineDeps);
|
|
646
|
+
if (cloud) {
|
|
647
|
+
await stopSnapshots();
|
|
648
|
+
return await completeCloud(cloud, { result });
|
|
649
|
+
}
|
|
487
650
|
const headless = buildResult(result, {
|
|
488
651
|
...(args.runId ? { runId: args.runId } : {}),
|
|
489
652
|
...(tierCap ? { tierCap } : {}),
|
|
@@ -501,10 +664,19 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
501
664
|
const stage = err?.stageId;
|
|
502
665
|
emit({ type: "pipeline_error", ...(stage ? { stage } : {}), message });
|
|
503
666
|
writeErr(message);
|
|
667
|
+
if (cloud) {
|
|
668
|
+
await stopSnapshots();
|
|
669
|
+
const partial = err instanceof PipelineStageError ? err.partial : [];
|
|
670
|
+
return await completeCloud(cloud, { error: message, partial });
|
|
671
|
+
}
|
|
504
672
|
return { exitCode: HEADLESS_GO_EXIT.unverified, error: message };
|
|
505
673
|
}
|
|
506
674
|
finally {
|
|
507
|
-
await stopSnapshots
|
|
675
|
+
await stopSnapshots();
|
|
508
676
|
}
|
|
509
677
|
}
|
|
678
|
+
/** A tree as the cloud session compares it: HEAD and the porcelain status. */
|
|
679
|
+
function treeKey(snapshot) {
|
|
680
|
+
return `${snapshot.head}\n${snapshot.status}`;
|
|
681
|
+
}
|
|
510
682
|
//# sourceMappingURL=headlessGo.js.map
|
|
@@ -32,7 +32,7 @@ import { type ResilienceAttemptRecord, type RunStageFn } from "./resilience.js";
|
|
|
32
32
|
import { runStage as defaultRunStage } from "./runner.js";
|
|
33
33
|
import { type VerifyOutcome, type WorkstreamCheckResult, type WorkstreamCheckTarget } from "./verify.js";
|
|
34
34
|
import { snapshotWorkspace as defaultSnapshotWorkspace } from "./workspace.js";
|
|
35
|
-
import { type CheckpointStore, type FanoutBudgetVerdict, type FanoutMode, type Finding, type PipelineStage, type JsonEvent, type PipelineProgress, type PipelineResult, type ResiliencePolicy, type ResumePlan, type StageId, type StageResult, type StageTag } from "./types.js";
|
|
35
|
+
import { type CheckpointStore, type FanoutBudgetVerdict, type FanoutMode, type Finding, type PipelineStage, type JsonEvent, type PipelineLane, type PipelineProgress, type PipelineResult, type ResiliencePolicy, type ResumePlan, type StageId, type StageResult, type StageTag } from "./types.js";
|
|
36
36
|
/** Each review round's findings as the host stores them: bounded, messages clipped. */
|
|
37
37
|
export declare const MAX_ROUND_ITEMS = 20;
|
|
38
38
|
export interface RoundFindingItem {
|
|
@@ -87,10 +87,16 @@ export interface RunPipelineDeps {
|
|
|
87
87
|
/** Wall clock for the budget's deadline checks (epoch ms). Defaults to `Date.now`; injected in tests. */
|
|
88
88
|
now?: () => number;
|
|
89
89
|
/**
|
|
90
|
-
* M6 eval: the stage list to run. Defaults to
|
|
90
|
+
* M6 eval: the stage list to run. Defaults to the lane's; the blind
|
|
91
91
|
* lane passes a grounding-tool-stripped copy (see eval.ts#blindStages).
|
|
92
92
|
*/
|
|
93
93
|
stages?: PipelineStage[];
|
|
94
|
+
/**
|
|
95
|
+
* The lane's stage list and loop rules (stages.ts). Defaults to `FULL_LANE`
|
|
96
|
+
* (the diamond, three lenses, two rounds); headless `--cloud` passes the
|
|
97
|
+
* cloud lane (one writer, one reviewer, four rounds, a plan that ends the run).
|
|
98
|
+
*/
|
|
99
|
+
lane?: PipelineLane;
|
|
94
100
|
/**
|
|
95
101
|
* M6 eval: when false, run the BLIND lane (grounding-free personas via
|
|
96
102
|
* personaBody's grounded flag, paired with grounding-stripped stages). Defaults
|