@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.
@@ -18,14 +18,13 @@ import {
18
18
  collectRecentDiscussionEntries,
19
19
  collectRecentSessionMessages,
20
20
  deriveCookContextProposalFromRecentDiscussion,
21
- extractLatestCookHandoffProposal,
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" | "unclear";
130
+ action: "continue" | "refocus" | "blocked";
127
131
  currentMissionAnchor: string;
128
132
  proposal?: ContextProposal;
129
- reason: "matching_mission" | "clear_refocus" | "missing_proposal" | "ambiguous_discussion";
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 recent discussion did not produce a clear execution-ready startup brief with Mission/Scope/Constraints/Acceptance for concrete repo changes. Clarify the concrete repo changes in the main chat and rerun /cook.";
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<ContextProposal | undefined> {
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 = extractLatestCookHandoffProposal(recentMessages, projectName, {
388
- asString,
389
- asStringArray,
390
- assessMissionAnchor,
391
- normalizeMissionAnchorText,
392
- isWeakMissionAnchor,
393
- missionAnchorsStrictlyEquivalent,
394
- stripCodeBlocks,
395
- });
396
- if (explicitHandoff) return explicitHandoff;
397
- return await deriveCookContextProposalFromRecentDiscussion(projectName, recentEntries, {
398
- asString,
399
- asStringArray,
400
- workflowContext: snapshot
401
- ? {
402
- currentMissionAnchor:
403
- asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? asString(snapshot.active?.mission_anchor),
404
- latestCompletedSlice: asString(snapshot.state?.latest_completed_slice),
405
- latestVerifiedSlice: asString(snapshot.state?.latest_verified_slice),
406
- activeSliceGoal: asString(snapshot.active?.goal),
407
- activeSliceWhyNow: asString(snapshot.active?.why_now),
408
- verificationGoal: asString(snapshot.verificationEvidence?.goal),
409
- verificationSummary: asString(snapshot.verificationEvidence?.summary),
410
- continuationPolicy: asString(snapshot.state?.continuation_policy),
411
- }
412
- : undefined,
413
- analyzeContextProposal: async (entries) =>
414
- await analyzeContextProposalWithAgent({
415
- ctx,
416
- projectName,
417
- recentEntries: entries,
418
- workflowContextLines,
419
- liveRoleActivityByRoot,
420
- completionStatusKey: COMPLETION_STATUS_KEY,
421
- safeUiCall,
422
- getCtxCwd,
423
- getCtxHasUI,
424
- getCtxUi,
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: derive a startup brief from recent discussion, then start, continue, refocus, or start the next round from the explicit /cook command",
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, and propose implementation approaches.",
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 — stop short of long-running implementation and tell the user to run /cook.",
33
- "At that handoff point, do not begin long-running product implementation in ordinary chat, do not edit tracked product files for that workflow-level task, and do not act as though /cook had already been invoked.",
34
- "When handing off, explain that /cook will first look for a fresh explicit primary-agent handoff capsule and otherwise fall back to recent discussion.",
35
- "Also 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, and optional task_type/evaluation_profile/why_cook_now.",
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: "implementation_workflow_ready";
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) !== "implementation_workflow_ready") continue;
1277
+ if (deps.asString(parsed.handoff_kind) !== "implementation_workflow_handoff") continue;
1255
1278
  const mission = deps.asString(parsed.mission);
1256
- if (!mission) continue;
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: "implementation_workflow_ready",
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 = [capsule.mission, ...capsule.scope, ...capsule.constraints, ...capsule.non_goals, ...capsule.acceptance];
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 isStartableCookHandoffCapsule(
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
- ): boolean {
1355
+ ): string[] {
1356
+ const failures: string[] = [];
1297
1357
  const mission = deps.normalizeMissionAnchorText(capsule.mission);
1298
- if (!mission || deps.isWeakMissionAnchor(mission)) return false;
1299
- if (COOK_HANDOFF_NEGATIVE_MISSION_REGEX.test(mission)) return false;
1300
- if (capsule.scope.length === 0 || capsule.acceptance.length === 0) return false;
1301
- return true;
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 laterMessagesInvalidateCookHandoff(
1305
- laterMessages: RecentSessionMessage[],
1306
- deps: Pick<ProposalParseDeps, "stripCodeBlocks">,
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
- const laterNonCommandMessages = laterMessages.filter((entry) => !entry.isCommand);
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
- [mission, goalText, capsule.mission, ...capsule.scope, ...constraints, ...capsule.acceptance],
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 extractLatestCookHandoffProposal(
1459
+ export function assessLatestCookHandoffProposal(
1369
1460
  recentMessages: RecentSessionMessage[],
1370
1461
  projectName: string,
1371
1462
  deps: ProposalParseDeps,
1372
- ): ContextProposal | undefined {
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
- if (laterMessagesInvalidateCookHandoff(laterMessages, deps)) continue;
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 undefined;
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.57",
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
- BOOTSTRAP_DISCUSSION=$'Mission: Exercise active-slice contract parity.\nScope:\n- Bootstrap canonical completion files for the active-slice contract fixture.\nConstraints:\n- Use supported bare /cook startup only.\nAcceptance:\n- Materialize canonical files before the fixture rewrites them.'
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
- write_session "$BOOTSTRAP_SESSION" "$ROOT" "$BOOTSTRAP_DISCUSSION"
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 \