@linimin/pi-letscook 0.1.57 → 0.1.59
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/.agent/README.md +28 -0
- package/.agent/mission.md +8 -0
- package/.agent/profile.json +13 -0
- package/.agent/verify_completion_control_plane.sh +203 -0
- package/.agent/verify_completion_stop.sh +20 -0
- package/CHANGELOG.md +22 -2
- package/README.md +27 -18
- package/extensions/completion/driver.ts +77 -35
- package/extensions/completion/index.ts +78 -53
- package/extensions/completion/prompt-surfaces.ts +10 -5
- package/extensions/completion/proposal.ts +134 -28
- package/package.json +6 -1
- package/scripts/active-slice-contract-test.sh +93 -2
- package/scripts/canonical-evidence-artifact-test.sh +93 -2
- package/scripts/context-proposal-test.sh +751 -741
- package/scripts/refocus-test.sh +196 -28
- package/scripts/release-check.sh +51 -22
- package/scripts/smoke-test.sh +115 -10
- package/skills/cook-handoff-boundary/SKILL.md +20 -7
|
@@ -18,14 +18,13 @@ import {
|
|
|
18
18
|
collectRecentDiscussionEntries,
|
|
19
19
|
collectRecentSessionMessages,
|
|
20
20
|
deriveCookContextProposalFromRecentDiscussion,
|
|
21
|
-
|
|
21
|
+
assessLatestCookHandoffProposal,
|
|
22
22
|
finalizeContextProposalAnalysis,
|
|
23
23
|
isWeakMissionAnchor,
|
|
24
24
|
missionAnchorsLikelyEquivalent,
|
|
25
25
|
missionAnchorsStrictlyEquivalent,
|
|
26
26
|
normalizeMissionAnchorText,
|
|
27
27
|
resolveContextProposalConfirmationAction,
|
|
28
|
-
shouldTreatBareActiveWorkflowProposalAsClearRefocus,
|
|
29
28
|
stripCodeBlocks,
|
|
30
29
|
} from "./proposal";
|
|
31
30
|
import type {
|
|
@@ -122,11 +121,21 @@ function candidateSlices(plan: JsonRecord | undefined): JsonRecord[] {
|
|
|
122
121
|
return Array.isArray(slices) ? slices.filter(isRecord) : [];
|
|
123
122
|
}
|
|
124
123
|
|
|
124
|
+
type CookContextProposalResult = {
|
|
125
|
+
proposal?: ContextProposal;
|
|
126
|
+
blockedFailureMessage?: string;
|
|
127
|
+
};
|
|
128
|
+
|
|
125
129
|
type ActiveWorkflowProposalAssessment = {
|
|
126
|
-
action: "continue" | "refocus" | "
|
|
130
|
+
action: "continue" | "refocus" | "blocked";
|
|
127
131
|
currentMissionAnchor: string;
|
|
128
132
|
proposal?: ContextProposal;
|
|
129
|
-
|
|
133
|
+
blockedFailureMessage?: string;
|
|
134
|
+
reason:
|
|
135
|
+
| "matching_mission"
|
|
136
|
+
| "missing_explicit_handoff"
|
|
137
|
+
| "fresh_explicit_handoff"
|
|
138
|
+
| "fresh_explicit_handoff_not_startable";
|
|
130
139
|
};
|
|
131
140
|
|
|
132
141
|
function completionTestWorkflowActionOverride(): "continue" | "refocus" | "cancel" | undefined {
|
|
@@ -200,7 +209,7 @@ function maybeWriteTestSnapshot(targetPath: string | undefined, content: string)
|
|
|
200
209
|
|
|
201
210
|
const COOK_MAIN_CHAT_RERUN_GUIDANCE = "Discuss changes in the main chat and rerun /cook.";
|
|
202
211
|
const COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL =
|
|
203
|
-
"/cook failed closed because
|
|
212
|
+
"/cook failed closed because new-workflow startup now requires a fresh valid explicit primary-agent handoff from recent ordinary-chat discussion; recent discussion alone no longer starts a workflow. Ask the primary agent to hand off explicitly in the main chat, then rerun /cook.";
|
|
204
213
|
|
|
205
214
|
function isWorkflowDone(snapshot: CompletionStateSnapshot | undefined): boolean {
|
|
206
215
|
return asString(snapshot?.state?.continuation_policy) === "done";
|
|
@@ -271,6 +280,7 @@ function maybeWriteActiveWorkflowRoutingSnapshot(assessment: ActiveWorkflowPropo
|
|
|
271
280
|
action: assessment.action,
|
|
272
281
|
reason: assessment.reason,
|
|
273
282
|
currentMissionAnchor: assessment.currentMissionAnchor,
|
|
283
|
+
blockedFailureMessage: assessment.blockedFailureMessage ?? null,
|
|
274
284
|
proposedMissionAnchor: assessment.proposal?.mission ?? null,
|
|
275
285
|
proposalSource: assessment.proposal?.source ?? null,
|
|
276
286
|
possibleNoise: assessment.proposal?.analysis.possibleNoise ?? [],
|
|
@@ -362,10 +372,31 @@ async function promptContextProposalConfirmationAction(
|
|
|
362
372
|
});
|
|
363
373
|
}
|
|
364
374
|
|
|
375
|
+
async function deriveCookStartupProposal(
|
|
376
|
+
ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
|
|
377
|
+
projectName: string,
|
|
378
|
+
): Promise<CookContextProposalResult> {
|
|
379
|
+
const recentMessages = collectRecentSessionMessages(ctx, { isRecord, asString, asNumber, isStaleContextError });
|
|
380
|
+
const explicitHandoff = assessLatestCookHandoffProposal(recentMessages, projectName, {
|
|
381
|
+
asString,
|
|
382
|
+
asStringArray,
|
|
383
|
+
assessMissionAnchor,
|
|
384
|
+
normalizeMissionAnchorText,
|
|
385
|
+
isWeakMissionAnchor,
|
|
386
|
+
missionAnchorsStrictlyEquivalent,
|
|
387
|
+
stripCodeBlocks,
|
|
388
|
+
});
|
|
389
|
+
if (explicitHandoff.status === "startable") return { proposal: explicitHandoff.proposal };
|
|
390
|
+
if (explicitHandoff.status === "fresh_but_not_startable") {
|
|
391
|
+
return { blockedFailureMessage: explicitHandoff.message };
|
|
392
|
+
}
|
|
393
|
+
return {};
|
|
394
|
+
}
|
|
395
|
+
|
|
365
396
|
async function deriveCookContextProposal(
|
|
366
397
|
ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
|
|
367
398
|
projectName: string,
|
|
368
|
-
): Promise<
|
|
399
|
+
): Promise<CookContextProposalResult> {
|
|
369
400
|
const recentMessages = collectRecentSessionMessages(ctx, { isRecord, asString, asNumber, isStaleContextError });
|
|
370
401
|
const recentEntries = recentMessages
|
|
371
402
|
.filter((entry) => (entry.role === "user" || entry.role === "custom") && !entry.isCommand)
|
|
@@ -384,51 +415,45 @@ async function deriveCookContextProposal(
|
|
|
384
415
|
`verification summary: ${asString(snapshot.verificationEvidence?.summary) ?? "(none)"}`,
|
|
385
416
|
]
|
|
386
417
|
: [];
|
|
387
|
-
const explicitHandoff =
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
asString(snapshot.state?.
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
assessMissionAnchor,
|
|
427
|
-
isWeakMissionAnchor,
|
|
428
|
-
missionAnchorsStrictlyEquivalent,
|
|
429
|
-
normalizeMissionAnchorText,
|
|
430
|
-
stripCodeBlocks,
|
|
431
|
-
});
|
|
418
|
+
const explicitHandoff = await deriveCookStartupProposal(ctx, projectName);
|
|
419
|
+
if (explicitHandoff.proposal || explicitHandoff.blockedFailureMessage) return explicitHandoff;
|
|
420
|
+
return {
|
|
421
|
+
proposal: await deriveCookContextProposalFromRecentDiscussion(projectName, recentEntries, {
|
|
422
|
+
asString,
|
|
423
|
+
asStringArray,
|
|
424
|
+
workflowContext: snapshot
|
|
425
|
+
? {
|
|
426
|
+
currentMissionAnchor:
|
|
427
|
+
asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? asString(snapshot.active?.mission_anchor),
|
|
428
|
+
latestCompletedSlice: asString(snapshot.state?.latest_completed_slice),
|
|
429
|
+
latestVerifiedSlice: asString(snapshot.state?.latest_verified_slice),
|
|
430
|
+
activeSliceGoal: asString(snapshot.active?.goal),
|
|
431
|
+
activeSliceWhyNow: asString(snapshot.active?.why_now),
|
|
432
|
+
verificationGoal: asString(snapshot.verificationEvidence?.goal),
|
|
433
|
+
verificationSummary: asString(snapshot.verificationEvidence?.summary),
|
|
434
|
+
continuationPolicy: asString(snapshot.state?.continuation_policy),
|
|
435
|
+
}
|
|
436
|
+
: undefined,
|
|
437
|
+
analyzeContextProposal: async (entries) =>
|
|
438
|
+
await analyzeContextProposalWithAgent({
|
|
439
|
+
ctx,
|
|
440
|
+
projectName,
|
|
441
|
+
recentEntries: entries,
|
|
442
|
+
workflowContextLines,
|
|
443
|
+
liveRoleActivityByRoot,
|
|
444
|
+
completionStatusKey: COMPLETION_STATUS_KEY,
|
|
445
|
+
safeUiCall,
|
|
446
|
+
getCtxCwd,
|
|
447
|
+
getCtxHasUI,
|
|
448
|
+
getCtxUi,
|
|
449
|
+
}),
|
|
450
|
+
assessMissionAnchor,
|
|
451
|
+
isWeakMissionAnchor,
|
|
452
|
+
missionAnchorsStrictlyEquivalent,
|
|
453
|
+
normalizeMissionAnchorText,
|
|
454
|
+
stripCodeBlocks,
|
|
455
|
+
}),
|
|
456
|
+
};
|
|
432
457
|
}
|
|
433
458
|
|
|
434
459
|
async function confirmContextProposal(
|
|
@@ -904,7 +929,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
904
929
|
structuredDiscussionFailureDetail: COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL,
|
|
905
930
|
mainChatRerunGuidance: COOK_MAIN_CHAT_RERUN_GUIDANCE,
|
|
906
931
|
cookCommandSpec: {
|
|
907
|
-
description: "/cook workflow:
|
|
932
|
+
description: "/cook workflow: start a new workflow or next round only from a fresh recent explicit primary-agent handoff, resume the current workflow from canonical state, or confirm an explicit replacement from the explicit /cook command",
|
|
908
933
|
},
|
|
909
934
|
buildContextProposalContinuationReason,
|
|
910
935
|
completionKickoff,
|
|
@@ -917,6 +942,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
917
942
|
completionTestWorkflowMissionOverride,
|
|
918
943
|
confirmContextProposal,
|
|
919
944
|
deriveCookContextProposal,
|
|
945
|
+
deriveCookStartupProposal,
|
|
920
946
|
emitCommandText,
|
|
921
947
|
finalizeContextProposalAnalysis,
|
|
922
948
|
getCtxCwd,
|
|
@@ -931,7 +957,6 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
931
957
|
scaffoldCompletionFiles,
|
|
932
958
|
shouldSkipDriverKickoffForTests,
|
|
933
959
|
shouldTestAutoContinueOnSessionStart,
|
|
934
|
-
shouldTreatBareActiveWorkflowProposalAsClearRefocus,
|
|
935
960
|
};
|
|
936
961
|
|
|
937
962
|
|
|
@@ -27,12 +27,17 @@ export type AdvisoryStartupBrief = {
|
|
|
27
27
|
export function buildCookHandoffBoundaryReminder(): string {
|
|
28
28
|
return [
|
|
29
29
|
"You are still in ordinary main chat before any explicit /cook workflow entry.",
|
|
30
|
-
"Use ordinary chat to clarify requirements, discuss tradeoffs,
|
|
30
|
+
"Use ordinary chat to clarify requirements, discuss tradeoffs, propose implementation approaches, and refine scope with the user.",
|
|
31
31
|
"/cook is the only explicit entrypoint into long-running completion workflow.",
|
|
32
|
-
"When you judge that the task has matured into completion-workflow scope — for example the user has clearly shifted from exploration into implementation intent, you have just produced a concrete plan or proposal whose next step would naturally be implementation, or the task spans multiple files, steps, or verification surfaces —
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
32
|
+
"When you judge that the task has matured into completion-workflow scope — for example the user has clearly shifted from exploration into implementation intent, you have just produced a concrete plan or proposal whose next step would naturally be implementation, or the task spans multiple files, steps, or verification surfaces — do not begin long-running product implementation in ordinary chat and do not edit tracked product files for that workflow-level task.",
|
|
33
|
+
"Instead, recommend /cook as the workflow boundary while keeping the conversation in ordinary chat until the user explicitly runs /cook.",
|
|
34
|
+
"If the user keeps asking follow-up questions or refining requirements before /cook, continue that ordinary-chat discussion normally instead of switching into a handoff-only refusal mode, but do not act as though /cook had already been invoked.",
|
|
35
|
+
"Distinguish a workflow-worthy handoff from an implementation-ready handoff: only emit the implementation-ready capsule when the first bounded implementation slice is concrete enough to start immediately.",
|
|
36
|
+
"If the task is workflow-worthy but that first slice is still vague, say that /cook will be the right next step once the first bounded slice is concrete enough, then keep refining in ordinary chat without emitting an implementation-ready capsule yet.",
|
|
37
|
+
"When handing off, explain that /cook can start a new workflow or next round only from a fresh valid explicit primary-agent handoff capsule from recent ordinary-chat discussion; otherwise it fails closed, while already-active workflows resume from canonical .agent state unless a fresh valid explicit handoff proposes replacement.",
|
|
38
|
+
"Once the task is implementation-ready, append one exact fenced block in the same assistant reply using ```cook_handoff ... ``` JSON with kind/source/handoff_kind plus mission, scope, constraints or non_goals, acceptance, risks, notes, captured_at, source_turn_id, first_slice_goal, first_slice_non_goals, implementation_surfaces, verification_commands, why_this_slice_first, and optional task_type/evaluation_profile/why_cook_now.",
|
|
39
|
+
"Use handoff_kind implementation_workflow_handoff for that implementation-ready capsule.",
|
|
40
|
+
"If later ordinary-chat discussion materially changes the startup brief before /cook runs, update or replace the capsule in a later assistant reply instead of pretending the workflow already started.",
|
|
36
41
|
"The capsule is startup intake for /cook only: do not present it as canonical .agent state, an active slice, or a persistent repo contract.",
|
|
37
42
|
"If the task is still ordinary Q&A, lightweight brainstorming, or a tiny one-off fix, continue normally without forcing /cook.",
|
|
38
43
|
].join(" ");
|
|
@@ -59,12 +59,30 @@ export type CookHandoffCapsule = {
|
|
|
59
59
|
acceptance: string[];
|
|
60
60
|
risks: string[];
|
|
61
61
|
notes: string[];
|
|
62
|
-
handoff_kind: "
|
|
62
|
+
handoff_kind: "implementation_workflow_handoff";
|
|
63
|
+
first_slice_goal: string;
|
|
64
|
+
first_slice_non_goals: string[];
|
|
65
|
+
implementation_surfaces: string[];
|
|
66
|
+
verification_commands: string[];
|
|
67
|
+
why_this_slice_first: string;
|
|
63
68
|
task_type?: string;
|
|
64
69
|
evaluation_profile?: string;
|
|
65
70
|
why_cook_now?: string;
|
|
66
71
|
};
|
|
67
72
|
|
|
73
|
+
export type CookHandoffProposalAssessment =
|
|
74
|
+
| {
|
|
75
|
+
status: "none";
|
|
76
|
+
}
|
|
77
|
+
| {
|
|
78
|
+
status: "startable";
|
|
79
|
+
proposal: ContextProposal;
|
|
80
|
+
}
|
|
81
|
+
| {
|
|
82
|
+
status: "fresh_but_not_startable";
|
|
83
|
+
message: string;
|
|
84
|
+
};
|
|
85
|
+
|
|
68
86
|
export type ContextProposalDecision = {
|
|
69
87
|
missionAnchor: string;
|
|
70
88
|
goalText: string;
|
|
@@ -1228,9 +1246,14 @@ export function extractContextProposalFromStructuredSession(
|
|
|
1228
1246
|
|
|
1229
1247
|
const COOK_HANDOFF_BLOCK_REGEX = /```cook_handoff\s*([\s\S]*?)```/giu;
|
|
1230
1248
|
const COOK_HANDOFF_MAX_AGE_MS = 45 * 60 * 1000;
|
|
1231
|
-
const COOK_HANDOFF_MAX_LATER_NON_COMMAND_MESSAGES = 2;
|
|
1232
1249
|
const COOK_HANDOFF_NEGATIVE_MISSION_REGEX =
|
|
1233
1250
|
/(?:\b(?:do not|don't|dont|not|never|avoid|skip|refuse|recognize that|suppress|ignore|block|prevent)\b|(?:不要|別|别|勿|禁止|避免|忽略|阻止))/iu;
|
|
1251
|
+
const COOK_HANDOFF_WORKFLOW_ONLY_ACCEPTANCE_REGEX =
|
|
1252
|
+
/(?:\b(?:confirm|discuss|clarify|decide|review|align(?: on)?|agree(?: on)?|explain|summari(?:s|z)e|describe|plan|proposal|spec(?:ification)?|design(?: doc(?:ument)?)?|next step|handoff|workflow|readiness)\b|(?:確認|确认|討論|讨论|釐清|厘清|決定|决定|審查|审查|對齊|对齐|同意|说明|說明|總結|总结|描述|規劃|规划|提案|方案|工作流|就緒|就绪))/iu;
|
|
1253
|
+
const COOK_HANDOFF_VERIFICATION_ACCEPTANCE_REGEX =
|
|
1254
|
+
/(?:\b(?:test|tests|testing|verify|verification|validated?|regression|coverage|assert(?:ion)?s?|check|checks|smoke|snapshot(?:s)?)\b|(?:測試|测试|驗證|验证|回歸|回归|覆蓋|覆盖|斷言|断言|檢查|检查|快照))/iu;
|
|
1255
|
+
const COOK_HANDOFF_VERIFICATION_ACTION_REGEX =
|
|
1256
|
+
/(?:\b(?:add|update|keep|run|rerun|cover|verify|validate|check|assert|exercise|prove)\b|(?:新增|更新|保持|執行|执行|重跑|覆蓋|覆盖|驗證|验证|檢查|检查|斷言|断言|證明|证明))/iu;
|
|
1234
1257
|
|
|
1235
1258
|
function parseCookHandoffCapsulesFromText(
|
|
1236
1259
|
text: string,
|
|
@@ -1251,15 +1274,20 @@ function parseCookHandoffCapsulesFromText(
|
|
|
1251
1274
|
if (!localIsRecord(parsed)) continue;
|
|
1252
1275
|
if (deps.asString(parsed.kind) !== "cook_handoff") continue;
|
|
1253
1276
|
if (deps.asString(parsed.source) !== "primary_agent") continue;
|
|
1254
|
-
if (deps.asString(parsed.handoff_kind) !== "
|
|
1277
|
+
if (deps.asString(parsed.handoff_kind) !== "implementation_workflow_handoff") continue;
|
|
1255
1278
|
const mission = deps.asString(parsed.mission);
|
|
1256
|
-
|
|
1279
|
+
const firstSliceGoal = deps.asString(parsed.first_slice_goal ?? parsed.firstSliceGoal);
|
|
1280
|
+
const whyThisSliceFirst = deps.asString(parsed.why_this_slice_first ?? parsed.whyThisSliceFirst);
|
|
1281
|
+
if (!mission || !firstSliceGoal || !whyThisSliceFirst) continue;
|
|
1257
1282
|
const scope = deps.asStringArray(parsed.scope);
|
|
1258
1283
|
const constraints = deps.asStringArray(parsed.constraints);
|
|
1259
1284
|
const nonGoals = deps.asStringArray(parsed.non_goals ?? parsed.nonGoals);
|
|
1260
1285
|
const acceptance = deps.asStringArray(parsed.acceptance);
|
|
1261
1286
|
const risks = deps.asStringArray(parsed.risks);
|
|
1262
1287
|
const notes = deps.asStringArray(parsed.notes);
|
|
1288
|
+
const firstSliceNonGoals = deps.asStringArray(parsed.first_slice_non_goals ?? parsed.firstSliceNonGoals);
|
|
1289
|
+
const implementationSurfaces = deps.asStringArray(parsed.implementation_surfaces ?? parsed.implementationSurfaces);
|
|
1290
|
+
const verificationCommands = deps.asStringArray(parsed.verification_commands ?? parsed.verificationCommands);
|
|
1263
1291
|
const capturedAt = deps.asString(parsed.captured_at) ?? (timestampMs ? new Date(timestampMs).toISOString() : undefined);
|
|
1264
1292
|
const sourceTurnId = deps.asString(parsed.source_turn_id) ?? messageId;
|
|
1265
1293
|
if (!capturedAt || !sourceTurnId) continue;
|
|
@@ -1275,7 +1303,12 @@ function parseCookHandoffCapsulesFromText(
|
|
|
1275
1303
|
acceptance,
|
|
1276
1304
|
risks,
|
|
1277
1305
|
notes,
|
|
1278
|
-
handoff_kind: "
|
|
1306
|
+
handoff_kind: "implementation_workflow_handoff",
|
|
1307
|
+
first_slice_goal: firstSliceGoal,
|
|
1308
|
+
first_slice_non_goals: firstSliceNonGoals,
|
|
1309
|
+
implementation_surfaces: implementationSurfaces,
|
|
1310
|
+
verification_commands: verificationCommands,
|
|
1311
|
+
why_this_slice_first: whyThisSliceFirst,
|
|
1279
1312
|
task_type: deps.asString(parsed.task_type),
|
|
1280
1313
|
evaluation_profile: deps.asString(parsed.evaluation_profile),
|
|
1281
1314
|
why_cook_now: deps.asString(parsed.why_cook_now),
|
|
@@ -1285,33 +1318,74 @@ function parseCookHandoffCapsulesFromText(
|
|
|
1285
1318
|
}
|
|
1286
1319
|
|
|
1287
1320
|
function buildCookHandoffBasisPreview(capsule: CookHandoffCapsule): string {
|
|
1288
|
-
const parts = [
|
|
1321
|
+
const parts = [
|
|
1322
|
+
capsule.mission,
|
|
1323
|
+
...capsule.scope,
|
|
1324
|
+
...capsule.constraints,
|
|
1325
|
+
...capsule.non_goals,
|
|
1326
|
+
...capsule.acceptance,
|
|
1327
|
+
`first_slice_goal: ${capsule.first_slice_goal}`,
|
|
1328
|
+
...capsule.first_slice_non_goals.map((item) => `first_slice_non_goals: ${item}`),
|
|
1329
|
+
...capsule.implementation_surfaces.map((item) => `implementation_surfaces: ${item}`),
|
|
1330
|
+
...capsule.verification_commands.map((item) => `verification_commands: ${item}`),
|
|
1331
|
+
`why_this_slice_first: ${capsule.why_this_slice_first}`,
|
|
1332
|
+
];
|
|
1289
1333
|
if (capsule.why_cook_now) parts.push(`why_cook_now: ${capsule.why_cook_now}`);
|
|
1290
1334
|
return parts.join("\n").trim();
|
|
1291
1335
|
}
|
|
1292
1336
|
|
|
1293
|
-
function
|
|
1337
|
+
function cookHandoffAcceptanceItemIsRepoChangeOrVerificationOriented(item: string): boolean {
|
|
1338
|
+
const normalized = normalizeProposalLine(item);
|
|
1339
|
+
if (!normalized) return false;
|
|
1340
|
+
if (hasExplicitPlanningOnlyDeliverable([normalized])) return false;
|
|
1341
|
+
if (hasClearNoImplementationSignal([normalized])) return false;
|
|
1342
|
+
if (implementationMissionSourceCandidateText(normalized)) return true;
|
|
1343
|
+
if (COOK_HANDOFF_WORKFLOW_ONLY_ACCEPTANCE_REGEX.test(normalized)) return false;
|
|
1344
|
+
return COOK_HANDOFF_VERIFICATION_ACCEPTANCE_REGEX.test(normalized) && COOK_HANDOFF_VERIFICATION_ACTION_REGEX.test(normalized);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function cookHandoffAcceptanceIsRepoChangeOriented(capsule: CookHandoffCapsule): boolean {
|
|
1348
|
+
if (capsule.acceptance.length === 0) return false;
|
|
1349
|
+
return capsule.acceptance.some((item) => cookHandoffAcceptanceItemIsRepoChangeOrVerificationOriented(item));
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function cookHandoffStartabilityFailures(
|
|
1294
1353
|
capsule: CookHandoffCapsule,
|
|
1295
1354
|
deps: Pick<ProposalParseDeps, "normalizeMissionAnchorText" | "isWeakMissionAnchor">,
|
|
1296
|
-
):
|
|
1355
|
+
): string[] {
|
|
1356
|
+
const failures: string[] = [];
|
|
1297
1357
|
const mission = deps.normalizeMissionAnchorText(capsule.mission);
|
|
1298
|
-
if (!mission || deps.isWeakMissionAnchor(mission))
|
|
1299
|
-
if (COOK_HANDOFF_NEGATIVE_MISSION_REGEX.test(mission))
|
|
1300
|
-
if (capsule.scope.length === 0
|
|
1301
|
-
|
|
1358
|
+
if (!mission || deps.isWeakMissionAnchor(mission)) failures.push("mission is missing a concrete implementation anchor");
|
|
1359
|
+
else if (COOK_HANDOFF_NEGATIVE_MISSION_REGEX.test(mission)) failures.push("mission is negative or workflow-suppression-only");
|
|
1360
|
+
if (capsule.scope.length === 0) failures.push("scope is empty");
|
|
1361
|
+
if (capsule.acceptance.length === 0) failures.push("acceptance is empty");
|
|
1362
|
+
else if (!cookHandoffAcceptanceIsRepoChangeOriented(capsule)) {
|
|
1363
|
+
failures.push("acceptance is not anchored to concrete repo changes or verification");
|
|
1364
|
+
}
|
|
1365
|
+
const firstSliceGoal = deps.normalizeMissionAnchorText(capsule.first_slice_goal);
|
|
1366
|
+
if (!firstSliceGoal || deps.isWeakMissionAnchor(firstSliceGoal) || COOK_HANDOFF_NEGATIVE_MISSION_REGEX.test(firstSliceGoal)) {
|
|
1367
|
+
failures.push("first_slice_goal is not a bounded implementation slice");
|
|
1368
|
+
} else if (hasExplicitPlanningOnlyDeliverable([capsule.first_slice_goal]) || hasClearNoImplementationSignal([capsule.first_slice_goal])) {
|
|
1369
|
+
failures.push("first_slice_goal is planning-only instead of a repo-change slice");
|
|
1370
|
+
}
|
|
1371
|
+
if (capsule.implementation_surfaces.length === 0) failures.push("implementation_surfaces is empty");
|
|
1372
|
+
if (capsule.verification_commands.length === 0) failures.push("verification_commands is empty");
|
|
1373
|
+
return failures;
|
|
1302
1374
|
}
|
|
1303
1375
|
|
|
1304
|
-
function
|
|
1305
|
-
|
|
1306
|
-
|
|
1376
|
+
function buildNonStartableCookHandoffMessage(failures: string[]): string {
|
|
1377
|
+
return [
|
|
1378
|
+
"/cook failed closed because a fresh explicit primary-agent handoff exists, but it is not concrete enough to start implementation workflow yet.",
|
|
1379
|
+
"Tighten the handoff in the main chat so it names a bounded first implementation slice, repo-change-oriented acceptance, implementation_surfaces, and verification_commands, then rerun /cook.",
|
|
1380
|
+
`Blocking details: ${failures.join("; ")}.`,
|
|
1381
|
+
].join(" ");
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function isStartableCookHandoffCapsule(
|
|
1385
|
+
capsule: CookHandoffCapsule,
|
|
1386
|
+
deps: Pick<ProposalParseDeps, "normalizeMissionAnchorText" | "isWeakMissionAnchor">,
|
|
1307
1387
|
): boolean {
|
|
1308
|
-
|
|
1309
|
-
if (laterNonCommandMessages.length > COOK_HANDOFF_MAX_LATER_NON_COMMAND_MESSAGES) return true;
|
|
1310
|
-
return laterNonCommandMessages.some((entry) => {
|
|
1311
|
-
if (entry.role === "summary") return false;
|
|
1312
|
-
if (!hasRecentDiscussionImplementationIntent(entry.text, deps.stripCodeBlocks)) return false;
|
|
1313
|
-
return true;
|
|
1314
|
-
});
|
|
1388
|
+
return cookHandoffStartabilityFailures(capsule, deps).length === 0;
|
|
1315
1389
|
}
|
|
1316
1390
|
|
|
1317
1391
|
function cookHandoffIsFreshEnough(capsule: CookHandoffCapsule, laterMessages: RecentSessionMessage[]): boolean {
|
|
@@ -1347,6 +1421,11 @@ function buildContextProposalFromCookHandoffCapsule(
|
|
|
1347
1421
|
evaluationProfile: capsule.evaluation_profile,
|
|
1348
1422
|
critique: [
|
|
1349
1423
|
...capsule.notes,
|
|
1424
|
+
`First slice goal: ${capsule.first_slice_goal}`,
|
|
1425
|
+
...(capsule.first_slice_non_goals.length > 0 ? [`First slice non-goals: ${capsule.first_slice_non_goals.join(" | ")}`] : []),
|
|
1426
|
+
...(capsule.implementation_surfaces.length > 0 ? [`Implementation surfaces: ${capsule.implementation_surfaces.join(" | ")}`] : []),
|
|
1427
|
+
...(capsule.verification_commands.length > 0 ? [`Verification commands: ${capsule.verification_commands.join(" | ")}`] : []),
|
|
1428
|
+
`Why this slice first: ${capsule.why_this_slice_first}`,
|
|
1350
1429
|
...(capsule.why_cook_now ? [`Primary-agent /cook handoff rationale: ${capsule.why_cook_now}`] : []),
|
|
1351
1430
|
],
|
|
1352
1431
|
risks: capsule.risks,
|
|
@@ -1355,7 +1434,19 @@ function buildContextProposalFromCookHandoffCapsule(
|
|
|
1355
1434
|
suppressedCompletedTopics: [],
|
|
1356
1435
|
suppressedNegatedTopics: [],
|
|
1357
1436
|
},
|
|
1358
|
-
[
|
|
1437
|
+
[
|
|
1438
|
+
mission,
|
|
1439
|
+
goalText,
|
|
1440
|
+
capsule.mission,
|
|
1441
|
+
...capsule.scope,
|
|
1442
|
+
...constraints,
|
|
1443
|
+
...capsule.acceptance,
|
|
1444
|
+
capsule.first_slice_goal,
|
|
1445
|
+
...capsule.first_slice_non_goals,
|
|
1446
|
+
...capsule.implementation_surfaces,
|
|
1447
|
+
...capsule.verification_commands,
|
|
1448
|
+
capsule.why_this_slice_first,
|
|
1449
|
+
],
|
|
1359
1450
|
),
|
|
1360
1451
|
goalText,
|
|
1361
1452
|
basisPreview: buildCookHandoffBasisPreview(capsule),
|
|
@@ -1365,11 +1456,11 @@ function buildContextProposalFromCookHandoffCapsule(
|
|
|
1365
1456
|
return finalizeContextProposal(proposal, projectName, deps);
|
|
1366
1457
|
}
|
|
1367
1458
|
|
|
1368
|
-
export function
|
|
1459
|
+
export function assessLatestCookHandoffProposal(
|
|
1369
1460
|
recentMessages: RecentSessionMessage[],
|
|
1370
1461
|
projectName: string,
|
|
1371
1462
|
deps: ProposalParseDeps,
|
|
1372
|
-
):
|
|
1463
|
+
): CookHandoffProposalAssessment {
|
|
1373
1464
|
for (let index = 0; index < recentMessages.length; index += 1) {
|
|
1374
1465
|
const entry = recentMessages[index];
|
|
1375
1466
|
if (entry.role !== "assistant" || entry.isCommand) continue;
|
|
@@ -1379,12 +1470,27 @@ export function extractLatestCookHandoffProposal(
|
|
|
1379
1470
|
const capsule = capsules[capsuleIndex];
|
|
1380
1471
|
const laterMessages = recentMessages.slice(0, index);
|
|
1381
1472
|
if (!cookHandoffIsFreshEnough(capsule, laterMessages)) continue;
|
|
1382
|
-
|
|
1473
|
+
const failures = cookHandoffStartabilityFailures(capsule, deps);
|
|
1474
|
+
if (failures.length > 0) {
|
|
1475
|
+
return {
|
|
1476
|
+
status: "fresh_but_not_startable",
|
|
1477
|
+
message: buildNonStartableCookHandoffMessage(failures),
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1383
1480
|
const proposal = buildContextProposalFromCookHandoffCapsule(capsule, projectName, deps);
|
|
1384
|
-
if (proposal) return proposal;
|
|
1481
|
+
if (proposal) return { status: "startable", proposal };
|
|
1385
1482
|
}
|
|
1386
1483
|
}
|
|
1387
|
-
return
|
|
1484
|
+
return { status: "none" };
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
export function extractLatestCookHandoffProposal(
|
|
1488
|
+
recentMessages: RecentSessionMessage[],
|
|
1489
|
+
projectName: string,
|
|
1490
|
+
deps: ProposalParseDeps,
|
|
1491
|
+
): ContextProposal | undefined {
|
|
1492
|
+
const assessment = assessLatestCookHandoffProposal(recentMessages, projectName, deps);
|
|
1493
|
+
return assessment.status === "startable" ? assessment.proposal : undefined;
|
|
1388
1494
|
}
|
|
1389
1495
|
|
|
1390
1496
|
export async function deriveCookContextProposalFromRecentDiscussion(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linimin/pi-letscook",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.59",
|
|
4
4
|
"description": "Pi package for long-running completion workflows with canonical .agent state, role-based subagents, continuity, and verification helpers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
"subagent"
|
|
23
23
|
],
|
|
24
24
|
"files": [
|
|
25
|
+
".agent/README.md",
|
|
26
|
+
".agent/mission.md",
|
|
27
|
+
".agent/profile.json",
|
|
28
|
+
".agent/verify_completion_stop.sh",
|
|
29
|
+
".agent/verify_completion_control_plane.sh",
|
|
25
30
|
"extensions",
|
|
26
31
|
"skills",
|
|
27
32
|
"agents",
|
|
@@ -47,6 +47,49 @@ with session_path.open('w', encoding='utf-8') as fh:
|
|
|
47
47
|
PY
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
write_session_messages() {
|
|
51
|
+
local session_path="$1"
|
|
52
|
+
local cwd="$2"
|
|
53
|
+
local messages_json="$3"
|
|
54
|
+
python3 - "$session_path" "$cwd" "$messages_json" <<'PY'
|
|
55
|
+
import json
|
|
56
|
+
import sys
|
|
57
|
+
from pathlib import Path
|
|
58
|
+
|
|
59
|
+
session_path = Path(sys.argv[1])
|
|
60
|
+
cwd = sys.argv[2]
|
|
61
|
+
messages = json.loads(sys.argv[3])
|
|
62
|
+
session_path.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
entries = [
|
|
64
|
+
{
|
|
65
|
+
"type": "session",
|
|
66
|
+
"version": 3,
|
|
67
|
+
"id": "11111111-1111-4111-8111-111111111111",
|
|
68
|
+
"timestamp": "2026-01-01T00:00:00.000Z",
|
|
69
|
+
"cwd": cwd,
|
|
70
|
+
},
|
|
71
|
+
]
|
|
72
|
+
parent_id = None
|
|
73
|
+
for index, message in enumerate(messages, start=1):
|
|
74
|
+
entry_id = f"m{index:04d}"
|
|
75
|
+
entries.append({
|
|
76
|
+
"type": "message",
|
|
77
|
+
"id": entry_id,
|
|
78
|
+
"parentId": parent_id,
|
|
79
|
+
"timestamp": f"2026-01-01T00:00:{index:02d}.000Z",
|
|
80
|
+
"message": {
|
|
81
|
+
"role": message["role"],
|
|
82
|
+
"content": message["content"],
|
|
83
|
+
"timestamp": 1767225600000 + index * 1000,
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
parent_id = entry_id
|
|
87
|
+
with session_path.open('w', encoding='utf-8') as fh:
|
|
88
|
+
for entry in entries:
|
|
89
|
+
fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
90
|
+
PY
|
|
91
|
+
}
|
|
92
|
+
|
|
50
93
|
cd "$PKG_ROOT"
|
|
51
94
|
|
|
52
95
|
node <<'NODE'
|
|
@@ -94,11 +137,59 @@ NODE
|
|
|
94
137
|
ROOT="$TMPDIR/repo"
|
|
95
138
|
PROMPT="$TMPDIR/resume-prompt.txt"
|
|
96
139
|
BOOTSTRAP_SESSION="$TMPDIR/session-active-slice-bootstrap.jsonl"
|
|
97
|
-
|
|
140
|
+
BOOTSTRAP_MESSAGES="$(python3 - <<'PY'
|
|
141
|
+
import json
|
|
142
|
+
capsule = {
|
|
143
|
+
"kind": "cook_handoff",
|
|
144
|
+
"source": "primary_agent",
|
|
145
|
+
"captured_at": "2026-01-01T00:00:02.000Z",
|
|
146
|
+
"source_turn_id": "m0002",
|
|
147
|
+
"mission": "Exercise active-slice contract parity.",
|
|
148
|
+
"scope": [
|
|
149
|
+
"Bootstrap canonical completion files for the active-slice contract fixture.",
|
|
150
|
+
"Keep the fixture on the shipped explicit-handoff startup path."
|
|
151
|
+
],
|
|
152
|
+
"constraints": [
|
|
153
|
+
"Use supported bare /cook startup only."
|
|
154
|
+
],
|
|
155
|
+
"acceptance": [
|
|
156
|
+
"Materialize .agent/profile.json, .agent/state.json, .agent/plan.json, .agent/active-slice.json, and .agent/verification-evidence.json before the fixture rewrites them.",
|
|
157
|
+
"Keep scripts/active-slice-contract-test.sh aligned with the packaged startup contract."
|
|
158
|
+
],
|
|
159
|
+
"risks": [
|
|
160
|
+
"Active-slice fixture bootstrap must stay anchored to the fresh explicit handoff."
|
|
161
|
+
],
|
|
162
|
+
"notes": [
|
|
163
|
+
"This handoff exists only to scaffold canonical files before the fixture rewrites them for contract parity coverage."
|
|
164
|
+
],
|
|
165
|
+
"handoff_kind": "implementation_workflow_handoff",
|
|
166
|
+
"first_slice_goal": "Scaffold active-slice contract fixture files before rewriting them for parity verification.",
|
|
167
|
+
"first_slice_non_goals": [
|
|
168
|
+
"Do not broaden the fixture beyond active-slice contract surfaces."
|
|
169
|
+
],
|
|
170
|
+
"implementation_surfaces": [
|
|
171
|
+
".agent/active-slice.json",
|
|
172
|
+
"scripts/active-slice-contract-test.sh"
|
|
173
|
+
],
|
|
174
|
+
"verification_commands": [
|
|
175
|
+
"bash scripts/active-slice-contract-test.sh"
|
|
176
|
+
],
|
|
177
|
+
"why_this_slice_first": "The active-slice fixture cannot validate parity until canonical files exist.",
|
|
178
|
+
"task_type": "completion-workflow",
|
|
179
|
+
"evaluation_profile": "completion-rubric-v1",
|
|
180
|
+
"why_cook_now": "The fixture bootstrap is concrete enough to scaffold canonical control-plane files."
|
|
181
|
+
}
|
|
182
|
+
messages = [
|
|
183
|
+
{"role": "user", "content": "Prepare the active-slice contract bootstrap fixture and tell me when it is ready for /cook."},
|
|
184
|
+
{"role": "assistant", "content": "The active-slice contract bootstrap fixture is ready for /cook. Run /cook to confirm it.\n\n```cook_handoff\n" + json.dumps(capsule, ensure_ascii=False, indent=2) + "\n```"},
|
|
185
|
+
]
|
|
186
|
+
print(json.dumps(messages, ensure_ascii=False))
|
|
187
|
+
PY
|
|
188
|
+
)"
|
|
98
189
|
mkdir -p "$ROOT"
|
|
99
190
|
cd "$ROOT"
|
|
100
191
|
git init -q
|
|
101
|
-
|
|
192
|
+
write_session_messages "$BOOTSTRAP_SESSION" "$ROOT" "$BOOTSTRAP_MESSAGES"
|
|
102
193
|
|
|
103
194
|
PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
|
|
104
195
|
PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
|