@linimin/pi-letscook 0.1.32 → 0.1.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +33 -11
- package/agents/completion-implementer.md +11 -2
- package/extensions/completion/index.ts +426 -262
- package/extensions/completion/role-reporting.js +107 -20
- package/package.json +2 -1
- package/scripts/active-slice-contract-test.sh +242 -0
- package/scripts/canonical-evidence-artifact-test.sh +348 -0
- package/scripts/context-proposal-test.sh +50 -49
- package/scripts/evaluator-calibration-test.sh +363 -0
- package/scripts/refocus-test.sh +31 -0
- package/scripts/release-check.sh +5 -1
- package/scripts/smoke-test.sh +56 -1
- package/skills/completion-protocol/SKILL.md +4 -1
- package/skills/completion-protocol/references/completion.md +24 -0
|
@@ -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" | "
|
|
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 {
|
|
@@ -203,7 +204,7 @@ class StartupAnalystOverlay extends Container {
|
|
|
203
204
|
|
|
204
205
|
private updateDisplay(): void {
|
|
205
206
|
this.title.setText(this.theme.fg("accent", this.theme.bold("/cook proposal analyst")));
|
|
206
|
-
this.body.setText(this.theme
|
|
207
|
+
this.body.setText(formatInlineRunningText(this.theme, this.lines, { primaryAssistant: true }));
|
|
207
208
|
this.footer.setText(this.theme.fg("muted", "Esc cancel • This analysis runs before /cook writes canonical workflow state"));
|
|
208
209
|
}
|
|
209
210
|
|
|
@@ -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
|
|
295
|
-
|
|
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
|
|
300
|
-
|
|
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
|
-
|
|
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" | "
|
|
475
|
+
function completionTestContextProposalActionOverride(): "accept" | "cancel" | undefined {
|
|
517
476
|
const raw = process.env.PI_COMPLETION_CONTEXT_PROPOSAL_ACTION?.trim().toLowerCase();
|
|
518
|
-
return raw === "accept" || raw === "
|
|
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 === "
|
|
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:
|
|
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(
|
|
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) =>
|
|
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(
|
|
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
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
|
@@ -1946,10 +1868,6 @@ function deriveMissionAnchor(rawGoal: string, projectName: string): string {
|
|
|
1946
1868
|
.replace(/\bwith docs\b/gi, "with docs parity")
|
|
1947
1869
|
.trim();
|
|
1948
1870
|
|
|
1949
|
-
if (mission.length > 120) {
|
|
1950
|
-
mission = `${mission.slice(0, 117).trimEnd()}...`;
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
1871
|
if (!/[.!?。!?]$/u.test(mission)) mission += ".";
|
|
1954
1872
|
return mission;
|
|
1955
1873
|
}
|
|
@@ -2047,8 +1965,25 @@ function defaultActiveSlice(
|
|
|
2047
1965
|
};
|
|
2048
1966
|
}
|
|
2049
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
|
+
|
|
2050
1985
|
function buildAgentReadme(projectName: string): string {
|
|
2051
|
-
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`;
|
|
2052
1987
|
}
|
|
2053
1988
|
|
|
2054
1989
|
function buildMission(projectName: string, missionAnchor: string): string {
|
|
@@ -2074,11 +2009,13 @@ for file in \
|
|
|
2074
2009
|
.agent/verify_completion_control_plane.sh \
|
|
2075
2010
|
.agent/state.json \
|
|
2076
2011
|
.agent/plan.json \
|
|
2077
|
-
.agent/active-slice.json
|
|
2012
|
+
.agent/active-slice.json \
|
|
2013
|
+
.agent/verification-evidence.json; do
|
|
2078
2014
|
[[ -e "$file" ]] || { echo "missing required file: $file"; exit 1; }
|
|
2079
2015
|
done
|
|
2080
2016
|
|
|
2081
2017
|
node <<'NODE'
|
|
2018
|
+
const childProcess = require('node:child_process');
|
|
2082
2019
|
const fs = require('node:fs');
|
|
2083
2020
|
|
|
2084
2021
|
const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
@@ -2101,8 +2038,10 @@ const requireKeys = (object, required, label) => {
|
|
|
2101
2038
|
assert(Object.prototype.hasOwnProperty.call(object, key), label + ': missing required field: ' + key);
|
|
2102
2039
|
}
|
|
2103
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]);
|
|
2104
2043
|
|
|
2105
|
-
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']) {
|
|
2106
2045
|
readJson(file);
|
|
2107
2046
|
}
|
|
2108
2047
|
|
|
@@ -2110,11 +2049,13 @@ const profile = readJson('.agent/profile.json');
|
|
|
2110
2049
|
const state = readJson('.agent/state.json');
|
|
2111
2050
|
const plan = readJson('.agent/plan.json');
|
|
2112
2051
|
const active = readJson('.agent/active-slice.json');
|
|
2052
|
+
const evidence = readJson('.agent/verification-evidence.json');
|
|
2113
2053
|
|
|
2114
2054
|
assert(isObject(profile), '.agent/profile.json must be an object');
|
|
2115
2055
|
assert(isObject(state), '.agent/state.json must be an object');
|
|
2116
2056
|
assert(isObject(plan), '.agent/plan.json must be an object');
|
|
2117
2057
|
assert(isObject(active), '.agent/active-slice.json must be an object');
|
|
2058
|
+
assert(isObject(evidence), '.agent/verification-evidence.json must be an object');
|
|
2118
2059
|
|
|
2119
2060
|
const requiredProfile = ['schema_version', 'protocol_id', 'project_name', 'required_stop_judges', 'priority_policy_id', 'task_type', 'evaluation_profile', 'docs_surfaces'];
|
|
2120
2061
|
requireKeys(profile, requiredProfile, '.agent/profile.json');
|
|
@@ -2145,6 +2086,8 @@ assert(isStringArray(state.release_blocker_ids), '.agent/state.json: release_blo
|
|
|
2145
2086
|
|
|
2146
2087
|
const requiredPlan = ['schema_version', 'mission_anchor', 'task_type', 'evaluation_profile', 'last_reground_at', 'plan_basis', 'candidate_slices'];
|
|
2147
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];
|
|
2148
2091
|
const sliceStatuses = ['planned', 'selected', 'in_progress', 'blocked', 'done', 'cancelled'];
|
|
2149
2092
|
requireKeys(plan, requiredPlan, '.agent/plan.json');
|
|
2150
2093
|
hasOnlyKeys(plan, requiredPlan, '.agent/plan.json');
|
|
@@ -2155,7 +2098,7 @@ for (const [index, slice] of plan.candidate_slices.entries()) {
|
|
|
2155
2098
|
const label = '.agent/plan.json candidate_slices[' + index + ']';
|
|
2156
2099
|
assert(isObject(slice), label + ' must be an object');
|
|
2157
2100
|
requireKeys(slice, requiredSlice, label);
|
|
2158
|
-
hasOnlyKeys(slice,
|
|
2101
|
+
hasOnlyKeys(slice, allowedSlice, label);
|
|
2159
2102
|
assert(isString(slice.slice_id) && slice.slice_id.length > 0, label + ': slice_id must be a non-empty string');
|
|
2160
2103
|
assert(isString(slice.goal) && slice.goal.length > 0, label + ': goal must be a non-empty string');
|
|
2161
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');
|
|
@@ -2165,6 +2108,14 @@ for (const [index, slice] of plan.candidate_slices.entries()) {
|
|
|
2165
2108
|
assert(isString(slice.why_now) && slice.why_now.length > 0, label + ': why_now must be a non-empty string');
|
|
2166
2109
|
assert(isStringArray(slice.blocked_on), label + ': blocked_on must be an array of strings');
|
|
2167
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');
|
|
2168
2119
|
}
|
|
2169
2120
|
|
|
2170
2121
|
const isNonEmptyStringArray = (value) => Array.isArray(value) && value.length > 0 && value.every((item) => isNonEmptyString(item));
|
|
@@ -2185,6 +2136,23 @@ assert(isStringArray(active.implementation_surfaces), '.agent/active-slice.json:
|
|
|
2185
2136
|
assert(isStringArray(active.verification_commands), '.agent/active-slice.json: verification_commands must be an array of strings');
|
|
2186
2137
|
assert(isStringArray(active.remaining_contract_ids_before), '.agent/active-slice.json: remaining_contract_ids_before must be an array of strings');
|
|
2187
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
|
+
|
|
2188
2156
|
assert(state.task_type === profile.task_type, '.agent/state.json: task_type must match .agent/profile.json');
|
|
2189
2157
|
assert(plan.task_type === profile.task_type, '.agent/plan.json: task_type must match .agent/profile.json');
|
|
2190
2158
|
assert(active.task_type === profile.task_type, '.agent/active-slice.json: task_type must match .agent/profile.json');
|
|
@@ -2202,7 +2170,80 @@ if (requiresExactHandoff) {
|
|
|
2202
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');
|
|
2203
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');
|
|
2204
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');
|
|
2205
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) {
|
|
2206
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');
|
|
2207
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');
|
|
2208
2249
|
}
|
|
@@ -2273,6 +2314,7 @@ async function scaffoldCompletionFiles(
|
|
|
2273
2314
|
},
|
|
2274
2315
|
{ path: files.planPath, content: `${JSON.stringify(defaultPlan(missionAnchor, { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile }), null, 2)}\n` },
|
|
2275
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` },
|
|
2276
2318
|
{ path: files.sliceHistoryPath, content: "" },
|
|
2277
2319
|
{ path: files.stopHistoryPath, content: "" },
|
|
2278
2320
|
];
|
|
@@ -2303,10 +2345,72 @@ function historyCounts(sliceHistory: JsonRecord[], stopHistory: JsonRecord[]) {
|
|
|
2303
2345
|
};
|
|
2304
2346
|
}
|
|
2305
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
|
+
|
|
2306
2408
|
function activeSliceMatchesPlan(snapshot: CompletionStateSnapshot): "yes" | "no" | "unknown" {
|
|
2307
2409
|
const activeId = asString(snapshot.active?.slice_id);
|
|
2308
2410
|
if (!activeId) return "unknown";
|
|
2309
|
-
|
|
2411
|
+
const drift = activeSliceContractDriftFields(snapshot);
|
|
2412
|
+
if (!snapshot.activeSlice || drift === undefined) return "no";
|
|
2413
|
+
return drift.length === 0 ? "yes" : "no";
|
|
2310
2414
|
}
|
|
2311
2415
|
|
|
2312
2416
|
function handoffSnapshotState(active: JsonRecord | undefined): "present" | "missing_or_unclear" {
|
|
@@ -2326,7 +2430,7 @@ function handoffSnapshotState(active: JsonRecord | undefined): "present" | "miss
|
|
|
2326
2430
|
active?.release_blocker_count_before,
|
|
2327
2431
|
active?.high_value_gap_count_before,
|
|
2328
2432
|
];
|
|
2329
|
-
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)
|
|
2330
2434
|
? "present"
|
|
2331
2435
|
: "missing_or_unclear";
|
|
2332
2436
|
}
|
|
@@ -2338,9 +2442,12 @@ function buildSystemReminder(snapshot: CompletionStateSnapshot, sliceHistory: Js
|
|
|
2338
2442
|
const activePriority = asNumber(snapshot.active?.priority);
|
|
2339
2443
|
const activeWhyNow = asString(snapshot.active?.why_now);
|
|
2340
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);
|
|
2341
2448
|
const lines = [
|
|
2342
2449
|
"Completion workflow detected.",
|
|
2343
|
-
"Canonical truth lives in .agent/state.json, .agent/plan.json, .agent/active-slice.json, .agent/slice-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.",
|
|
2344
2451
|
`Mission anchor: ${asString(snapshot.state?.mission_anchor) ?? "(unknown)"}`,
|
|
2345
2452
|
`Task type: ${currentTaskType(snapshot) ?? "(missing)"}`,
|
|
2346
2453
|
`Evaluation profile: ${currentEvaluationProfile(snapshot) ?? "(missing)"}`,
|
|
@@ -2358,10 +2465,20 @@ function buildSystemReminder(snapshot: CompletionStateSnapshot, sliceHistory: Js
|
|
|
2358
2465
|
"If canonical state is stale, invalid, ambiguous, or missing, route to completion-regrounder.",
|
|
2359
2466
|
"When recovering from compaction, prefer a deterministic restart from canonical files over conversational inference.",
|
|
2360
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
|
+
}
|
|
2361
2472
|
if (activePriority !== undefined) lines.push(`Active slice priority: ${activePriority}`);
|
|
2362
2473
|
if (activeWhyNow) lines.push(`Active slice why_now: ${activeWhyNow}`);
|
|
2363
2474
|
if (implementationSurfaces.length > 0) lines.push(`Active implementation surfaces: ${implementationSurfaces.join(", ")}`);
|
|
2364
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}`);
|
|
2365
2482
|
if (isRubricEvaluationRole(nextRole)) lines.push(buildEvaluationRoleReminderText(snapshot, nextRole));
|
|
2366
2483
|
return lines.join(" ");
|
|
2367
2484
|
}
|
|
@@ -2378,26 +2495,43 @@ function buildPostCompactionDriverInstructions(snapshot: CompletionStateSnapshot
|
|
|
2378
2495
|
const verificationCommands = asStringArray(snapshot.active?.verification_commands);
|
|
2379
2496
|
const activePriority = asNumber(snapshot.active?.priority);
|
|
2380
2497
|
const activeWhyNow = asString(snapshot.active?.why_now);
|
|
2498
|
+
const exactActiveContract = activeCarriesExactHandoff(snapshot.active);
|
|
2499
|
+
const activeContractDrift = activeSliceContractDriftSummary(snapshot);
|
|
2500
|
+
const evidence = verificationEvidenceContext(snapshot);
|
|
2381
2501
|
const lines = [
|
|
2382
2502
|
"POST-COMPACTION RECOVERY MODE is active.",
|
|
2383
2503
|
`Compaction marker time: ${markerAt}`,
|
|
2384
2504
|
"Treat the previous conversation as lossy continuity support only.",
|
|
2385
|
-
"Before taking any substantive action, re-read .agent/state.json, .agent/plan.json, .agent/active-slice.json, .agent/slice-history.jsonl,
|
|
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.",
|
|
2386
2506
|
`Canonical task_type is currently: ${taskType}`,
|
|
2387
2507
|
`Canonical evaluation_profile is currently: ${evaluationProfile}`,
|
|
2388
2508
|
`Canonical next mandatory role is currently: ${nextRole}`,
|
|
2389
2509
|
`Canonical next mandatory action is currently: ${nextAction}`,
|
|
2390
2510
|
`Canonical continuation policy is currently: ${continuation}`,
|
|
2391
2511
|
`Canonical active slice is currently: ${activeSliceId}`,
|
|
2512
|
+
`Canonical verification evidence artifact is currently: ${evidence.path} (${evidence.status})`,
|
|
2392
2513
|
"Do not trust pre-compaction memory over canonical files.",
|
|
2393
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.",
|
|
2394
2515
|
"If continuation_policy == continue and canonical state is coherent, continue dispatching the mandatory role directly without asking the user whether to continue.",
|
|
2395
2516
|
"If you are about to implement after compaction, confirm the active slice snapshot still matches .agent/plan.json before doing any work.",
|
|
2396
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
|
+
}
|
|
2397
2522
|
if (activePriority !== undefined) lines.push(`Canonical active-slice priority is currently: ${activePriority}`);
|
|
2398
2523
|
if (activeWhyNow) lines.push(`Canonical active-slice why_now is currently: ${activeWhyNow}`);
|
|
2399
2524
|
if (implementationSurfaces.length > 0) lines.push(`Canonical implementation surfaces are currently: ${implementationSurfaces.join(", ")}`);
|
|
2400
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}`);
|
|
2401
2535
|
if (isRubricEvaluationRole(nextRole)) lines.push(buildEvaluationRoleReminderText(snapshot, nextRole));
|
|
2402
2536
|
return lines.join(" ");
|
|
2403
2537
|
}
|
|
@@ -2478,6 +2612,8 @@ function buildResumeCapsule(snapshot: CompletionStateSnapshot, sliceHistory: Jso
|
|
|
2478
2612
|
const implementationSurfaces = asStringArray(snapshot.active?.implementation_surfaces);
|
|
2479
2613
|
const verificationCommands = asStringArray(snapshot.active?.verification_commands);
|
|
2480
2614
|
const remainingBefore = asStringArray(snapshot.active?.remaining_contract_ids_before);
|
|
2615
|
+
const activeContractDrift = activeSliceContractDriftSummary(snapshot);
|
|
2616
|
+
const evidence = verificationEvidenceContext(snapshot);
|
|
2481
2617
|
const lines = [
|
|
2482
2618
|
"Authoritative completion resume capsule:",
|
|
2483
2619
|
"",
|
|
@@ -2494,9 +2630,23 @@ function buildResumeCapsule(snapshot: CompletionStateSnapshot, sliceHistory: Jso
|
|
|
2494
2630
|
`remaining_slice_count: ${remainingSliceCount(snapshot.plan)}`,
|
|
2495
2631
|
`remaining_stop_judges: ${asNumber(snapshot.state?.remaining_stop_judges) ?? "(unknown)"}`,
|
|
2496
2632
|
`active_slice_matches_plan: ${activeSliceMatchesPlan(snapshot)}`,
|
|
2633
|
+
`active_slice_contract_drift_fields: ${activeContractDrift}`,
|
|
2497
2634
|
`implementer_handoff_snapshot: ${handoffSnapshotState(snapshot.active)}`,
|
|
2498
2635
|
`history_counts: reviewed=${history.reviewed}, audited=${history.audited}, accepted=${history.accepted}, reopened=${history.reopened}, judgments=${history.judgments}`,
|
|
2499
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
|
+
"",
|
|
2500
2650
|
"active_slice:",
|
|
2501
2651
|
`- slice_id: ${asString(snapshot.active?.slice_id) ?? asString(snapshot.activeSlice?.slice_id) ?? "(none)"}`,
|
|
2502
2652
|
`- status: ${asString(snapshot.active?.status) ?? asString(snapshot.activeSlice?.status) ?? "unknown"}`,
|
|
@@ -2521,14 +2671,16 @@ function buildResumeCapsule(snapshot: CompletionStateSnapshot, sliceHistory: Jso
|
|
|
2521
2671
|
"",
|
|
2522
2672
|
"Rules:",
|
|
2523
2673
|
"- Treat this block as continuity support derived from canonical .agent state.",
|
|
2524
|
-
"-
|
|
2525
|
-
"-
|
|
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.",
|
|
2526
2678
|
"- Invoke completion-regrounder before continuing when requires_reground is true or unknown.",
|
|
2527
2679
|
"- Invoke completion-regrounder before continuing when next_mandatory_role or next_mandatory_action is unknown or ambiguous.",
|
|
2528
|
-
"- 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.",
|
|
2529
2681
|
"- If continuation_policy is continue, do not stop after a slice or ask whether to continue. Dispatch the next mandatory role directly.",
|
|
2530
2682
|
"- Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.",
|
|
2531
|
-
"- If you are completion-implementer after compaction, resume from the canonical active-slice
|
|
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.",
|
|
2532
2684
|
"- Do not replace canonical .agent state with summary inference.",
|
|
2533
2685
|
"</completion-state>",
|
|
2534
2686
|
);
|
|
@@ -2938,6 +3090,53 @@ function collapseRecentActivity(items: string[], maxItems = 4): string[] {
|
|
|
2938
3090
|
return collapsed.slice(-maxItems);
|
|
2939
3091
|
}
|
|
2940
3092
|
|
|
3093
|
+
function formatInlineRunningText(theme: any, lines: string[], options?: { primaryAssistant?: boolean }): string {
|
|
3094
|
+
let text = "";
|
|
3095
|
+
for (const [index, line] of lines.entries()) {
|
|
3096
|
+
if (index > 0) text += "\n";
|
|
3097
|
+
if (index === 0) {
|
|
3098
|
+
const [prefix, ...rest] = line.split(" ");
|
|
3099
|
+
text += theme.fg("warning", prefix);
|
|
3100
|
+
if (rest.length > 0) text += ` ${theme.fg("accent", rest.join(" "))}`;
|
|
3101
|
+
continue;
|
|
3102
|
+
}
|
|
3103
|
+
if (line.startsWith("tool:") || line.startsWith("progress:")) {
|
|
3104
|
+
text += theme.fg("toolOutput", line);
|
|
3105
|
+
continue;
|
|
3106
|
+
}
|
|
3107
|
+
if (line.startsWith("activity:")) {
|
|
3108
|
+
text += line.includes("stalled") ? theme.fg("warning", line) : line;
|
|
3109
|
+
continue;
|
|
3110
|
+
}
|
|
3111
|
+
if (line === "recent tools:") {
|
|
3112
|
+
text += theme.fg("muted", line);
|
|
3113
|
+
continue;
|
|
3114
|
+
}
|
|
3115
|
+
if (line.startsWith("- ")) {
|
|
3116
|
+
text += `${theme.fg("muted", "- ")}${theme.fg("muted", line.slice(2))}`;
|
|
3117
|
+
continue;
|
|
3118
|
+
}
|
|
3119
|
+
if (line.startsWith("elapsed:")) {
|
|
3120
|
+
text += line;
|
|
3121
|
+
continue;
|
|
3122
|
+
}
|
|
3123
|
+
if (line.startsWith("assistant:")) {
|
|
3124
|
+
text += options?.primaryAssistant ? line : theme.fg("muted", line);
|
|
3125
|
+
continue;
|
|
3126
|
+
}
|
|
3127
|
+
if (line.startsWith("next:") || line.startsWith("verifying:")) {
|
|
3128
|
+
text += theme.fg("muted", line);
|
|
3129
|
+
continue;
|
|
3130
|
+
}
|
|
3131
|
+
if (line.startsWith("rationale:") || line.startsWith("state-delta:")) {
|
|
3132
|
+
text += line;
|
|
3133
|
+
continue;
|
|
3134
|
+
}
|
|
3135
|
+
text += theme.fg("muted", line);
|
|
3136
|
+
}
|
|
3137
|
+
return text;
|
|
3138
|
+
}
|
|
3139
|
+
|
|
2941
3140
|
function buildInlineRunningLines(details: {
|
|
2942
3141
|
role?: string;
|
|
2943
3142
|
startedAt?: number;
|
|
@@ -3150,11 +3349,11 @@ function completionKickoff(
|
|
|
3150
3349
|
: intent === "refocus" && missionAnchor
|
|
3151
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`
|
|
3152
3351
|
: "";
|
|
3153
|
-
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,
|
|
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.`;
|
|
3154
3353
|
}
|
|
3155
3354
|
|
|
3156
3355
|
function completionResumePrompt(taskType: string, evaluationProfile: string): string {
|
|
3157
|
-
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,
|
|
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.`;
|
|
3158
3357
|
}
|
|
3159
3358
|
|
|
3160
3359
|
export default function completionExtension(pi: ExtensionAPI) {
|
|
@@ -3473,7 +3672,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
3473
3672
|
const task = typeof args.task === "string" ? args.task.trim() : "";
|
|
3474
3673
|
let text = theme.fg("toolTitle", theme.bold("completion_role ")) + theme.fg("accent", role);
|
|
3475
3674
|
if (task) {
|
|
3476
|
-
text += `\n${theme.fg("
|
|
3675
|
+
text += `\n${theme.fg("muted", task)}`;
|
|
3477
3676
|
}
|
|
3478
3677
|
return new Text(text, 0, 0);
|
|
3479
3678
|
},
|
|
@@ -3501,53 +3700,26 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
3501
3700
|
};
|
|
3502
3701
|
if (isPartial) {
|
|
3503
3702
|
const lines = buildInlineRunningLines(details);
|
|
3504
|
-
|
|
3505
|
-
for (const [index, line] of lines.entries()) {
|
|
3506
|
-
if (index > 0) text += "\n";
|
|
3507
|
-
if (index === 0) {
|
|
3508
|
-
const [prefix, ...rest] = line.split(" ");
|
|
3509
|
-
text += theme.fg("warning", prefix);
|
|
3510
|
-
if (rest.length > 0) text += ` ${theme.fg("accent", rest.join(" "))}`;
|
|
3511
|
-
continue;
|
|
3512
|
-
}
|
|
3513
|
-
if (line.startsWith("tool:") || line.startsWith("progress:")) {
|
|
3514
|
-
text += theme.fg("toolOutput", line);
|
|
3515
|
-
continue;
|
|
3516
|
-
}
|
|
3517
|
-
if (line.startsWith("activity:")) {
|
|
3518
|
-
text += theme.fg(line.includes("stalled") ? "warning" : "dim", line);
|
|
3519
|
-
continue;
|
|
3520
|
-
}
|
|
3521
|
-
if (line === "recent tools:") {
|
|
3522
|
-
text += theme.fg("dim", line);
|
|
3523
|
-
continue;
|
|
3524
|
-
}
|
|
3525
|
-
if (line.startsWith("- ")) {
|
|
3526
|
-
text += `${theme.fg("muted", "- ")}${theme.fg("dim", line.slice(2))}`;
|
|
3527
|
-
continue;
|
|
3528
|
-
}
|
|
3529
|
-
text += theme.fg("dim", line);
|
|
3530
|
-
}
|
|
3531
|
-
return new Text(text, 0, 0);
|
|
3703
|
+
return new Text(formatInlineRunningText(theme, lines), 0, 0);
|
|
3532
3704
|
}
|
|
3533
3705
|
const role = details.role ?? "completion-role";
|
|
3534
3706
|
const ok = details.status === "ok" && !result.isError;
|
|
3535
3707
|
let text = `${theme.fg(ok ? "success" : "error", ok ? "done" : "error")} ${theme.fg("toolTitle", theme.bold(role))}`;
|
|
3536
|
-
if (details.startedAt !== undefined) text += `\n${theme.fg("
|
|
3708
|
+
if (details.startedAt !== undefined) text += `\n${theme.fg("muted", `elapsed: ${formatElapsed(nowMs() - details.startedAt)}`)}`;
|
|
3537
3709
|
if (details.toolActivity) text += `\n${theme.fg("toolOutput", `tool: ${details.toolActivity}`)}`;
|
|
3538
3710
|
if (details.progress) text += `\n${theme.fg("toolOutput", `progress: ${details.progress}`)}`;
|
|
3539
|
-
else if (details.assistantSummary) text += `\
|
|
3540
|
-
if (details.rationale) text += `\n${theme.fg("
|
|
3541
|
-
if (details.nextStep) text += `\n${theme.fg("
|
|
3542
|
-
if (details.verifying) text += `\n${theme.fg("
|
|
3711
|
+
else if (details.assistantSummary) text += `\nassistant: ${details.assistantSummary}`;
|
|
3712
|
+
if (details.rationale) text += `\n${theme.fg("muted", `rationale: ${details.rationale}`)}`;
|
|
3713
|
+
if (details.nextStep) text += `\n${theme.fg("muted", `next: ${details.nextStep}`)}`;
|
|
3714
|
+
if (details.verifying) text += `\n${theme.fg("muted", `verifying: ${details.verifying}`)}`;
|
|
3543
3715
|
if (details.stateDeltas?.length) {
|
|
3544
|
-
for (const delta of details.stateDeltas.slice(-4)) text += `\n${theme.fg("
|
|
3716
|
+
for (const delta of details.stateDeltas.slice(-4)) text += `\n${theme.fg("muted", `state-delta: ${delta}`)}`;
|
|
3545
3717
|
}
|
|
3546
3718
|
if (details.transcription?.appended?.length) {
|
|
3547
3719
|
text += `\n${theme.fg("success", `transcribed: ${details.transcription.appended.join(", ")}`)}`;
|
|
3548
3720
|
}
|
|
3549
3721
|
if (details.transcription?.skipped?.length && expanded) {
|
|
3550
|
-
text += `\n${theme.fg("
|
|
3722
|
+
text += `\n${theme.fg("muted", `skipped: ${details.transcription.skipped.join(" | ")}`)}`;
|
|
3551
3723
|
}
|
|
3552
3724
|
if (details.transcription?.errors?.length) {
|
|
3553
3725
|
text += `\n${theme.fg("warning", `warnings: ${details.transcription.errors.join(" | ")}`)}`;
|
|
@@ -3565,14 +3737,14 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
3565
3737
|
for (const key of summaryKeys) {
|
|
3566
3738
|
const value = reportFields[key];
|
|
3567
3739
|
if (!value) continue;
|
|
3568
|
-
text += `\n${theme.fg("
|
|
3740
|
+
text += `\n${theme.fg("muted", `${key}: `)}${value}`;
|
|
3569
3741
|
}
|
|
3570
3742
|
const body = result.content.find((item) => item.type === "text");
|
|
3571
3743
|
if (expanded && body?.type === "text") {
|
|
3572
3744
|
text += `\n\n${body.text}`;
|
|
3573
3745
|
} else if (!expanded && body?.type === "text") {
|
|
3574
3746
|
const preview = body.text.split("\n").slice(0, 4).join("\n");
|
|
3575
|
-
text += `\n${theme.fg("
|
|
3747
|
+
text += `\n${theme.fg("muted", preview)}`;
|
|
3576
3748
|
}
|
|
3577
3749
|
if (details.stderr && expanded) text += `\n${theme.fg("error", details.stderr)}`;
|
|
3578
3750
|
return new Text(text, 0, 0);
|
|
@@ -3605,13 +3777,11 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
3605
3777
|
);
|
|
3606
3778
|
return;
|
|
3607
3779
|
}
|
|
3608
|
-
const decision = await confirmContextProposal(ctx, proposal,
|
|
3780
|
+
const decision = await confirmContextProposal(ctx, proposal, {
|
|
3609
3781
|
title: "Start a completion workflow from the recent discussion?",
|
|
3610
|
-
editorPrompt:
|
|
3611
|
-
"Start a completion workflow from the recent discussion?\n\nEdit the proposed mission, scope, constraints, and acceptance details below.",
|
|
3612
3782
|
});
|
|
3613
3783
|
if (!decision) {
|
|
3614
|
-
emitCommandText(ctx, "Cancelled recent-discussion workflow proposal", "info");
|
|
3784
|
+
emitCommandText(ctx, buildCookCancellationMessage("Cancelled recent-discussion workflow proposal"), "info");
|
|
3615
3785
|
return;
|
|
3616
3786
|
}
|
|
3617
3787
|
goal = decision.goalText;
|
|
@@ -3619,14 +3789,12 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
3619
3789
|
kickoffAnalysis = decision.analysis;
|
|
3620
3790
|
} else {
|
|
3621
3791
|
const proposal = await buildGoalAnchoredContextProposal(ctx, goal, projectName);
|
|
3622
|
-
const decision = await confirmContextProposal(ctx, proposal,
|
|
3792
|
+
const decision = await confirmContextProposal(ctx, proposal, {
|
|
3623
3793
|
title: "Start a completion workflow from this goal?",
|
|
3624
3794
|
nonInteractiveBehavior: "accept",
|
|
3625
|
-
editorPrompt:
|
|
3626
|
-
"Start a completion workflow from this goal?\n\nEdit the proposed mission, scope, constraints, and acceptance details below.",
|
|
3627
3795
|
});
|
|
3628
3796
|
if (!decision) {
|
|
3629
|
-
emitCommandText(ctx, "Cancelled workflow startup proposal", "info");
|
|
3797
|
+
emitCommandText(ctx, buildCookCancellationMessage("Cancelled workflow startup proposal"), "info");
|
|
3630
3798
|
return;
|
|
3631
3799
|
}
|
|
3632
3800
|
goal = decision.goalText;
|
|
@@ -3665,13 +3833,11 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
3665
3833
|
);
|
|
3666
3834
|
return;
|
|
3667
3835
|
}
|
|
3668
|
-
const decision = await confirmContextProposal(ctx, proposal,
|
|
3836
|
+
const decision = await confirmContextProposal(ctx, proposal, {
|
|
3669
3837
|
title: "The previous completion workflow is done. Start the next workflow round from the recent discussion?",
|
|
3670
|
-
editorPrompt:
|
|
3671
|
-
"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.",
|
|
3672
3838
|
});
|
|
3673
3839
|
if (!decision) {
|
|
3674
|
-
emitCommandText(ctx, "Cancelled next workflow round proposal", "info");
|
|
3840
|
+
emitCommandText(ctx, buildCookCancellationMessage("Cancelled next workflow round proposal"), "info");
|
|
3675
3841
|
return;
|
|
3676
3842
|
}
|
|
3677
3843
|
goal = decision.goalText;
|
|
@@ -3694,7 +3860,9 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
3694
3860
|
current_phase: asString(snapshot.state?.current_phase) ?? null,
|
|
3695
3861
|
next_mandatory_role: asString(snapshot.state?.next_mandatory_role) ?? null,
|
|
3696
3862
|
});
|
|
3697
|
-
|
|
3863
|
+
const resumeKind =
|
|
3864
|
+
shouldTestAutoContinueOnSessionStart() && completionTestAutoContinuePromptPath() ? "auto-resume" : "resume";
|
|
3865
|
+
await queueCompletionDriverPrompt(pi, ctx, rootKey, fingerprint, resumePrompt, resumeKind);
|
|
3698
3866
|
return;
|
|
3699
3867
|
}
|
|
3700
3868
|
}
|
|
@@ -3703,14 +3871,12 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
3703
3871
|
if (workflowDone) {
|
|
3704
3872
|
const projectName = path.basename(snapshot.files.root);
|
|
3705
3873
|
const proposal = await buildGoalAnchoredContextProposal(ctx, goal, projectName);
|
|
3706
|
-
const decision = await confirmContextProposal(ctx, proposal,
|
|
3874
|
+
const decision = await confirmContextProposal(ctx, proposal, {
|
|
3707
3875
|
title: "Start the next workflow round from this goal?",
|
|
3708
3876
|
nonInteractiveBehavior: "accept",
|
|
3709
|
-
editorPrompt:
|
|
3710
|
-
"Start the next workflow round from this goal?\n\nEdit the proposed mission, scope, constraints, and acceptance details below.",
|
|
3711
3877
|
});
|
|
3712
3878
|
if (!decision) {
|
|
3713
|
-
emitCommandText(ctx, "Cancelled next workflow round proposal", "info");
|
|
3879
|
+
emitCommandText(ctx, buildCookCancellationMessage("Cancelled next workflow round proposal"), "info");
|
|
3714
3880
|
return;
|
|
3715
3881
|
}
|
|
3716
3882
|
goal = decision.goalText;
|
|
@@ -3722,7 +3888,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
3722
3888
|
} else {
|
|
3723
3889
|
const decision = await confirmExistingWorkflowGoal(ctx, snapshot, goal);
|
|
3724
3890
|
if (!decision) {
|
|
3725
|
-
emitCommandText(ctx, "Cancelled existing workflow confirmation", "info");
|
|
3891
|
+
emitCommandText(ctx, buildCookCancellationMessage("Cancelled existing workflow confirmation"), "info");
|
|
3726
3892
|
return;
|
|
3727
3893
|
}
|
|
3728
3894
|
kickoffIntent = decision.action;
|
|
@@ -3730,14 +3896,12 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
3730
3896
|
if (decision.action === "refocus") {
|
|
3731
3897
|
const projectName = path.basename(snapshot.files.root);
|
|
3732
3898
|
const proposal = await buildGoalAnchoredContextProposal(ctx, goal, projectName);
|
|
3733
|
-
const proposalDecision = await confirmContextProposal(ctx, proposal,
|
|
3899
|
+
const proposalDecision = await confirmContextProposal(ctx, proposal, {
|
|
3734
3900
|
title: "Start the replacement workflow from this goal?",
|
|
3735
3901
|
nonInteractiveBehavior: "accept",
|
|
3736
|
-
editorPrompt:
|
|
3737
|
-
"Start the replacement workflow from this goal?\n\nEdit the proposed mission, scope, constraints, and acceptance details below.",
|
|
3738
3902
|
});
|
|
3739
3903
|
if (!proposalDecision) {
|
|
3740
|
-
emitCommandText(ctx, "Cancelled replacement workflow proposal", "info");
|
|
3904
|
+
emitCommandText(ctx, buildCookCancellationMessage("Cancelled replacement workflow proposal"), "info");
|
|
3741
3905
|
return;
|
|
3742
3906
|
}
|
|
3743
3907
|
goal = proposalDecision.goalText;
|