@linimin/pi-letscook 0.1.33 → 0.1.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.
@@ -50,6 +50,7 @@ type CompletionFiles = {
50
50
  activePath: string;
51
51
  sliceHistoryPath: string;
52
52
  stopHistoryPath: string;
53
+ verificationEvidencePath: string;
53
54
  compactionMarkerPath: string;
54
55
  };
55
56
 
@@ -59,6 +60,7 @@ type CompletionStateSnapshot = {
59
60
  state?: JsonRecord;
60
61
  plan?: JsonRecord;
61
62
  active?: JsonRecord;
63
+ verificationEvidence?: JsonRecord;
62
64
  activeSlice?: JsonRecord;
63
65
  };
64
66
 
@@ -146,7 +148,7 @@ type ContextProposalDecision = {
146
148
  analysis: ContextProposalAnalysis;
147
149
  };
148
150
 
149
- type ContextProposalConfirmAction = "start" | "edit" | "cancel";
151
+ type ContextProposalConfirmAction = "start" | "cancel";
150
152
 
151
153
  type ContextProposalConfirmationActionItem = {
152
154
  id: ContextProposalConfirmAction;
@@ -171,7 +173,6 @@ type ContextProposalConfirmationLayout = {
171
173
  type ContextProposalConfirmOptions = {
172
174
  title: string;
173
175
  nonInteractiveBehavior?: "accept" | "cancel";
174
- editorPrompt?: string;
175
176
  };
176
177
 
177
178
  class StartupAnalystOverlay extends Container {
@@ -275,6 +276,7 @@ function resolveFiles(root: string): CompletionFiles {
275
276
  activePath: path.join(agentDir, "active-slice.json"),
276
277
  sliceHistoryPath: path.join(agentDir, "slice-history.jsonl"),
277
278
  stopHistoryPath: path.join(agentDir, "stop-check-history.jsonl"),
279
+ verificationEvidencePath: path.join(agentDir, "verification-evidence.json"),
278
280
  compactionMarkerPath: path.join(tmpDir, "post-compaction-recovery.json"),
279
281
  };
280
282
  }
@@ -290,14 +292,24 @@ function walkUpForDir(startCwd: string, segments: string[]): string | undefined
290
292
  }
291
293
  }
292
294
 
295
+ function completionSearchRoots(startCwd: string): string[] {
296
+ return [...new Set([path.resolve(startCwd), path.resolve(process.cwd())])];
297
+ }
298
+
293
299
  function findCompletionRoot(startCwd: string): string | undefined {
294
- const profilePath = walkUpForDir(startCwd, [".agent", "profile.json"]);
295
- return profilePath ? path.dirname(path.dirname(profilePath)) : undefined;
300
+ for (const candidateRoot of completionSearchRoots(startCwd)) {
301
+ const profilePath = walkUpForDir(candidateRoot, [".agent", "profile.json"]);
302
+ if (profilePath) return path.dirname(path.dirname(profilePath));
303
+ }
304
+ return undefined;
296
305
  }
297
306
 
298
307
  function findRepoRoot(startCwd: string): string | undefined {
299
- const gitPath = walkUpForDir(startCwd, [".git"]);
300
- return gitPath ? path.dirname(gitPath) : undefined;
308
+ for (const candidateRoot of completionSearchRoots(startCwd)) {
309
+ const gitPath = walkUpForDir(candidateRoot, [".git"]);
310
+ if (gitPath) return path.dirname(gitPath);
311
+ }
312
+ return undefined;
301
313
  }
302
314
 
303
315
  async function readJson(filePath: string): Promise<JsonRecord | undefined> {
@@ -354,12 +366,14 @@ async function loadCompletionSnapshot(startCwd: string): Promise<CompletionState
354
366
  const state = await readJson(files.statePath);
355
367
  const plan = await readJson(files.planPath);
356
368
  const active = await readJson(files.activePath);
369
+ const verificationEvidence = await readJson(files.verificationEvidencePath);
357
370
  return {
358
371
  files,
359
372
  profile,
360
373
  state,
361
374
  plan,
362
375
  active,
376
+ verificationEvidence,
363
377
  activeSlice: findActiveSlice(plan, active),
364
378
  };
365
379
  }
@@ -439,92 +453,37 @@ function isWeakMissionAnchor(value: string): boolean {
439
453
 
440
454
  type MissionAnchorAssessment = {
441
455
  derived: string;
442
- needsConfirmation: boolean;
443
- reason?: string;
444
456
  };
445
457
 
446
458
  function assessMissionAnchor(rawGoal: string, projectName: string): MissionAnchorAssessment {
447
- const normalized = normalizeMissionAnchorText(rawGoal);
448
- const derived = deriveMissionAnchor(rawGoal, projectName);
449
- if (!normalized) {
450
- return {
451
- derived,
452
- needsConfirmation: true,
453
- reason: "No meaningful goal text was provided.",
454
- };
455
- }
456
- if (isWeakMissionAnchor(normalized)) {
457
- return {
458
- derived,
459
- needsConfirmation: true,
460
- reason: "The goal is too short or vague for stable canonical workflow state.",
461
- };
462
- }
463
- const vaguePronouns = /\b(this|that|it|things|stuff|something)\b/i.test(normalized);
464
- const fallback = derived === `Drive ${projectName} to truthful, verifiable completion.`;
465
- if (fallback || vaguePronouns) {
466
- return {
467
- derived,
468
- needsConfirmation: true,
469
- reason: fallback
470
- ? "The initial goal was too ambiguous, so the workflow fell back to a generic repo-based mission."
471
- : "The goal still contains ambiguous references that are better confirmed before writing canonical state.",
472
- };
473
- }
474
- return { derived, needsConfirmation: false };
475
- }
476
-
477
- async function confirmMissionAnchor(
478
- ctx: { hasUI: boolean; ui: any },
479
- assessment: MissionAnchorAssessment,
480
- ): Promise<string | undefined> {
481
- if (!getCtxHasUI(ctx)) return assessment.derived;
482
- const ui = getCtxUi(ctx);
483
- if (!ui) return assessment.derived;
484
- if (!assessment.needsConfirmation) return assessment.derived;
485
- const title = "Confirm mission anchor";
486
- const reason = assessment.reason ? `${assessment.reason}\n\n` : "";
487
- const choice = await ui.select(
488
- title,
489
- [
490
- `${reason}Proposed mission anchor:\n${assessment.derived}\n\nUse proposed mission anchor`,
491
- "Edit mission anchor",
492
- "Cancel",
493
- ],
494
- );
495
- if (!choice || choice === "Cancel") return undefined;
496
- if (choice === "Edit mission anchor") {
497
- const edited = await ui.editor(title, assessment.derived);
498
- return edited?.trim() ? edited.trim() : undefined;
499
- }
500
- return assessment.derived;
459
+ return { derived: deriveMissionAnchor(rawGoal, projectName) };
501
460
  }
502
461
 
503
462
  type ExistingWorkflowDecision =
504
463
  | { action: "continue"; currentMissionAnchor: string }
505
464
  | { action: "refocus"; currentMissionAnchor: string; missionAnchor: string };
506
465
 
507
- function completionTestWorkflowActionOverride(): "continue" | "refocus" | undefined {
466
+ function completionTestWorkflowActionOverride(): "continue" | "refocus" | "cancel" | undefined {
508
467
  const raw = process.env.PI_COMPLETION_EXISTING_WORKFLOW_ACTION?.trim().toLowerCase();
509
- return raw === "continue" || raw === "refocus" ? raw : undefined;
468
+ return raw === "continue" || raw === "refocus" || raw === "cancel" ? raw : undefined;
510
469
  }
511
470
 
512
471
  function shouldSkipDriverKickoffForTests(): boolean {
513
472
  return process.env.PI_COMPLETION_SKIP_DRIVER_KICKOFF === "1";
514
473
  }
515
474
 
516
- function completionTestContextProposalActionOverride(): "accept" | "edit" | "cancel" | undefined {
475
+ function completionTestContextProposalActionOverride(): "accept" | "cancel" | undefined {
517
476
  const raw = process.env.PI_COMPLETION_CONTEXT_PROPOSAL_ACTION?.trim().toLowerCase();
518
- return raw === "accept" || raw === "edit" || raw === "cancel" ? raw : undefined;
519
- }
520
-
521
- function completionTestContextProposalEditText(): string | undefined {
522
- return asString(process.env.PI_COMPLETION_CONTEXT_PROPOSAL_EDIT_TEXT);
477
+ return raw === "accept" || raw === "cancel" ? raw : undefined;
523
478
  }
524
479
 
525
480
  function completionTestContextProposalUiActionOverride(): ContextProposalConfirmAction | undefined {
526
481
  const raw = process.env.PI_COMPLETION_TEST_CONTEXT_PROPOSAL_UI_ACTION?.trim().toLowerCase();
527
- return raw === "start" || raw === "edit" || raw === "cancel" ? raw : undefined;
482
+ return raw === "start" || raw === "cancel" ? raw : undefined;
483
+ }
484
+
485
+ function completionTestExistingWorkflowChooserSnapshotPath(): string | undefined {
486
+ return asString(process.env.PI_COMPLETION_TEST_EXISTING_WORKFLOW_CHOOSER_PATH);
528
487
  }
529
488
 
530
489
  function completionTestContextProposalUiSnapshotPath(): string | undefined {
@@ -561,6 +520,12 @@ function maybeWriteTestSnapshot(targetPath: string | undefined, content: string)
561
520
  }
562
521
  }
563
522
 
523
+ const COOK_MAIN_CHAT_RERUN_GUIDANCE = "Discuss changes in the main chat and rerun /cook.";
524
+
525
+ function buildCookCancellationMessage(prefix: string): string {
526
+ return `${prefix}. ${COOK_MAIN_CHAT_RERUN_GUIDANCE}`;
527
+ }
528
+
564
529
  function shouldDisableContextProposalAnalyst(): boolean {
565
530
  return process.env.PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST === "1";
566
531
  }
@@ -1210,15 +1175,10 @@ function buildContextProposalConfirmationActions(): ContextProposalConfirmationA
1210
1175
  label: "Start",
1211
1176
  description: "Accept this proposal and let /cook write or refocus canonical workflow state.",
1212
1177
  },
1213
- {
1214
- id: "edit",
1215
- label: "Edit",
1216
- description: "Open the existing proposal editor before starting the workflow.",
1217
- },
1218
1178
  {
1219
1179
  id: "cancel",
1220
1180
  label: "Cancel",
1221
- description: "Exit without changing canonical workflow state.",
1181
+ description: `Stop here without changing canonical workflow state. ${COOK_MAIN_CHAT_RERUN_GUIDANCE}`,
1222
1182
  },
1223
1183
  ];
