@yagni-app/code-staging 1.2.6-staging.1952.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.
Files changed (30) hide show
  1. package/dist/extension/branding.d.ts +20 -0
  2. package/dist/extension/branding.js +49 -8
  3. package/dist/extension/index.js +5 -0
  4. package/dist/extension/pipeline/changeScan.d.ts +62 -0
  5. package/dist/extension/pipeline/changeScan.js +307 -0
  6. package/dist/extension/pipeline/cloud.d.ts +205 -0
  7. package/dist/extension/pipeline/cloud.js +382 -0
  8. package/dist/extension/pipeline/continuation.d.ts +7 -3
  9. package/dist/extension/pipeline/continuation.js +29 -13
  10. package/dist/extension/pipeline/findings.d.ts +9 -4
  11. package/dist/extension/pipeline/findings.js +25 -10
  12. package/dist/extension/pipeline/finish.d.ts +11 -1
  13. package/dist/extension/pipeline/finish.js +51 -5
  14. package/dist/extension/pipeline/headlessGo.d.ts +20 -1
  15. package/dist/extension/pipeline/headlessGo.js +205 -33
  16. package/dist/extension/pipeline/orchestrator.d.ts +8 -2
  17. package/dist/extension/pipeline/orchestrator.js +249 -68
  18. package/dist/extension/pipeline/personas.d.ts +8 -3
  19. package/dist/extension/pipeline/personas.js +42 -7
  20. package/dist/extension/pipeline/planOutput.d.ts +42 -0
  21. package/dist/extension/pipeline/planOutput.js +94 -0
  22. package/dist/extension/pipeline/stages.d.ts +34 -5
  23. package/dist/extension/pipeline/stages.js +136 -7
  24. package/dist/extension/pipeline/tierCap.d.ts +22 -1
  25. package/dist/extension/pipeline/tierCap.js +48 -1
  26. package/dist/extension/pipeline/types.d.ts +31 -0
  27. package/dist/extension/pipeline/types.js +2 -0
  28. package/dist/extension/pipeline/verify.d.ts +33 -0
  29. package/dist/extension/pipeline/verify.js +101 -9
  30. 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 { continuationResumePlan, parseContinuation } from "./continuation.js";
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].sort((a, b) => Number(isBlocking(b)) - Number(isBlocking(a))).slice(0, 10),
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 { error: `Could not read the ${label} at ${path}: ${err instanceof Error ? err.message : String(err)}` };
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
- : { ...(deps.budget ?? SANDBOX_RUN_BUDGET), deadlineMs: deps.budget?.deadlineMs ?? deadlineMs };
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
- const resumeFrom = continuation
326
- ? continuationResumePlan(continuation, { ticket: files.ticket, ...(args.runId ? { runId: args.runId } : {}), ...(mission ? { mission } : {}) })
327
- : undefined;
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 once; the host then trusts the
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 ? undefined : Math.floor(deadlineLeftMs / BASELINE_SHARE_OF_TIME_LEFT);
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 && baselineBudgetMs !== undefined && baselineBudgetMs < MIN_BASELINE_BUDGET_MS) {
396
- logBaseline({ skipped: "too little of the run's time left", deadlineLeftMs, baselineBudgetMs });
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
- if (gateOpts || baseline) {
416
- const gate = (deps.makeRunVerify ?? makeRunVerify)({ ...gateOpts, ...(baseline ? { baseline } : {}) });
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 gate(verifyCwd, signal, baselinePaths);
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
- ...(continuation ? { continuation: { stageAtStop: continuation.stageAtStop } } : {}),
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
- const stopSnapshots = mission
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 selectStages("full"); the blind
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