@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.
- package/CHANGELOG.md +25 -0
- package/README.md +48 -77
- package/agents/completion-implementer.md +11 -2
- package/extensions/completion/index.ts +370 -222
- 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 {
|
|
@@ -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
|
|
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
"-
|
|
2521
|
-
"-
|
|
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
|
|
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 +=
|
|
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 +=
|
|
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 +=
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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;
|