@yagni-app/code-staging 1.2.6-staging.1951.1 → 1.2.6-staging.1956.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 +20 -0
- package/dist/extension/branding.js +49 -8
- 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 +30 -3
- package/dist/extension/pipeline/headlessGo.js +253 -29
- 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 +128 -4
- package/dist/extension/pipeline/verify.js +356 -31
- 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
|
|
@@ -45,7 +56,8 @@
|
|
|
45
56
|
* - Always, as the last line: the result object
|
|
46
57
|
* `{type:"pipeline_result", ok, stopReason, rounds, findings, stages:[{stage,
|
|
47
58
|
* tier, usage}], …}` — or `{type:"pipeline_error", …}` when a build stage
|
|
48
|
-
* threw.
|
|
59
|
+
* threw. `preexistingFailures` (absent at 0) counts the gate's own
|
|
60
|
+
* typecheck, lint and test failures the clean tree already had (YAG-1003).
|
|
49
61
|
* - Exit codes: 0 ONLY on a verified candidate (a `clean` stop), 1 for any
|
|
50
62
|
* other outcome or a thrown pipeline error, 2 for a usage/input problem.
|
|
51
63
|
*
|
|
@@ -59,8 +71,12 @@
|
|
|
59
71
|
* ceilings (`SANDBOX_RUN_BUDGET`), not interactive /go's.
|
|
60
72
|
*/
|
|
61
73
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
62
|
-
import { dirname, join } from "node:path";
|
|
63
|
-
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";
|
|
64
80
|
import { resolveFanoutMode } from "./fanout.js";
|
|
65
81
|
import { makeFanoutBeats } from "./fanoutBeats.js";
|
|
66
82
|
import { isBlockingFinding } from "./findings.js";
|
|
@@ -68,20 +84,25 @@ import { MISSION_FIX_ROUND_FILE_NAME, readFixRoundFile } from "./fixRound.js";
|
|
|
68
84
|
import { normalizeMission } from "./mission.js";
|
|
69
85
|
import { MISSION_CHECKS_FILE_NAME, MISSION_CI_RESULTS_FILE_NAME, readMissionChecksFile, renderCiResults, } from "./missionChecks.js";
|
|
70
86
|
import { startTreeSnapshots } from "./treeSnapshot.js";
|
|
71
|
-
import { runPipeline as defaultRunPipeline } from "./orchestrator.js";
|
|
72
|
-
import { makeRunVerify } from "./verify.js";
|
|
87
|
+
import { PipelineStageError, runPipeline as defaultRunPipeline, } from "./orchestrator.js";
|
|
88
|
+
import { baselineDirs, makeRunVerify, makeTakeVerifyBaseline, makeWorkstreamCheck, parseChangedPaths, planPaths, } from "./verify.js";
|
|
73
89
|
import { loadGroundingEnabled } from "../grounding.js";
|
|
74
90
|
import { codeStateHome } from "../stateHome.js";
|
|
75
91
|
import { parseTierCap, TIER_CAP_ENV } from "./tierCap.js";
|
|
76
|
-
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";
|
|
77
93
|
/** One-line usage copy, shared by every argument error. */
|
|
78
|
-
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;
|
|
79
97
|
/**
|
|
80
98
|
* Exit codes. `verified` is deliberately narrow: only a `clean` stop means the
|
|
81
99
|
* run produced a reviewed candidate, so a round_cap / no_changes / failed run
|
|
82
100
|
* can never read as success to a script, a smoke gate, or CI.
|
|
83
101
|
*/
|
|
84
102
|
export const HEADLESS_GO_EXIT = { verified: 0, unverified: 1, usage: 2 };
|
|
103
|
+
/** The verify baseline's share of a deadline run's time left, and the least it is worth starting with. */
|
|
104
|
+
const BASELINE_SHARE_OF_TIME_LEFT = 6;
|
|
105
|
+
const MIN_BASELINE_BUDGET_MS = 60_000;
|
|
85
106
|
const VALUE_FLAGS = {
|
|
86
107
|
"--ticket-file": "ticketFile",
|
|
87
108
|
"--plan-file": "planFile",
|
|
@@ -101,6 +122,7 @@ export function parseHeadlessGoArgs(argv) {
|
|
|
101
122
|
const args = {
|
|
102
123
|
headless: false,
|
|
103
124
|
json: false,
|
|
125
|
+
cloud: false,
|
|
104
126
|
unknownFlags: [],
|
|
105
127
|
positionals: [],
|
|
106
128
|
missingValues: [],
|
|
@@ -120,6 +142,10 @@ export function parseHeadlessGoArgs(argv) {
|
|
|
120
142
|
args.json = true;
|
|
121
143
|
continue;
|
|
122
144
|
}
|
|
145
|
+
if (name === "--cloud") {
|
|
146
|
+
args.cloud = true;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
123
149
|
const key = VALUE_FLAGS[name];
|
|
124
150
|
if (key) {
|
|
125
151
|
const next = inlineValue ?? argv[i + 1];
|
|
@@ -195,7 +221,9 @@ function buildResult(result, ctx) {
|
|
|
195
221
|
rounds: result.rounds.length,
|
|
196
222
|
findings: result.findings.length,
|
|
197
223
|
blocking: result.findings.filter(isBlocking).length,
|
|
198
|
-
finalFindings: [...result.findings]
|
|
224
|
+
finalFindings: [...result.findings]
|
|
225
|
+
.sort((a, b) => Number(isBlocking(b)) - Number(isBlocking(a)))
|
|
226
|
+
.slice(0, 10),
|
|
199
227
|
stages: stageRows(result),
|
|
200
228
|
...(ctx.runId ? { runId: ctx.runId } : {}),
|
|
201
229
|
...(ctx.tierCap ? { tierCap: ctx.tierCap } : {}),
|
|
@@ -204,6 +232,7 @@ function buildResult(result, ctx) {
|
|
|
204
232
|
...(finish?.prUrl ? { prUrl: finish.prUrl } : {}),
|
|
205
233
|
...(result.verifyNote ? { verifyNote: result.verifyNote } : {}),
|
|
206
234
|
...(result.verifyCommand ? { verifyCommand: result.verifyCommand } : {}),
|
|
235
|
+
...(ctx.preexistingFailures ? { preexistingFailures: ctx.preexistingFailures } : {}),
|
|
207
236
|
};
|
|
208
237
|
}
|
|
209
238
|
/** Attribution rider for a streamed child event (never overwrites pi's own keys). */
|
|
@@ -245,7 +274,9 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
245
274
|
return readFile(path);
|
|
246
275
|
}
|
|
247
276
|
catch (err) {
|
|
248
|
-
return {
|
|
277
|
+
return {
|
|
278
|
+
error: `Could not read the ${label} at ${path}: ${err instanceof Error ? err.message : String(err)}`,
|
|
279
|
+
};
|
|
249
280
|
}
|
|
250
281
|
};
|
|
251
282
|
const files = {
|
|
@@ -289,22 +320,35 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
289
320
|
const deadlineMs = timeLeftMs === undefined ? undefined : (deps.now ?? Date.now)() + timeLeftMs;
|
|
290
321
|
const budget = deadlineMs === undefined
|
|
291
322
|
? deps.budget
|
|
292
|
-
: {
|
|
323
|
+
: {
|
|
324
|
+
...(deps.budget ?? SANDBOX_RUN_BUDGET),
|
|
325
|
+
deadlineMs: deps.budget?.deadlineMs ?? deadlineMs,
|
|
326
|
+
};
|
|
293
327
|
const cwd = args.cwd ?? deps.cwd ?? process.cwd();
|
|
294
328
|
const emit = (obj) => write(JSON.stringify(obj));
|
|
295
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
|
+
}
|
|
296
341
|
const childEnv = {
|
|
297
342
|
...(deps.childEnv ?? env),
|
|
298
343
|
...(tierCap ? { [TIER_CAP_ENV]: tierCap } : {}),
|
|
344
|
+
...(cloudEnv ? { [CLOUD_SESSION_ENV]: "1" } : {}),
|
|
299
345
|
};
|
|
300
|
-
// A blank plan or memo file is NOT mission context: it must never read as an
|
|
301
|
-
// approved plan (which would skip the pipeline's planning on nothing at all).
|
|
302
|
-
const mission = normalizeMission({ plan: files.plan, memo: files.memo });
|
|
303
346
|
// The continue file fails soft on every path: a continuation the run cannot
|
|
304
347
|
// read still has the saved change in its tree, and a normal mission on it beats
|
|
305
|
-
// 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.
|
|
306
350
|
let continuation;
|
|
307
|
-
if (args.continueFile && !mission) {
|
|
351
|
+
if (args.continueFile && !mission && !cloudEnv) {
|
|
308
352
|
writeErr(`Ignoring --continue-file ${args.continueFile}: a continuation needs a mission (--plan-file or --memo-file).`);
|
|
309
353
|
}
|
|
310
354
|
else if (args.continueFile) {
|
|
@@ -317,9 +361,27 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
317
361
|
else if (parsed)
|
|
318
362
|
writeErr(`Ignoring --continue-file ${args.continueFile}: ${parsed.reason}. Running as a normal mission.`);
|
|
319
363
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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;
|
|
323
385
|
// The implement diamond's recording thread, per RUN. The beats ride the
|
|
324
386
|
// NDJSON stream as `pipeline_fanout` records, and the mission collector
|
|
325
387
|
// folds them onto the Run.
|
|
@@ -327,16 +389,16 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
327
389
|
// A mission's approved checks and its pull request's CI jobs, written by the
|
|
328
390
|
// host beside the ticket: the verify gate runs them inside the loop. No file
|
|
329
391
|
// (a host that predates it) leaves the gate as it was.
|
|
330
|
-
let
|
|
331
|
-
if (mission && args.ticketFile) {
|
|
392
|
+
let gateOpts;
|
|
393
|
+
if ((mission || cloud) && args.ticketFile) {
|
|
332
394
|
const checksPath = join(dirname(args.ticketFile), MISSION_CHECKS_FILE_NAME);
|
|
333
395
|
const read = readMissionChecksFile(checksPath, readFile);
|
|
334
396
|
if (read.file) {
|
|
335
397
|
const checks = read.file;
|
|
336
398
|
// How the failed CI checks' jobs went, for the host, beside the checks
|
|
337
|
-
// file. A write that fails is said
|
|
338
|
-
// round's own account, as before.
|
|
339
|
-
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);
|
|
340
402
|
let resultsWriteFailed = false;
|
|
341
403
|
const onFailedChecks = (results) => {
|
|
342
404
|
try {
|
|
@@ -349,7 +411,7 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
349
411
|
writeErr(`Could not write ${resultsPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
350
412
|
}
|
|
351
413
|
};
|
|
352
|
-
|
|
414
|
+
gateOpts = { repoChecks: () => checks, onFailedChecks };
|
|
353
415
|
}
|
|
354
416
|
else if (read.problem !== "missing") {
|
|
355
417
|
const why = read.problem === "malformed" ? "not a version 1 checks file" : "it could not be read";
|
|
@@ -361,7 +423,7 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
361
423
|
// is an ordinary mission; an unreadable one is reported and the run goes on
|
|
362
424
|
// the full path, which is slower but never wrong.
|
|
363
425
|
let fixRound;
|
|
364
|
-
if (mission && args.ticketFile) {
|
|
426
|
+
if ((cloud ? cloudStage === "fix" : mission) && args.ticketFile) {
|
|
365
427
|
const fixPath = join(dirname(args.ticketFile), MISSION_FIX_ROUND_FILE_NAME);
|
|
366
428
|
const read = readFixRoundFile(fixPath, readFile);
|
|
367
429
|
if (read.file)
|
|
@@ -371,6 +433,91 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
371
433
|
writeErr(`Ignoring ${fixPath}: ${why}. The fix round runs the full pipeline.`);
|
|
372
434
|
}
|
|
373
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;
|
|
446
|
+
// The gate's own checks, once on the clean tree before the build (YAG-1003),
|
|
447
|
+
// in the packages the approved plan names: what already failed there is a
|
|
448
|
+
// note, never a finding. Only a fresh build's tree is the clean base; a
|
|
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.
|
|
453
|
+
let baseline;
|
|
454
|
+
const logBaseline = (data) => {
|
|
455
|
+
deps.logger?.("verify_baseline", data);
|
|
456
|
+
if (stream)
|
|
457
|
+
emit({ type: "pipeline_log", event: "verify_baseline", data });
|
|
458
|
+
};
|
|
459
|
+
// With a deadline, the baseline gets at most a sixth of the run's time left,
|
|
460
|
+
// and none below a minute: the build itself must still fit.
|
|
461
|
+
const deadlineLeftMs = budget?.deadlineMs === undefined ? undefined : budget.deadlineMs - (deps.now ?? Date.now)();
|
|
462
|
+
const baselineBudgetMs = deadlineLeftMs === undefined
|
|
463
|
+
? undefined
|
|
464
|
+
: Math.floor(deadlineLeftMs / BASELINE_SHARE_OF_TIME_LEFT);
|
|
465
|
+
if (mission?.plan && !continuation && !fixRound && !deps.signal?.aborted) {
|
|
466
|
+
const dirs = baselineDirs(cwd, planPaths(mission.plan), readFile);
|
|
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
|
+
});
|
|
475
|
+
}
|
|
476
|
+
else if (dirs.length > 0) {
|
|
477
|
+
try {
|
|
478
|
+
const taken = await (deps.takeVerifyBaseline ?? makeTakeVerifyBaseline())(cwd, dirs, deps.signal, baselineBudgetMs);
|
|
479
|
+
logBaseline({ ran: taken.ran, failures: taken.failures, notes: taken.notes });
|
|
480
|
+
if (taken.baseline.size > 0)
|
|
481
|
+
baseline = taken.baseline;
|
|
482
|
+
}
|
|
483
|
+
catch (err) {
|
|
484
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
485
|
+
logBaseline({ error: message });
|
|
486
|
+
writeErr(`The verify baseline could not run (every failure counts): ${message}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
// The last gate verdict's count of failures the clean tree already had, for the result.
|
|
491
|
+
let preexistingFailures = 0;
|
|
492
|
+
let runVerify;
|
|
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;
|
|
512
|
+
runVerify = async (verifyCwd, signal, baselinePaths) => {
|
|
513
|
+
const outcome = await made(verifyCwd, signal, baselinePaths);
|
|
514
|
+
if (outcome.ran)
|
|
515
|
+
preexistingFailures = outcome.preexisting ?? 0;
|
|
516
|
+
if (cloud)
|
|
517
|
+
lastVerify = { outcome, tree: treeKey(await snapshot(verifyCwd, signal)) };
|
|
518
|
+
return outcome;
|
|
519
|
+
};
|
|
520
|
+
}
|
|
374
521
|
const pipelineDeps = {
|
|
375
522
|
cwd,
|
|
376
523
|
...(fixRound ? { fixRound } : {}),
|
|
@@ -389,9 +536,13 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
389
536
|
...(budget ? { budget } : {}),
|
|
390
537
|
...(deps.stages ? { stages: deps.stages } : {}),
|
|
391
538
|
grounded: deps.grounded ?? grounding,
|
|
539
|
+
...(lane ? { lane } : {}),
|
|
392
540
|
...(mission ? { mission } : {}),
|
|
393
541
|
...(runVerify ? { runVerify } : {}),
|
|
394
|
-
...(
|
|
542
|
+
...(baseline ? { runWorkstreamCheck: makeWorkstreamCheck({ baseline }) } : {}),
|
|
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 } } : {}),
|
|
395
546
|
...(resumeFrom ? { resumeFrom } : {}),
|
|
396
547
|
onEvent: (ev, tag) => {
|
|
397
548
|
deps.onEvent?.(ev, tag);
|
|
@@ -424,19 +575,83 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
424
575
|
emit({ type: "pipeline_log", event, ...(data === undefined ? {} : { data }) });
|
|
425
576
|
},
|
|
426
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
|
+
};
|
|
427
632
|
// A mission's tree is the only copy of its work until the host commits it,
|
|
428
|
-
// so it is snapshotted for the host to recover from (treeSnapshot.ts).
|
|
429
|
-
|
|
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)
|
|
430
638
|
? (deps.startTreeSnapshots ?? startTreeSnapshots)(cwd, {
|
|
431
639
|
onError: (message) => writeErr(`Working-tree checkpoint failed (the run continues without it): ${message}`),
|
|
432
640
|
})
|
|
433
641
|
: null;
|
|
642
|
+
let stopping;
|
|
643
|
+
const stopSnapshots = () => (stopping ??= snapshots?.() ?? Promise.resolve());
|
|
434
644
|
try {
|
|
435
645
|
const result = await runPipeline(files.ticket, pipelineDeps);
|
|
646
|
+
if (cloud) {
|
|
647
|
+
await stopSnapshots();
|
|
648
|
+
return await completeCloud(cloud, { result });
|
|
649
|
+
}
|
|
436
650
|
const headless = buildResult(result, {
|
|
437
651
|
...(args.runId ? { runId: args.runId } : {}),
|
|
438
652
|
...(tierCap ? { tierCap } : {}),
|
|
439
653
|
...(mission ? { missionMode: true } : {}),
|
|
654
|
+
preexistingFailures,
|
|
440
655
|
});
|
|
441
656
|
emit({ type: "pipeline_result", ...headless });
|
|
442
657
|
return {
|
|
@@ -449,10 +664,19 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
449
664
|
const stage = err?.stageId;
|
|
450
665
|
emit({ type: "pipeline_error", ...(stage ? { stage } : {}), message });
|
|
451
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
|
+
}
|
|
452
672
|
return { exitCode: HEADLESS_GO_EXIT.unverified, error: message };
|
|
453
673
|
}
|
|
454
674
|
finally {
|
|
455
|
-
await stopSnapshots
|
|
675
|
+
await stopSnapshots();
|
|
456
676
|
}
|
|
457
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
|
+
}
|
|
458
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
|