1224
1184
  }
@@ -1230,7 +1190,7 @@ function buildContextProposalConfirmationLayout(
1230
1190
  const analysis = finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]);
1231
1191
  return {
1232
1192
  title,
1233
- intro: "Review the proposed mission, scope, constraints, acceptance, critique, and routing details before /cook writes canonical workflow state.",
1193
+ intro: "Review the proposed mission, scope, constraints, acceptance, critique, and routing details before /cook writes canonical workflow state. This gate is approval-only: either Start it as-is or Cancel, discuss changes in the main chat, and rerun /cook.",
1234
1194
  proposalHeading: "Proposed workflow",
1235
1195
  proposalBody: buildContextProposalDisplayText(proposal),
1236
1196
  critiqueHeading: "Critique and risks",
@@ -1282,7 +1242,7 @@ async function promptContextProposalConfirmationAction(
1282
1242
  const container = new Container();
1283
1243
  container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
1284
1244
  container.addChild(new Text(theme.fg("accent", theme.bold(layout.title)), 1, 0));
1285
- container.addChild(new Text(theme.fg("dim", layout.intro), 1, 0));
1245
+ container.addChild(new Text(layout.intro, 1, 0));
1286
1246
  container.addChild(new Text("", 0, 0));
1287
1247
  container.addChild(new Text(theme.fg("accent", theme.bold(layout.proposalHeading)), 1, 0));
1288
1248
  container.addChild(new Text(layout.proposalBody, 1, 0));
@@ -1302,13 +1262,13 @@ async function promptContextProposalConfirmationAction(
1302
1262
  selectedPrefix: (text) => theme.fg("accent", text),
1303
1263
  selectedText: (text) => theme.fg("accent", text),
1304
1264
  description: (text) => theme.fg("muted", text),
1305
- scrollInfo: (text) => theme.fg("dim", text),
1265
+ scrollInfo: (text) => text,
1306
1266
  noMatch: (text) => theme.fg("warning", text),
1307
1267
  });
1308
1268
  selectList.onSelect = (item) => done(item.value as ContextProposalConfirmAction);
1309
1269
  selectList.onCancel = () => done(undefined);
1310
1270
  container.addChild(selectList);
1311
- container.addChild(new Text(theme.fg("dim", layout.footer), 1, 0));
1271
+ container.addChild(new Text(layout.footer, 1, 0));
1312
1272
  container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
1313
1273
 
1314
1274
  return {
@@ -1326,60 +1286,16 @@ async function promptContextProposalConfirmationAction(
1326
1286
  });
1327
1287
  }
1328
1288
 
1329
- async function resolveEditedContextProposalDecision(
1330
- ctx: { hasUI: boolean; ui: any },
1331
- projectName: string,
1332
- editedText: string,
1333
- confirmMissionWhenNeeded: boolean,
1334
- fallbackAnalysis?: ContextProposalAnalysis,
1335
- ): Promise<ContextProposalDecision | undefined> {
1336
- if (!editedText.trim()) return undefined;
1337
- const editedProposal = parseContextProposal(editedText, projectName);
1338
- if (editedProposal) {
1339
- return {
1340
- missionAnchor: editedProposal.mission,
1341
- goalText: editedProposal.goalText,
1342
- analysis: finalizeContextProposalAnalysis(
1343
- mergeContextProposalAnalysis([editedProposal.analysis, fallbackAnalysis], [editedText, editedProposal.mission]),
1344
- [editedText, editedProposal.mission],
1345
- ),
1346
- };
1347
- }
1348
- const assessment = assessMissionAnchor(editedText, projectName);
1349
- const analysis = finalizeContextProposalAnalysis(fallbackAnalysis, [editedText, assessment.derived]);
1350
- if (!confirmMissionWhenNeeded) {
1351
- return { missionAnchor: assessment.derived, goalText: editedText.trim(), analysis };
1352
- }
1353
- const missionAnchor = await confirmMissionAnchor(ctx, assessment);
1354
- if (!missionAnchor) return undefined;
1355
- return { missionAnchor, goalText: editedText.trim(), analysis };
1356
- }
1357
-
1358
1289
  async function resolveContextProposalConfirmationAction(
1359
- ctx: { hasUI: boolean; ui: any },
1360
1290
  proposal: ContextProposal,
1361
- projectName: string,
1362
- options: ContextProposalConfirmOptions,
1363
1291
  action: ContextProposalConfirmAction,
1364
- editedTextOverride?: string,
1365
1292
  ): Promise<ContextProposalDecision | undefined> {
1366
1293
  if (action === "cancel") return undefined;
1367
- const analysis = finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]);
1368
- if (action === "start") {
1369
- return { missionAnchor: proposal.mission, goalText: proposal.goalText, analysis };
1370
- }
1371
- const editedText =
1372
- editedTextOverride ??
1373
- (await getCtxUi(ctx)?.editor(
1374
- options.editorPrompt ?? `${options.title}\n\nEdit the proposed mission, scope, constraints, and acceptance details below.`,
1375
- buildContextProposalEditorText(proposal),
1376
- ));
1377
- if (!editedText?.trim()) return undefined;
1378
- return await resolveEditedContextProposalDecision(ctx, projectName, editedText, editedTextOverride === undefined, analysis);
1379
- }
1380
-
1381
- function buildContextProposalEditorText(proposal: ContextProposal): string {
1382
- return buildContextProposalGoalText(proposal);
1294
+ return {
1295
+ missionAnchor: proposal.mission,
1296
+ goalText: proposal.goalText,
1297
+ analysis: finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]),
1298
+ };
1383
1299
  }
1384
1300
 
1385
1301
  function parseContextProposal(text: string, projectName: string): ContextProposal | undefined {
@@ -1571,65 +1487,34 @@ async function buildGoalAnchoredContextProposal(
1571
1487
  async function confirmContextProposal(
1572
1488
  ctx: { hasUI: boolean; ui: any },
1573
1489
  proposal: ContextProposal,
1574
- projectName: string,
1575
1490
  options: ContextProposalConfirmOptions,
1576
1491
  ): Promise<ContextProposalDecision | undefined> {
1577
1492
  maybeWriteContextProposalSnapshot(proposal);
1578
1493
  const actionOverride = completionTestContextProposalActionOverride();
1579
1494
  if (actionOverride === "cancel") return undefined;
1580
1495
  if (actionOverride === "accept") {
1581
- return {
1582
- missionAnchor: proposal.mission,
1583
- goalText: proposal.goalText,
1584
- analysis: finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]),
1585
- };
1586
- }
1587
- if (actionOverride === "edit") {
1588
- const editedText = completionTestContextProposalEditText();
1589
- if (!editedText) return undefined;
1590
- return await resolveEditedContextProposalDecision(
1591
- ctx,
1592
- projectName,
1593
- editedText,
1594
- false,
1595
- finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]),
1596
- );
1496
+ return await resolveContextProposalConfirmationAction(proposal, "start");
1597
1497
  }
1598
1498
  const layout = buildContextProposalConfirmationLayout(options.title, proposal);
1599
1499
  maybeWriteContextProposalConfirmationSnapshot(layout);
1600
1500
  const uiActionOverride = completionTestContextProposalUiActionOverride();
1601
1501
  if (uiActionOverride) {
1602
- return await resolveContextProposalConfirmationAction(
1603
- ctx,
1604
- proposal,
1605
- projectName,
1606
- options,
1607
- uiActionOverride,
1608
- uiActionOverride === "edit" ? completionTestContextProposalEditText() : undefined,
1609
- );
1502
+ return await resolveContextProposalConfirmationAction(proposal, uiActionOverride);
1610
1503
  }
1611
1504
  if (!getCtxHasUI(ctx)) {
1612
1505
  return options.nonInteractiveBehavior === "accept"
1613
- ? {
1614
- missionAnchor: proposal.mission,
1615
- goalText: proposal.goalText,
1616
- analysis: finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]),
1617
- }
1506
+ ? await resolveContextProposalConfirmationAction(proposal, "start")
1618
1507
  : undefined;
1619
1508
  }
1620
1509
  const ui = getCtxUi(ctx);
1621
1510
  if (!ui) {
1622
1511
  return options.nonInteractiveBehavior === "accept"
1623
- ? {
1624
- missionAnchor: proposal.mission,
1625
- goalText: proposal.goalText,
1626
- analysis: finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]),
1627
- }
1512
+ ? await resolveContextProposalConfirmationAction(proposal, "start")
1628
1513
  : undefined;
1629
1514
  }
1630
1515
  const choice = await promptContextProposalConfirmationAction(ui, layout);
1631
1516
  if (!choice) return undefined;
1632
- return await resolveContextProposalConfirmationAction(ctx, proposal, projectName, options, choice);
1517
+ return await resolveContextProposalConfirmationAction(proposal, choice);
1633
1518
  }
1634
1519
 
1635
1520
  function currentMissionAnchor(snapshot: CompletionStateSnapshot): string {
@@ -1809,8 +1694,29 @@ function activeSliceContext(snapshot: CompletionStateSnapshot) {
1809
1694
  };
1810
1695
  }
1811
1696
 
1697
+ function verificationEvidenceContext(snapshot: CompletionStateSnapshot) {
1698
+ const evidence = snapshot.verificationEvidence;
1699
+ return {
1700
+ path: path.relative(snapshot.files.root, snapshot.files.verificationEvidencePath) || ".agent/verification-evidence.json",
1701
+ status: evidence ? "present" : "missing",
1702
+ subjectType: asString(evidence?.subject_type),
1703
+ sliceId: asString(evidence?.slice_id),
1704
+ goal: asString(evidence?.goal),
1705
+ contractIds: asStringArray(evidence?.contract_ids),
1706
+ basisCommit: asString(evidence?.basis_commit),
1707
+ headSha: asString(evidence?.head_sha),
1708
+ verificationCommands: asStringArray(evidence?.verification_commands),
1709
+ outcome: asString(evidence?.outcome),
1710
+ recordedAt: asString(evidence?.recorded_at),
1711
+ summary:
1712
+ asString(evidence?.summary) ??
1713
+ (evidence ? "Canonical verification evidence is present but its summary is missing." : "Canonical verification evidence is missing."),
1714
+ };
1715
+ }
1716
+
1812
1717
  function buildEvaluationRoleContextLines(snapshot: CompletionStateSnapshot, role: RubricEvaluationRole): string[] {
1813
1718
  const context = activeSliceContext(snapshot);
1719
+ const evidence = verificationEvidenceContext(snapshot);
1814
1720
  const lines = [
1815
1721
  `Canonical evaluation handoff for ${role}:`,
1816
1722
  `- task_type: ${currentTaskType(snapshot) ?? "(missing)"}`,
@@ -1829,6 +1735,17 @@ function buildEvaluationRoleContextLines(snapshot: CompletionStateSnapshot, role
1829
1735
  `- remaining_contract_ids_before: ${context.remainingBefore.length > 0 ? context.remainingBefore.join(", ") : "(none)"}`,
1830
1736
  `- release_blocker_count_before: ${context.releaseBlockerCountBefore ?? "(unknown)"}`,
1831
1737
  `- high_value_gap_count_before: ${context.highValueGapCountBefore ?? "(unknown)"}`,
1738
+ `- verification_evidence_path: ${evidence.path}`,
1739
+ `- verification_evidence_status: ${evidence.status}`,
1740
+ `- verification_evidence_subject_type: ${evidence.subjectType ?? "(missing)"}`,
1741
+ `- verification_evidence_slice_id: ${evidence.sliceId ?? "(none)"}`,
1742
+ `- verification_evidence_contract_ids: ${evidence.contractIds.length > 0 ? evidence.contractIds.join(", ") : "(none)"}`,
1743
+ `- verification_evidence_outcome: ${evidence.outcome ?? "(missing)"}`,
1744
+ `- verification_evidence_recorded_at: ${evidence.recordedAt ?? "(missing)"}`,
1745
+ `- verification_evidence_head_sha: ${evidence.headSha ?? "(missing)"}`,
1746
+ `- verification_evidence_basis_commit: ${evidence.basisCommit ?? "(missing)"}`,
1747
+ `- verification_evidence_commands: ${evidence.verificationCommands.length > 0 ? evidence.verificationCommands.join(" | ") : "(none)"}`,
1748
+ `- verification_evidence_summary: ${evidence.summary}`,
1832
1749
  ];
1833
1750
  return lines;
1834
1751
  }
@@ -1850,6 +1767,25 @@ async function confirmExistingWorkflowGoal(
1850
1767
  if (!normalizedGoal || normalizedGoal === normalizedCurrent || normalizedProposed === normalizedCurrent) {
1851
1768
  return { action: "continue", currentMissionAnchor: currentMission };
1852
1769
  }
1770
+ const title = [
1771
+ "Existing completion workflow found",
1772
+ "",
1773
+ "A workflow is already in progress. Choose how /cook should proceed:",
1774
+ "",
1775
+ "Current mission",
1776
+ currentMission,
1777
+ "",
1778
+ "New proposed mission",
1779
+ assessment.derived,
1780
+ ].join("\n");
1781
+ const continueChoice = "Continue current workflow\n\nKeep the current mission and treat the new goal as extra direction only.";
1782
+ const refocusChoice =
1783
+ "Abandon current workflow and start this new one\n\nReview the proposed replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state.";
1784
+ const cancelChoice = `Cancel\n\nKeep the current workflow unchanged. ${COOK_MAIN_CHAT_RERUN_GUIDANCE}`;
1785
+ maybeWriteTestSnapshot(
1786
+ completionTestExistingWorkflowChooserSnapshotPath(),
1787
+ `${JSON.stringify({ title, choices: [continueChoice, refocusChoice, cancelChoice] }, null, 2)}\n`,
1788
+ );
1853
1789
  const actionOverride = completionTestWorkflowActionOverride();
1854
1790
  if (actionOverride === "continue") {
1855
1791
  return { action: "continue", currentMissionAnchor: currentMission };
@@ -1857,6 +1793,7 @@ async function confirmExistingWorkflowGoal(
1857
1793
  if (actionOverride === "refocus") {
1858
1794
  return { action: "refocus", currentMissionAnchor: currentMission, missionAnchor: assessment.derived };
1859
1795
  }
1796
+ if (actionOverride === "cancel") return undefined;
1860
1797
  if (!getCtxHasUI(ctx)) {
1861
1798
  return { action: "continue", currentMissionAnchor: currentMission };
1862
1799
  }
@@ -1864,26 +1801,10 @@ async function confirmExistingWorkflowGoal(
1864
1801
  if (!ui) {
1865
1802
  return { action: "continue", currentMissionAnchor: currentMission };
1866
1803
  }
1867
- const title = [
1868
- "Existing completion workflow found",
1869
- "",
1870
- "A workflow is already in progress. Choose how /cook should proceed:",
1871
- "",
1872
- "Current mission",
1873
- currentMission,
1874
- "",
1875
- "New proposed mission",
1876
- assessment.derived,
1877
- ].join("\n");
1878
- const continueChoice = "Continue current workflow\n\nKeep the current mission and treat the new goal as extra direction only.";
1879
- const refocusChoice = "Abandon current workflow and start this new one\n\nReplace the current mission with the new goal, then rebuild canonical state from that new direction.";
1880
- const cancelChoice = "Cancel\n\nExit without changing the current workflow.";
1881
1804
  const choice = await ui.select(title, [continueChoice, refocusChoice, cancelChoice]);
1882
1805
  if (!choice || choice === cancelChoice) return undefined;
1883
1806
  if (choice === refocusChoice) {
1884
- const missionAnchor = await confirmMissionAnchor(ctx, assessment);
1885
- if (!missionAnchor) return undefined;
1886
- return { action: "refocus", currentMissionAnchor: currentMission, missionAnchor };
1807
+ return { action: "refocus", currentMissionAnchor: currentMission, missionAnchor: assessment.derived };
1887
1808
  }
1888
1809
  return { action: "continue", currentMissionAnchor: currentMission };
1889
1810
  }
@@ -1926,6 +1847,7 @@ async function refocusCompletionMission(
1926
1847
  writeJsonFile(snapshot.files.statePath, nextState),
1927
1848
  writeJsonFile(snapshot.files.planPath, nextPlan),
1928
1849
  writeJsonFile(snapshot.files.activePath, nextActive),
1850
+ writeJsonFile(snapshot.files.verificationEvidencePath, defaultVerificationEvidence()),
1929
1851
  ]);
1930
1852
  }
1931
1853
 
@@ -2043,8 +1965,25 @@ function defaultActiveSlice(
2043
1965
  };
2044
1966
  }
2045
1967
 
1968
+ function defaultVerificationEvidence(): JsonRecord {
1969
+ return {
1970
+ schema_version: 1,
1971
+ artifact_type: "completion-verification-evidence",
1972
+ subject_type: "none",
1973
+ slice_id: null,
1974
+ goal: null,
1975
+ contract_ids: [],
1976
+ basis_commit: null,
1977
+ head_sha: null,
1978
+ verification_commands: [],
1979
+ outcome: "not_recorded",
1980
+ recorded_at: null,
1981
+ summary: "No deterministic verification evidence is recorded yet because no selected slice or current-HEAD verification subject exists.",
1982
+ };
1983
+ }
1984
+
2046
1985
  function buildAgentReadme(projectName: string): string {
2047
- return `# Completion Control Plane\n\nThis repository uses the \`completion\` workflow for long-running coding tasks.\n\n## Canonical tracked contract files\n\n- \`.agent/README.md\`\n- \`.agent/mission.md\`\n- \`.agent/profile.json\`\n- \`.agent/verify_completion_stop.sh\`\n- \`.agent/verify_completion_control_plane.sh\`\n\n## Ignored canonical execution state\n\n- \`.agent/state.json\`\n- \`.agent/plan.json\`\n- \`.agent/active-slice.json\`\n- \`.agent/slice-history.jsonl\`\n- \`.agent/stop-check-history.jsonl\`\n- \`.agent/*.log\`\n- \`.agent/tmp/\`\n\nThe source of truth for long-running completion work is canonical \`.agent/**\` state plus current repo truth.\n\nProject: ${projectName}\n`;
1986
+ return `# Completion Control Plane\n\nThis repository uses the \`completion\` workflow for long-running coding tasks.\n\n## Canonical tracked contract files\n\n- \`.agent/README.md\`\n- \`.agent/mission.md\`\n- \`.agent/profile.json\`\n- \`.agent/verify_completion_stop.sh\`\n- \`.agent/verify_completion_control_plane.sh\`\n\n## Ignored canonical execution state\n\n- \`.agent/state.json\`\n- \`.agent/plan.json\`\n- \`.agent/active-slice.json\`\n- \`.agent/slice-history.jsonl\`\n- \`.agent/stop-check-history.jsonl\`\n- \`.agent/verification-evidence.json\`\n- \`.agent/*.log\`\n- \`.agent/tmp/\`\n\n\`.agent/verification-evidence.json\` is the durable canonical record of deterministic verification for the selected slice or current HEAD. Recovery, review, audit, and stop-check reminder surfaces consume it instead of temp-only artifacts or conversational summaries when it is populated.\n\nThe source of truth for long-running completion work is canonical \`.agent/**\` state plus current repo truth.\n\nProject: ${projectName}\n`;
2048
1987
  }
2049
1988
 
2050
1989
  function buildMission(projectName: string, missionAnchor: string): string {
@@ -2070,11 +2009,13 @@ for file in \
2070
2009
  .agent/verify_completion_control_plane.sh \
2071
2010
  .agent/state.json \
2072
2011
  .agent/plan.json \
2073
- .agent/active-slice.json; do
2012
+ .agent/active-slice.json \
2013
+ .agent/verification-evidence.json; do
2074
2014
  [[ -e "$file" ]] || { echo "missing required file: $file"; exit 1; }
2075
2015
  done
2076
2016
 
2077
2017
  node <<'NODE'
2018
+ const childProcess = require('node:child_process');
2078
2019
  const fs = require('node:fs');
2079
2020
 
2080
2021
  const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
@@ -2097,8 +2038,10 @@ const requireKeys = (object, required, label) => {
2097
2038
  assert(Object.prototype.hasOwnProperty.call(object, key), label + ': missing required field: ' + key);
2098
2039
  }
2099
2040
  };
2041
+ const hasOwn = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
2042
+ const sameStringArrays = (left, right) => left.length === right.length && left.every((item, index) => item === right[index]);
2100
2043
 
2101
- for (const file of ['.agent/profile.json', '.agent/state.json', '.agent/plan.json', '.agent/active-slice.json']) {
2044
+ for (const file of ['.agent/profile.json', '.agent/state.json', '.agent/plan.json', '.agent/active-slice.json', '.agent/verification-evidence.json']) {
2102
2045
  readJson(file);
2103
2046
  }
2104
2047
 
@@ -2106,11 +2049,13 @@ const profile = readJson('.agent/profile.json');
2106
2049
  const state = readJson('.agent/state.json');
2107
2050
  const plan = readJson('.agent/plan.json');
2108
2051
  const active = readJson('.agent/active-slice.json');
2052
+ const evidence = readJson('.agent/verification-evidence.json');
2109
2053
 
2110
2054
  assert(isObject(profile), '.agent/profile.json must be an object');
2111
2055
  assert(isObject(state), '.agent/state.json must be an object');
2112
2056
  assert(isObject(plan), '.agent/plan.json must be an object');
2113
2057
  assert(isObject(active), '.agent/active-slice.json must be an object');
2058
+ assert(isObject(evidence), '.agent/verification-evidence.json must be an object');
2114
2059
 
2115
2060
  const requiredProfile = ['schema_version', 'protocol_id', 'project_name', 'required_stop_judges', 'priority_policy_id', 'task_type', 'evaluation_profile', 'docs_surfaces'];
2116
2061
  requireKeys(profile, requiredProfile, '.agent/profile.json');
@@ -2141,6 +2086,8 @@ assert(isStringArray(state.release_blocker_ids), '.agent/state.json: release_blo
2141
2086
 
2142
2087
  const requiredPlan = ['schema_version', 'mission_anchor', 'task_type', 'evaluation_profile', 'last_reground_at', 'plan_basis', 'candidate_slices'];
2143
2088
  const requiredSlice = ['slice_id', 'goal', 'acceptance_criteria', 'contract_ids', 'priority', 'status', 'why_now', 'blocked_on', 'evidence'];
2089
+ const planMirrorFields = ['locked_notes', 'must_fix_findings', 'implementation_surfaces', 'verification_commands', 'basis_commit', 'remaining_contract_ids_before', 'release_blocker_count_before', 'high_value_gap_count_before'];
2090
+ const allowedSlice = [...requiredSlice, ...planMirrorFields];
2144
2091
  const sliceStatuses = ['planned', 'selected', 'in_progress', 'blocked', 'done', 'cancelled'];
2145
2092
  requireKeys(plan, requiredPlan, '.agent/plan.json');
2146
2093
  hasOnlyKeys(plan, requiredPlan, '.agent/plan.json');
@@ -2151,7 +2098,7 @@ for (const [index, slice] of plan.candidate_slices.entries()) {
2151
2098
  const label = '.agent/plan.json candidate_slices[' + index + ']';
2152
2099
  assert(isObject(slice), label + ' must be an object');
2153
2100
  requireKeys(slice, requiredSlice, label);
2154
- hasOnlyKeys(slice, requiredSlice, label);
2101
+ hasOnlyKeys(slice, allowedSlice, label);
2155
2102
  assert(isString(slice.slice_id) && slice.slice_id.length > 0, label + ': slice_id must be a non-empty string');
2156
2103
  assert(isString(slice.goal) && slice.goal.length > 0, label + ': goal must be a non-empty string');
2157
2104
  assert(Array.isArray(slice.acceptance_criteria) && slice.acceptance_criteria.length > 0 && slice.acceptance_criteria.every((item) => typeof item === 'string' && item.length > 0), label + ': acceptance_criteria must be a non-empty array of strings');
@@ -2161,6 +2108,14 @@ for (const [index, slice] of plan.candidate_slices.entries()) {
2161
2108
  assert(isString(slice.why_now) && slice.why_now.length > 0, label + ': why_now must be a non-empty string');
2162
2109
  assert(isStringArray(slice.blocked_on), label + ': blocked_on must be an array of strings');
2163
2110
  assert(isStringArray(slice.evidence), label + ': evidence must be an array of strings');
2111
+ if (hasOwn(slice, 'locked_notes')) assert(isStringArray(slice.locked_notes), label + ': locked_notes must be an array of strings when present');
2112
+ if (hasOwn(slice, 'must_fix_findings')) assert(isStringArray(slice.must_fix_findings), label + ': must_fix_findings must be an array of strings when present');
2113
+ if (hasOwn(slice, 'implementation_surfaces')) assert(isStringArray(slice.implementation_surfaces), label + ': implementation_surfaces must be an array of strings when present');
2114
+ if (hasOwn(slice, 'verification_commands')) assert(isStringArray(slice.verification_commands), label + ': verification_commands must be an array of strings when present');
2115
+ if (hasOwn(slice, 'basis_commit')) assert(isNonEmptyString(slice.basis_commit), label + ': basis_commit must be a non-empty string when present');
2116
+ if (hasOwn(slice, 'remaining_contract_ids_before')) assert(isStringArray(slice.remaining_contract_ids_before), label + ': remaining_contract_ids_before must be an array of strings when present');
2117
+ if (hasOwn(slice, 'release_blocker_count_before')) assert(typeof slice.release_blocker_count_before === 'number' && Number.isFinite(slice.release_blocker_count_before), label + ': release_blocker_count_before must be a finite number when present');
2118
+ if (hasOwn(slice, 'high_value_gap_count_before')) assert(typeof slice.high_value_gap_count_before === 'number' && Number.isFinite(slice.high_value_gap_count_before), label + ': high_value_gap_count_before must be a finite number when present');
2164
2119
  }
2165
2120
 
2166
2121
  const isNonEmptyStringArray = (value) => Array.isArray(value) && value.length > 0 && value.every((item) => isNonEmptyString(item));
@@ -2181,6 +2136,23 @@ assert(isStringArray(active.implementation_surfaces), '.agent/active-slice.json:
2181
2136
  assert(isStringArray(active.verification_commands), '.agent/active-slice.json: verification_commands must be an array of strings');
2182
2137
  assert(isStringArray(active.remaining_contract_ids_before), '.agent/active-slice.json: remaining_contract_ids_before must be an array of strings');
2183
2138
 
2139
+ const requiredEvidence = ['schema_version', 'artifact_type', 'subject_type', 'slice_id', 'goal', 'contract_ids', 'basis_commit', 'head_sha', 'verification_commands', 'outcome', 'recorded_at', 'summary'];
2140
+ const evidenceSubjectTypes = ['none', 'selected_slice', 'current_head'];
2141
+ const evidenceOutcomes = ['not_recorded', 'passed', 'failed'];
2142
+ requireKeys(evidence, requiredEvidence, '.agent/verification-evidence.json');
2143
+ hasOnlyKeys(evidence, requiredEvidence, '.agent/verification-evidence.json');
2144
+ assert(evidence.artifact_type === 'completion-verification-evidence', '.agent/verification-evidence.json: artifact_type must be completion-verification-evidence');
2145
+ assert(evidenceSubjectTypes.includes(evidence.subject_type), '.agent/verification-evidence.json: invalid subject_type');
2146
+ assert(evidence.slice_id === null || isNonEmptyString(evidence.slice_id), '.agent/verification-evidence.json: slice_id must be null or a non-empty string');
2147
+ assert(evidence.goal === null || isNonEmptyString(evidence.goal), '.agent/verification-evidence.json: goal must be null or a non-empty string');
2148
+ assert(isStringArray(evidence.contract_ids), '.agent/verification-evidence.json: contract_ids must be an array of strings');
2149
+ assert(evidence.basis_commit === null || isNonEmptyString(evidence.basis_commit), '.agent/verification-evidence.json: basis_commit must be null or a non-empty string');
2150
+ assert(evidence.head_sha === null || isNonEmptyString(evidence.head_sha), '.agent/verification-evidence.json: head_sha must be null or a non-empty string');
2151
+ assert(isStringArray(evidence.verification_commands), '.agent/verification-evidence.json: verification_commands must be an array of strings');
2152
+ assert(evidenceOutcomes.includes(evidence.outcome), '.agent/verification-evidence.json: invalid outcome');
2153
+ assert(evidence.recorded_at === null || (isNonEmptyString(evidence.recorded_at) && !Number.isNaN(Date.parse(evidence.recorded_at))), '.agent/verification-evidence.json: recorded_at must be null or an ISO-8601 string');
2154
+ assert(isNonEmptyString(evidence.summary), '.agent/verification-evidence.json: summary must be a non-empty string');
2155
+
2184
2156
  assert(state.task_type === profile.task_type, '.agent/state.json: task_type must match .agent/profile.json');
2185
2157
  assert(plan.task_type === profile.task_type, '.agent/plan.json: task_type must match .agent/profile.json');
2186
2158
  assert(active.task_type === profile.task_type, '.agent/active-slice.json: task_type must match .agent/profile.json');
@@ -2198,7 +2170,80 @@ if (requiresExactHandoff) {
2198
2170
  assert(isString(active.basis_commit) && active.basis_commit.length > 0, '.agent/active-slice.json: basis_commit must be a non-empty string when status carries an exact handoff');
2199
2171
  assert(typeof active.release_blocker_count_before === 'number' && Number.isFinite(active.release_blocker_count_before), '.agent/active-slice.json: release_blocker_count_before must be a finite number when status carries an exact handoff');
2200
2172
  assert(typeof active.high_value_gap_count_before === 'number' && Number.isFinite(active.high_value_gap_count_before), '.agent/active-slice.json: high_value_gap_count_before must be a finite number when status carries an exact handoff');
2173
+
2174
+ const planSlice = plan.candidate_slices.find((slice) => isObject(slice) && slice.slice_id === active.slice_id);
2175
+ assert(isObject(planSlice), '.agent/active-slice.json: slice_id must match a slice in .agent/plan.json when status carries an exact handoff');
2176
+ const drift = [];
2177
+ if (planSlice.goal !== active.goal) drift.push('goal');
2178
+ if (!sameStringArrays(planSlice.contract_ids, active.contract_ids)) drift.push('contract_ids');
2179
+ if (!sameStringArrays(planSlice.acceptance_criteria, active.acceptance_criteria)) drift.push('acceptance_criteria');
2180
+ if (!sameStringArrays(planSlice.blocked_on, active.blocked_on)) drift.push('blocked_on');
2181
+ if (planSlice.priority !== active.priority) drift.push('priority');
2182
+ if (planSlice.why_now !== active.why_now) drift.push('why_now');
2183
+
2184
+ const expectPlanArrayMirror = (field) => {
2185
+ if (!hasOwn(planSlice, field) || !sameStringArrays(planSlice[field], active[field])) drift.push(field);
2186
+ };
2187
+ const expectPlanStringMirror = (field) => {
2188
+ if (!hasOwn(planSlice, field) || planSlice[field] !== active[field]) drift.push(field);
2189
+ };
2190
+ const expectPlanNumberMirror = (field) => {
2191
+ if (!hasOwn(planSlice, field) || planSlice[field] !== active[field]) drift.push(field);
2192
+ };
2193
+
2194
+ expectPlanArrayMirror('implementation_surfaces');
2195
+ expectPlanArrayMirror('verification_commands');
2196
+ expectPlanArrayMirror('locked_notes');
2197
+ expectPlanArrayMirror('must_fix_findings');
2198
+ expectPlanStringMirror('basis_commit');
2199
+ expectPlanArrayMirror('remaining_contract_ids_before');
2200
+ expectPlanNumberMirror('release_blocker_count_before');
2201
+ expectPlanNumberMirror('high_value_gap_count_before');
2202
+ assert(drift.length === 0, '.agent/active-slice.json must match the selected .agent/plan.json slice across: ' + Array.from(new Set(drift)).join(', '));
2203
+ }
2204
+
2205
+ const currentHead = (() => {
2206
+ try {
2207
+ return childProcess.execSync('git rev-parse HEAD', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
2208
+ } catch {
2209
+ return null;
2210
+ }
2211
+ })();
2212
+
2213
+ if (requiresExactHandoff) {
2214
+ assert(evidence.subject_type === 'selected_slice', '.agent/verification-evidence.json: subject_type must be selected_slice when active slice exact handoff requires verification evidence');
2215
+ assert(evidence.slice_id === active.slice_id, '.agent/verification-evidence.json: slice_id must match .agent/active-slice.json when active slice exact handoff requires verification evidence');
2216
+ assert(evidence.goal === active.goal, '.agent/verification-evidence.json: goal must match .agent/active-slice.json when active slice exact handoff requires verification evidence');
2217
+ assert(sameStringArrays(evidence.contract_ids, active.contract_ids), '.agent/verification-evidence.json: contract_ids must match .agent/active-slice.json when active slice exact handoff requires verification evidence');
2218
+ assert(evidence.basis_commit === active.basis_commit, '.agent/verification-evidence.json: basis_commit must match .agent/active-slice.json when active slice exact handoff requires verification evidence');
2219
+ assert(sameStringArrays(evidence.verification_commands, active.verification_commands), '.agent/verification-evidence.json: verification_commands must match .agent/active-slice.json when active slice exact handoff requires verification evidence');
2220
+ assert(evidence.outcome === 'passed', '.agent/verification-evidence.json: outcome must be passed when active slice exact handoff requires verification evidence');
2221
+ assert(isNonEmptyString(evidence.recorded_at) && !Number.isNaN(Date.parse(evidence.recorded_at)), '.agent/verification-evidence.json: recorded_at must be an ISO-8601 string when active slice exact handoff requires verification evidence');
2222
+ if (currentHead) assert(evidence.head_sha === currentHead, '.agent/verification-evidence.json: head_sha must match current git HEAD when active slice exact handoff requires verification evidence');
2223
+ } else if (evidence.subject_type === 'none') {
2224
+ assert(evidence.slice_id === null, '.agent/verification-evidence.json: slice_id must be null when subject_type is none');
2225
+ assert(evidence.goal === null, '.agent/verification-evidence.json: goal must be null when subject_type is none');
2226
+ assert(evidence.contract_ids.length === 0, '.agent/verification-evidence.json: contract_ids must be empty when subject_type is none');
2227
+ assert(evidence.basis_commit === null, '.agent/verification-evidence.json: basis_commit must be null when subject_type is none');
2228
+ assert(evidence.head_sha === null, '.agent/verification-evidence.json: head_sha must be null when subject_type is none');
2229
+ assert(evidence.verification_commands.length === 0, '.agent/verification-evidence.json: verification_commands must be empty when subject_type is none');
2230
+ assert(evidence.outcome === 'not_recorded', '.agent/verification-evidence.json: outcome must be not_recorded when subject_type is none');
2231
+ assert(evidence.recorded_at === null, '.agent/verification-evidence.json: recorded_at must be null when subject_type is none');
2201
2232
  } else {
2233
+ assert(evidence.outcome === 'passed', '.agent/verification-evidence.json: outcome must be passed when verification evidence is recorded');
2234
+ assert(isNonEmptyStringArray(evidence.verification_commands), '.agent/verification-evidence.json: verification_commands must be a non-empty array when verification evidence is recorded');
2235
+ assert(isNonEmptyString(evidence.recorded_at) && !Number.isNaN(Date.parse(evidence.recorded_at)), '.agent/verification-evidence.json: recorded_at must be an ISO-8601 string when verification evidence is recorded');
2236
+ if (currentHead) assert(evidence.head_sha === currentHead, '.agent/verification-evidence.json: head_sha must match current git HEAD when verification evidence is recorded');
2237
+ if (evidence.subject_type === 'selected_slice') {
2238
+ assert(isNonEmptyString(evidence.slice_id), '.agent/verification-evidence.json: slice_id must be a non-empty string when subject_type is selected_slice');
2239
+ assert(isNonEmptyString(evidence.goal), '.agent/verification-evidence.json: goal must be a non-empty string when subject_type is selected_slice');
2240
+ assert(isNonEmptyString(evidence.basis_commit), '.agent/verification-evidence.json: basis_commit must be a non-empty string when subject_type is selected_slice');
2241
+ } else {
2242
+ assert(evidence.subject_type === 'current_head', '.agent/verification-evidence.json: only current_head or selected_slice may carry recorded verification evidence');
2243
+ }
2244
+ }
2245
+
2246
+ if (!requiresExactHandoff) {
2202
2247
  assert(active.priority === null || active.priority === undefined || (typeof active.priority === 'number' && Number.isFinite(active.priority)), '.agent/active-slice.json: idle priority must be null/undefined or a finite number');
2203
2248
  assert(active.why_now === null || active.why_now === undefined || typeof active.why_now === 'string', '.agent/active-slice.json: idle why_now must be null/undefined or a string');
2204
2249
  }
@@ -2269,6 +2314,7 @@ async function scaffoldCompletionFiles(
2269
2314
  },
2270
2315
  { path: files.planPath, content: `${JSON.stringify(defaultPlan(missionAnchor, { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile }), null, 2)}\n` },
2271
2316
  { path: files.activePath, content: `${JSON.stringify(defaultActiveSlice(missionAnchor, { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile }), null, 2)}\n` },
2317
+ { path: files.verificationEvidencePath, content: `${JSON.stringify(defaultVerificationEvidence(), null, 2)}\n` },
2272
2318
  { path: files.sliceHistoryPath, content: "" },
2273
2319
  { path: files.stopHistoryPath, content: "" },
2274
2320
  ];
@@ -2299,10 +2345,72 @@ function historyCounts(sliceHistory: JsonRecord[], stopHistory: JsonRecord[]) {
2299
2345
  };
2300
2346
  }
2301
2347
 
2348
+ function sameStringArrays(left: string[], right: string[]): boolean {
2349
+ return left.length === right.length && left.every((item, index) => item === right[index]);
2350
+ }
2351
+
2352
+ function hasOwnField(record: JsonRecord | undefined, field: string): boolean {
2353
+ return !!record && Object.prototype.hasOwnProperty.call(record, field);
2354
+ }
2355
+
2356
+ function activeCarriesExactHandoff(active: JsonRecord | undefined): boolean {
2357
+ const status = asString(active?.status);
2358
+ return status === "selected" || status === "in_progress" || status === "committed" || status === "done";
2359
+ }
2360
+
2361
+ function activeSliceContractDriftFields(snapshot: CompletionStateSnapshot): string[] | undefined {
2362
+ const active = snapshot.active;
2363
+ const planSlice = snapshot.activeSlice;
2364
+ const activeId = asString(active?.slice_id);
2365
+ if (!activeId || !planSlice) return undefined;
2366
+ const drift: string[] = [];
2367
+ const expectPlanArrayMirror = (field: string) => {
2368
+ if (!hasOwnField(planSlice, field) || !sameStringArrays(asStringArray(planSlice[field]), asStringArray(active?.[field]))) {
2369
+ drift.push(field);
2370
+ }
2371
+ };
2372
+ const expectPlanStringMirror = (field: string) => {
2373
+ if (!hasOwnField(planSlice, field) || asString(planSlice[field]) !== asString(active?.[field])) {
2374
+ drift.push(field);
2375
+ }
2376
+ };
2377
+ const expectPlanNumberMirror = (field: string) => {
2378
+ if (!hasOwnField(planSlice, field) || asNumber(planSlice[field]) !== asNumber(active?.[field])) {
2379
+ drift.push(field);
2380
+ }
2381
+ };
2382
+ if (asString(planSlice.slice_id) !== activeId) drift.push("slice_id");
2383
+ if (asString(planSlice.goal) !== asString(active?.goal)) drift.push("goal");
2384
+ if (!sameStringArrays(asStringArray(planSlice.contract_ids), asStringArray(active?.contract_ids))) drift.push("contract_ids");
2385
+ if (!sameStringArrays(asStringArray(planSlice.acceptance_criteria), asStringArray(active?.acceptance_criteria))) drift.push("acceptance_criteria");
2386
+ if (!sameStringArrays(asStringArray(planSlice.blocked_on), asStringArray(active?.blocked_on))) drift.push("blocked_on");
2387
+ if (asNumber(planSlice.priority) !== asNumber(active?.priority)) drift.push("priority");
2388
+ if (asString(planSlice.why_now) !== asString(active?.why_now)) drift.push("why_now");
2389
+ expectPlanArrayMirror("implementation_surfaces");
2390
+ expectPlanArrayMirror("verification_commands");
2391
+ expectPlanArrayMirror("locked_notes");
2392
+ expectPlanArrayMirror("must_fix_findings");
2393
+ expectPlanStringMirror("basis_commit");
2394
+ expectPlanArrayMirror("remaining_contract_ids_before");
2395
+ expectPlanNumberMirror("release_blocker_count_before");
2396
+ expectPlanNumberMirror("high_value_gap_count_before");
2397
+ return Array.from(new Set(drift));
2398
+ }
2399
+
2400
+ function activeSliceContractDriftSummary(snapshot: CompletionStateSnapshot): string {
2401
+ const activeId = asString(snapshot.active?.slice_id);
2402
+ if (!activeId) return "unknown";
2403
+ if (!snapshot.activeSlice) return "slice_id (no matching plan slice)";
2404
+ const drift = activeSliceContractDriftFields(snapshot);
2405
+ return drift && drift.length > 0 ? drift.join(", ") : "none";
2406
+ }
2407
+
2302
2408
  function activeSliceMatchesPlan(snapshot: CompletionStateSnapshot): "yes" | "no" | "unknown" {
2303
2409
  const activeId = asString(snapshot.active?.slice_id);
2304
2410
  if (!activeId) return "unknown";
2305
- return snapshot.activeSlice ? "yes" : "no";
2411
+ const drift = activeSliceContractDriftFields(snapshot);
2412
+ if (!snapshot.activeSlice || drift === undefined) return "no";
2413
+ return drift.length === 0 ? "yes" : "no";
2306
2414
  }
2307
2415
 
2308
2416
  function handoffSnapshotState(active: JsonRecord | undefined): "present" | "missing_or_unclear" {
@@ -2322,7 +2430,7 @@ function handoffSnapshotState(active: JsonRecord | undefined): "present" | "miss
2322
2430
  active?.release_blocker_count_before,
2323
2431
  active?.high_value_gap_count_before,
2324
2432
  ];
2325
- return exactArrays.every((items) => items.length > 0) && required.every((value) => value !== undefined && value !== null)
2433
+ return activeCarriesExactHandoff(active) && exactArrays.every((items) => items.length > 0) && required.every((value) => value !== undefined && value !== null)
2326
2434
  ? "present"
2327
2435
  : "missing_or_unclear";
2328
2436
  }
@@ -2334,9 +2442,12 @@ function buildSystemReminder(snapshot: CompletionStateSnapshot, sliceHistory: Js
2334
2442
  const activePriority = asNumber(snapshot.active?.priority);
2335
2443
  const activeWhyNow = asString(snapshot.active?.why_now);
2336
2444
  const nextRole = asString(snapshot.state?.next_mandatory_role);
2445
+ const exactActiveContract = activeCarriesExactHandoff(snapshot.active);
2446
+ const activeContractDrift = activeSliceContractDriftSummary(snapshot);
2447
+ const evidence = verificationEvidenceContext(snapshot);
2337
2448
  const lines = [
2338
2449
  "Completion workflow detected.",
2339
- "Canonical truth lives in .agent/state.json, .agent/plan.json, .agent/active-slice.json, .agent/slice-history.jsonl, and .agent/stop-check-history.jsonl.",
2450
+ "Canonical truth lives in .agent/state.json, .agent/plan.json, .agent/active-slice.json, .agent/slice-history.jsonl, .agent/stop-check-history.jsonl, and .agent/verification-evidence.json.",
2340
2451
  `Mission anchor: ${asString(snapshot.state?.mission_anchor) ?? "(unknown)"}`,
2341
2452
  `Task type: ${currentTaskType(snapshot) ?? "(missing)"}`,
2342
2453
  `Evaluation profile: ${currentEvaluationProfile(snapshot) ?? "(missing)"}`,
@@ -2354,10 +2465,20 @@ function buildSystemReminder(snapshot: CompletionStateSnapshot, sliceHistory: Js
2354
2465
  "If canonical state is stale, invalid, ambiguous, or missing, route to completion-regrounder.",
2355
2466
  "When recovering from compaction, prefer a deterministic restart from canonical files over conversational inference.",
2356
2467
  ];
2468
+ if (exactActiveContract) {
2469
+ lines.push("Selected/in-progress/committed/done .agent/active-slice.json is the canonical implementation contract.");
2470
+ lines.push(`Active slice contract drift: ${activeContractDrift}`);
2471
+ }
2357
2472
  if (activePriority !== undefined) lines.push(`Active slice priority: ${activePriority}`);
2358
2473
  if (activeWhyNow) lines.push(`Active slice why_now: ${activeWhyNow}`);
2359
2474
  if (implementationSurfaces.length > 0) lines.push(`Active implementation surfaces: ${implementationSurfaces.join(", ")}`);
2360
2475
  if (verificationCommands.length > 0) lines.push(`Active verification commands: ${verificationCommands.join(" | ")}`);
2476
+ lines.push(`Verification evidence artifact: ${evidence.path} (${evidence.status})`);
2477
+ if (evidence.subjectType) lines.push(`Verification evidence subject: ${evidence.subjectType}`);
2478
+ if (evidence.outcome) lines.push(`Verification evidence outcome: ${evidence.outcome}`);
2479
+ if (evidence.recordedAt) lines.push(`Verification evidence recorded_at: ${evidence.recordedAt}`);
2480
+ if (evidence.verificationCommands.length > 0) lines.push(`Verification evidence commands: ${evidence.verificationCommands.join(" | ")}`);
2481
+ lines.push(`Verification evidence summary: ${evidence.summary}`);
2361
2482
  if (isRubricEvaluationRole(nextRole)) lines.push(buildEvaluationRoleReminderText(snapshot, nextRole));
2362
2483
  return lines.join(" ");
2363
2484
  }
@@ -2374,26 +2495,43 @@ function buildPostCompactionDriverInstructions(snapshot: CompletionStateSnapshot
2374
2495
  const verificationCommands = asStringArray(snapshot.active?.verification_commands);
2375
2496
  const activePriority = asNumber(snapshot.active?.priority);
2376
2497
  const activeWhyNow = asString(snapshot.active?.why_now);
2498
+ const exactActiveContract = activeCarriesExactHandoff(snapshot.active);
2499
+ const activeContractDrift = activeSliceContractDriftSummary(snapshot);
2500
+ const evidence = verificationEvidenceContext(snapshot);
2377
2501
  const lines = [
2378
2502
  "POST-COMPACTION RECOVERY MODE is active.",
2379
2503
  `Compaction marker time: ${markerAt}`,
2380
2504
  "Treat the previous conversation as lossy continuity support only.",
2381
- "Before taking any substantive action, re-read .agent/state.json, .agent/plan.json, .agent/active-slice.json, .agent/slice-history.jsonl, and .agent/stop-check-history.jsonl from disk.",
2505
+ "Before taking any substantive action, re-read .agent/state.json, .agent/plan.json, .agent/active-slice.json, .agent/slice-history.jsonl, .agent/stop-check-history.jsonl, and .agent/verification-evidence.json from disk.",
2382
2506
  `Canonical task_type is currently: ${taskType}`,
2383
2507
  `Canonical evaluation_profile is currently: ${evaluationProfile}`,
2384
2508
  `Canonical next mandatory role is currently: ${nextRole}`,
2385
2509
  `Canonical next mandatory action is currently: ${nextAction}`,
2386
2510
  `Canonical continuation policy is currently: ${continuation}`,
2387
2511
  `Canonical active slice is currently: ${activeSliceId}`,
2512
+ `Canonical verification evidence artifact is currently: ${evidence.path} (${evidence.status})`,
2388
2513
  "Do not trust pre-compaction memory over canonical files.",
2389
2514
  "If the canonical state is ambiguous, inconsistent, missing, or stale after re-reading it, your first mandatory action is to dispatch completion-regrounder rather than guessing.",
2390
2515
  "If continuation_policy == continue and canonical state is coherent, continue dispatching the mandatory role directly without asking the user whether to continue.",
2391
2516
  "If you are about to implement after compaction, confirm the active slice snapshot still matches .agent/plan.json before doing any work.",
2392
2517
  ];
2518
+ if (exactActiveContract) {
2519
+ lines.push("For selected/in-progress/committed/done slices, .agent/active-slice.json is the canonical implementation contract.");
2520
+ lines.push(`Canonical active-slice contract drift is currently: ${activeContractDrift}`);
2521
+ }
2393
2522
  if (activePriority !== undefined) lines.push(`Canonical active-slice priority is currently: ${activePriority}`);
2394
2523
  if (activeWhyNow) lines.push(`Canonical active-slice why_now is currently: ${activeWhyNow}`);
2395
2524
  if (implementationSurfaces.length > 0) lines.push(`Canonical implementation surfaces are currently: ${implementationSurfaces.join(", ")}`);
2396
2525
  if (verificationCommands.length > 0) lines.push(`Canonical verification commands are currently: ${verificationCommands.join(" | ")}`);
2526
+ if (evidence.subjectType) lines.push(`Canonical verification evidence subject is currently: ${evidence.subjectType}`);
2527
+ if (evidence.outcome) lines.push(`Canonical verification evidence outcome is currently: ${evidence.outcome}`);
2528
+ if (evidence.recordedAt) lines.push(`Canonical verification evidence recorded_at is currently: ${evidence.recordedAt}`);
2529
+ if (evidence.headSha) lines.push(`Canonical verification evidence head_sha is currently: ${evidence.headSha}`);
2530
+ if (evidence.basisCommit) lines.push(`Canonical verification evidence basis_commit is currently: ${evidence.basisCommit}`);
2531
+ if (evidence.verificationCommands.length > 0) {
2532
+ lines.push(`Canonical verification evidence commands are currently: ${evidence.verificationCommands.join(" | ")}`);
2533
+ }
2534
+ lines.push(`Canonical verification evidence summary is currently: ${evidence.summary}`);
2397
2535
  if (isRubricEvaluationRole(nextRole)) lines.push(buildEvaluationRoleReminderText(snapshot, nextRole));
2398
2536
  return lines.join(" ");
2399
2537
  }
@@ -2474,6 +2612,8 @@ function buildResumeCapsule(snapshot: CompletionStateSnapshot, sliceHistory: Jso
2474
2612
  const implementationSurfaces = asStringArray(snapshot.active?.implementation_surfaces);
2475
2613
  const verificationCommands = asStringArray(snapshot.active?.verification_commands);
2476
2614
  const remainingBefore = asStringArray(snapshot.active?.remaining_contract_ids_before);
2615
+ const activeContractDrift = activeSliceContractDriftSummary(snapshot);
2616
+ const evidence = verificationEvidenceContext(snapshot);
2477
2617
  const lines = [
2478
2618
  "Authoritative completion resume capsule:",
2479
2619
  "",
@@ -2490,9 +2630,23 @@ function buildResumeCapsule(snapshot: CompletionStateSnapshot, sliceHistory: Jso
2490
2630
  `remaining_slice_count: ${remainingSliceCount(snapshot.plan)}`,
2491
2631
  `remaining_stop_judges: ${asNumber(snapshot.state?.remaining_stop_judges) ?? "(unknown)"}`,
2492
2632
  `active_slice_matches_plan: ${activeSliceMatchesPlan(snapshot)}`,
2633
+ `active_slice_contract_drift_fields: ${activeContractDrift}`,
2493
2634
  `implementer_handoff_snapshot: ${handoffSnapshotState(snapshot.active)}`,
2494
2635
  `history_counts: reviewed=${history.reviewed}, audited=${history.audited}, accepted=${history.accepted}, reopened=${history.reopened}, judgments=${history.judgments}`,
2495
2636
  "",
2637
+ "verification_evidence:",
2638
+ `- path: ${evidence.path}`,
2639
+ `- status: ${evidence.status}`,
2640
+ `- subject_type: ${evidence.subjectType ?? "(missing)"}`,
2641
+ `- slice_id: ${evidence.sliceId ?? "(none)"}`,
2642
+ `- contract_ids: ${evidence.contractIds.length > 0 ? evidence.contractIds.join(", ") : "(none)"}`,
2643
+ `- outcome: ${evidence.outcome ?? "(missing)"}`,
2644
+ `- recorded_at: ${evidence.recordedAt ?? "(missing)"}`,
2645
+ `- head_sha: ${evidence.headSha ?? "(missing)"}`,
2646
+ `- basis_commit: ${evidence.basisCommit ?? "(missing)"}`,
2647
+ `- verification_commands: ${evidence.verificationCommands.length > 0 ? evidence.verificationCommands.join(" | ") : "(none)"}`,
2648
+ `- summary: ${evidence.summary}`,
2649
+ "",
2496
2650
  "active_slice:",
2497
2651
  `- slice_id: ${asString(snapshot.active?.slice_id) ?? asString(snapshot.activeSlice?.slice_id) ?? "(none)"}`,
2498
2652
  `- status: ${asString(snapshot.active?.status) ?? asString(snapshot.activeSlice?.status) ?? "unknown"}`,
@@ -2517,14 +2671,16 @@ function buildResumeCapsule(snapshot: CompletionStateSnapshot, sliceHistory: Jso
2517
2671
  "",
2518
2672
  "Rules:",
2519
2673
  "- Treat this block as continuity support derived from canonical .agent state.",
2520
- "- Preserve exact slice_id, contract_ids, acceptance criteria, priority, why_now, implementation surfaces, verification commands, locked notes, and must-fix findings where still true.",
2521
- "- After compaction, re-read .agent/state.json, .agent/plan.json, .agent/active-slice.json, .agent/slice-history.jsonl, and .agent/stop-check-history.jsonl before resuming long-running completion work.",
2674
+ "- For selected/in-progress/committed/done slices, .agent/active-slice.json is the canonical implementation contract and the selected plan slice must mirror it exactly.",
2675
+ "- Preserve exact slice_id, goal, contract_ids, acceptance criteria, blocked_on, priority, why_now, implementation surfaces, verification commands, locked notes, must-fix findings, basis_commit, and before-slice counters where still true.",
2676
+ "- When populated, .agent/verification-evidence.json is the durable canonical verification record for the selected slice or current HEAD and should be consumed instead of temp-only artifacts or conversational summaries.",
2677
+ "- After compaction, re-read .agent/state.json, .agent/plan.json, .agent/active-slice.json, .agent/slice-history.jsonl, .agent/stop-check-history.jsonl, and .agent/verification-evidence.json before resuming long-running completion work.",
2522
2678
  "- Invoke completion-regrounder before continuing when requires_reground is true or unknown.",
2523
2679
  "- Invoke completion-regrounder before continuing when next_mandatory_role or next_mandatory_action is unknown or ambiguous.",
2524
- "- Invoke completion-regrounder before continuing when active_slice_matches_plan is no or implementer_handoff_snapshot is missing_or_unclear.",
2680
+ "- Invoke completion-regrounder before continuing when active_slice_matches_plan is no, active_slice_contract_drift_fields is not none, or implementer_handoff_snapshot is missing_or_unclear.",
2525
2681
  "- If continuation_policy is continue, do not stop after a slice or ask whether to continue. Dispatch the next mandatory role directly.",
2526
2682
  "- Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.",
2527
- "- If you are completion-implementer after compaction, resume from the canonical active-slice handoff instead of asking the user to resend the original caller payload.",
2683
+ "- If you are completion-implementer after compaction, resume from the canonical active-slice implementation contract instead of asking the user to resend the original caller payload.",
2528
2684
  "- Do not replace canonical .agent state with summary inference.",
2529
2685
  "</completion-state>",
2530
2686
  );
@@ -2949,7 +3105,7 @@ function formatInlineRunningText(theme: any, lines: string[], options?: { primar
2949
3105
  continue;
2950
3106
  }
2951
3107
  if (line.startsWith("activity:")) {
2952
- text += theme.fg(line.includes("stalled") ? "warning" : "dim", line);
3108
+ text += line.includes("stalled") ? theme.fg("warning", line) : line;
2953
3109
  continue;
2954
3110
  }
2955
3111
  if (line === "recent tools:") {
@@ -2961,7 +3117,7 @@ function formatInlineRunningText(theme: any, lines: string[], options?: { primar
2961
3117
  continue;
2962
3118
  }
2963
3119
  if (line.startsWith("elapsed:")) {
2964
- text += theme.fg("dim", line);
3120
+ text += line;
2965
3121
  continue;
2966
3122
  }
2967
3123
  if (line.startsWith("assistant:")) {
@@ -2973,7 +3129,7 @@ function formatInlineRunningText(theme: any, lines: string[], options?: { primar
2973
3129
  continue;
2974
3130
  }
2975
3131
  if (line.startsWith("rationale:") || line.startsWith("state-delta:")) {
2976
- text += theme.fg("dim", line);
3132
+ text += line;
2977
3133
  continue;
2978
3134
  }
2979
3135
  text += theme.fg("muted", line);
@@ -3193,11 +3349,11 @@ function completionKickoff(
3193
3349
  : intent === "refocus" && missionAnchor
3194
3350
  ? `Updated canonical mission anchor:\n${missionAnchor}\n\nWorkflow intent:\n- The user explicitly refocused the workflow before this kickoff.\n- Re-read canonical .agent/** state and continue from the refocused mission.\n\n`
3195
3351
  : "";
3196
- return `/skill:completion-protocol Start or continue the completion workflow for this repo.\n\nBefore acting, read:\n- ${SKILL_PATH}\n- ${REFERENCE_PATH}\n\nCanonical routing profile:\n- task_type: ${taskType}\n- evaluation_profile: ${evaluationProfile}\n\nUser goal:\n${goal}\n\n${intentBlock}Driver instructions:\n- Canonical truth is in .agent/**. Re-read .agent/state.json, .agent/plan.json, and .agent/active-slice.json before acting when they exist.\n- If tracked completion contract files are missing or onboarding is required, invoke completion_role with role completion-bootstrapper.\n- Otherwise follow the mandatory dispatch rules from completion-protocol.\n- Use completion_role for all completion-* role work. Do not directly implement tracked product changes yourself.\n- Continue dispatching mandatory roles while continuation_policy == continue.\n- Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.`;
3352
+ return `/skill:completion-protocol Start or continue the completion workflow for this repo.\n\nBefore acting, read:\n- ${SKILL_PATH}\n- ${REFERENCE_PATH}\n\nCanonical routing profile:\n- task_type: ${taskType}\n- evaluation_profile: ${evaluationProfile}\n\nUser goal:\n${goal}\n\n${intentBlock}Driver instructions:\n- Canonical truth is in .agent/**. Re-read .agent/state.json, .agent/plan.json, .agent/active-slice.json, and .agent/verification-evidence.json before acting when they exist.\n- If tracked completion contract files are missing or onboarding is required, invoke completion_role with role completion-bootstrapper.\n- Otherwise follow the mandatory dispatch rules from completion-protocol.\n- For selected, in-progress, committed, or done slices, treat .agent/active-slice.json as the canonical implementation contract and route to completion-regrounder if it drifts from the selected plan slice or the exact handoff is unclear.\n- Consume .agent/verification-evidence.json instead of temp-only verification summaries when it is populated.\n- Use completion_role for all completion-* role work. Do not directly implement tracked product changes yourself.\n- Continue dispatching mandatory roles while continuation_policy == continue.\n- Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.`;
3197
3353
  }
3198
3354
 
3199
3355
  function completionResumePrompt(taskType: string, evaluationProfile: string): string {
3200
- return `/skill:completion-protocol Resume the completion workflow from canonical state.\n\nBefore acting, read:\n- ${SKILL_PATH}\n- ${REFERENCE_PATH}\n\nCanonical routing profile:\n- task_type: ${taskType}\n- evaluation_profile: ${evaluationProfile}\n\nResume instructions:\n- Re-read .agent/state.json, .agent/plan.json, and .agent/active-slice.json before acting.\n- If canonical state is missing, invalid, contradictory, stale, or ambiguous, route to completion-regrounder first.\n- Continue from next_mandatory_role and next_mandatory_action.\n- Use completion_role for all completion-* role work.\n- Continue dispatching mandatory roles while continuation_policy == continue.\n- Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.`;
3356
+ return `/skill:completion-protocol Resume the completion workflow from canonical state.\n\nBefore acting, read:\n- ${SKILL_PATH}\n- ${REFERENCE_PATH}\n\nCanonical routing profile:\n- task_type: ${taskType}\n- evaluation_profile: ${evaluationProfile}\n\nResume instructions:\n- Re-read .agent/state.json, .agent/plan.json, .agent/active-slice.json, and .agent/verification-evidence.json before acting.\n- If canonical state is missing, invalid, contradictory, stale, or ambiguous, route to completion-regrounder first.\n- For selected, in-progress, committed, or done slices, treat .agent/active-slice.json as the canonical implementation contract and route to completion-regrounder if it drifts from the selected plan slice or the exact handoff is unclear.\n- Consume .agent/verification-evidence.json instead of temp-only verification summaries when it is populated.\n- Continue from next_mandatory_role and next_mandatory_action.\n- Use completion_role for all completion-* role work.\n- Continue dispatching mandatory roles while continuation_policy == continue.\n- Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.`;
3201
3357
  }
3202
3358
 
3203
3359
  export default function completionExtension(pi: ExtensionAPI) {
@@ -3621,13 +3777,11 @@ export default function completionExtension(pi: ExtensionAPI) {
3621
3777
  );
3622
3778
  return;
3623
3779
  }
3624
- const decision = await confirmContextProposal(ctx, proposal, projectName, {
3780
+ const decision = await confirmContextProposal(ctx, proposal, {
3625
3781
  title: "Start a completion workflow from the recent discussion?",
3626
- editorPrompt:
3627
- "Start a completion workflow from the recent discussion?\n\nEdit the proposed mission, scope, constraints, and acceptance details below.",
3628
3782
  });
3629
3783
  if (!decision) {
3630
- emitCommandText(ctx, "Cancelled recent-discussion workflow proposal", "info");
3784
+ emitCommandText(ctx, buildCookCancellationMessage("Cancelled recent-discussion workflow proposal"), "info");
3631
3785
  return;
3632
3786
  }
3633
3787
  goal = decision.goalText;
@@ -3635,14 +3789,12 @@ export default function completionExtension(pi: ExtensionAPI) {
3635
3789
  kickoffAnalysis = decision.analysis;
3636
3790
  } else {
3637
3791
  const proposal = await buildGoalAnchoredContextProposal(ctx, goal, projectName);
3638
- const decision = await confirmContextProposal(ctx, proposal, projectName, {
3792
+ const decision = await confirmContextProposal(ctx, proposal, {
3639
3793
  title: "Start a completion workflow from this goal?",
3640
3794
  nonInteractiveBehavior: "accept",
3641
- editorPrompt:
3642
- "Start a completion workflow from this goal?\n\nEdit the proposed mission, scope, constraints, and acceptance details below.",
3643
3795
  });
3644
3796
  if (!decision) {
3645
- emitCommandText(ctx, "Cancelled workflow startup proposal", "info");
3797
+ emitCommandText(ctx, buildCookCancellationMessage("Cancelled workflow startup proposal"), "info");
3646
3798
  return;
3647
3799
  }
3648
3800
  goal = decision.goalText;
@@ -3681,13 +3833,11 @@ export default function completionExtension(pi: ExtensionAPI) {
3681
3833
  );
3682
3834
  return;
3683
3835
  }
3684
- const decision = await confirmContextProposal(ctx, proposal, projectName, {
3836
+ const decision = await confirmContextProposal(ctx, proposal, {
3685
3837
  title: "The previous completion workflow is done. Start the next workflow round from the recent discussion?",
3686
- editorPrompt:
3687
- "The previous completion workflow is done. Start the next workflow round from the recent discussion?\n\nEdit the proposed mission, scope, constraints, and acceptance details below.",
3688
3838
  });
3689
3839
  if (!decision) {
3690
- emitCommandText(ctx, "Cancelled next workflow round proposal", "info");
3840
+ emitCommandText(ctx, buildCookCancellationMessage("Cancelled next workflow round proposal"), "info");
3691
3841
  return;
3692
3842
  }
3693
3843
  goal = decision.goalText;
@@ -3710,7 +3860,9 @@ export default function completionExtension(pi: ExtensionAPI) {
3710
3860
  current_phase: asString(snapshot.state?.current_phase) ?? null,
3711
3861
  next_mandatory_role: asString(snapshot.state?.next_mandatory_role) ?? null,
3712
3862
  });
3713
- await queueCompletionDriverPrompt(pi, ctx, rootKey, fingerprint, resumePrompt, "resume");
3863
+ const resumeKind =
3864
+ shouldTestAutoContinueOnSessionStart() && completionTestAutoContinuePromptPath() ? "auto-resume" : "resume";
3865
+ await queueCompletionDriverPrompt(pi, ctx, rootKey, fingerprint, resumePrompt, resumeKind);
3714
3866
  return;
3715
3867
  }
3716
3868
  }
@@ -3719,14 +3871,12 @@ export default function completionExtension(pi: ExtensionAPI) {
3719
3871
  if (workflowDone) {
3720
3872
  const projectName = path.basename(snapshot.files.root);
3721
3873
  const proposal = await buildGoalAnchoredContextProposal(ctx, goal, projectName);
3722
- const decision = await confirmContextProposal(ctx, proposal, projectName, {
3874
+ const decision = await confirmContextProposal(ctx, proposal, {
3723
3875
  title: "Start the next workflow round from this goal?",
3724
3876
  nonInteractiveBehavior: "accept",
3725
- editorPrompt:
3726
- "Start the next workflow round from this goal?\n\nEdit the proposed mission, scope, constraints, and acceptance details below.",
3727
3877
  });
3728
3878
  if (!decision) {
3729
- emitCommandText(ctx, "Cancelled next workflow round proposal", "info");
3879
+ emitCommandText(ctx, buildCookCancellationMessage("Cancelled next workflow round proposal"), "info");
3730
3880
  return;
3731
3881
  }
3732
3882
  goal = decision.goalText;
@@ -3738,7 +3888,7 @@ export default function completionExtension(pi: ExtensionAPI) {
3738
3888
  } else {
3739
3889
  const decision = await confirmExistingWorkflowGoal(ctx, snapshot, goal);
3740
3890
  if (!decision) {
3741
- emitCommandText(ctx, "Cancelled existing workflow confirmation", "info");
3891
+ emitCommandText(ctx, buildCookCancellationMessage("Cancelled existing workflow confirmation"), "info");
3742
3892
  return;
3743
3893
  }
3744
3894
  kickoffIntent = decision.action;
@@ -3746,14 +3896,12 @@ export default function completionExtension(pi: ExtensionAPI) {
3746
3896
  if (decision.action === "refocus") {
3747
3897
  const projectName = path.basename(snapshot.files.root);
3748
3898
  const proposal = await buildGoalAnchoredContextProposal(ctx, goal, projectName);
3749
- const proposalDecision = await confirmContextProposal(ctx, proposal, projectName, {
3899
+ const proposalDecision = await confirmContextProposal(ctx, proposal, {
3750
3900
  title: "Start the replacement workflow from this goal?",
3751
3901
  nonInteractiveBehavior: "accept",
3752
- editorPrompt:
3753
- "Start the replacement workflow from this goal?\n\nEdit the proposed mission, scope, constraints, and acceptance details below.",
3754
3902
  });
3755
3903
  if (!proposalDecision) {
3756
- emitCommandText(ctx, "Cancelled replacement workflow proposal", "info");
3904
+ emitCommandText(ctx, buildCookCancellationMessage("Cancelled replacement workflow proposal"), "info");
3757
3905
  return;
3758
3906
  }
3759
3907
  goal = proposalDecision.goalText;