@linimin/pi-letscook 0.1.62 → 0.1.64

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/README.md CHANGED
@@ -34,8 +34,8 @@ Then run `/reload` in Pi.
34
34
  2. Run `/reload` in Pi.
35
35
  3. In the main chat, either implement directly with the agent or refine the concrete repo change you want.
36
36
  4. When you want workflow mode, run `/cook`.
37
- 5. Review the synthesized startup brief and choose **Start** or **Cancel**.
38
- 6. Later, run `/cook` again to resume from canonical state or confirm a synthesized replacement or next-round startup brief.
37
+ 5. Review the startup brief and choose **Start** or **Cancel**.
38
+ 6. Later, run `/cook` again to resume from canonical state or confirm a replacement or next-round handoff.
39
39
 
40
40
  ```text
41
41
  /cook
@@ -46,20 +46,20 @@ Then run `/reload` in Pi.
46
46
  | If you want to... | Do this |
47
47
  |---|---|
48
48
  | Implement directly without workflow | Ask in ordinary chat and let the agent modify the repo directly |
49
- | Start a tracked workflow | Discuss the concrete repo change in the main chat, then run `/cook` when you want workflow to begin |
49
+ | Start a tracked workflow | Discuss the concrete repo change in ordinary chat, then run `/cook` when you want workflow mode |
50
50
  | Continue the current workflow | Run `/cook` |
51
- | Refocus or start the next round | Discuss the new concrete repo change in the main chat, then run `/cook` when you want a new startup brief synthesized |
51
+ | Refocus or start the next round | Discuss the new concrete repo change in ordinary chat, then run `/cook` when you want workflow mode |
52
52
 
53
53
  ## What `/cook` expects
54
54
 
55
- - enough recent main-chat discussion for `/cook` to synthesize a concrete startup brief when you explicitly invoke it
56
- - a mission that is concrete enough to anchor bounded repo work rather than planning-only discussion
55
+ - enough current task context for a primary-agent handoff synthesis step to produce a concrete workflow startup handoff
56
+ - a mission and first slice concrete enough for the primary-agent handoff step to author a truthful implementation-startable handoff
57
57
  - acceptance and verification intent that can support a truthful first workflow round
58
58
  - README/CHANGELOG updates still count as concrete repo changes
59
- - assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts still do not count unless they can be turned into a concrete startup brief
60
- - optional explicit `cook_handoff` capsules may still be consumed as a compatibility intake path, but they are no longer required for new-workflow or next-round entry
59
+ - assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts still do not count unless the primary-agent handoff step turns them into a concrete `cook_handoff` capsule
60
+ - `/cook` first prefers a fresh explicit `cook_handoff` capsule when one already exists, but otherwise calls the primary-agent handoff synthesis step in the same `/cook` entry
61
61
 
62
- If `/cook` cannot derive a concrete startup brief, it fails closed, leaves canonical `.agent/**` state unchanged, and tells you to refine the mission, first slice, or verification intent in the main chat before rerunning `/cook`.
62
+ If the primary-agent handoff step still cannot prepare a concrete handoff, `/cook` fails closed, leaves canonical `.agent/**` state unchanged, and tells you to refine the mission, first slice, or verification intent in the main chat before rerunning `/cook`.
63
63
 
64
64
  If a fresh explicit handoff exists but is still workflow-worthy rather than implementation-startable, `/cook` also fails closed instead of silently treating that capsule as planning support or canonical workflow state.
65
65
 
@@ -69,16 +69,16 @@ If you pass inline arguments to `/cook`, it also fails closed and tells you to m
69
69
 
70
70
  Only explicit `/cook` enters workflow mode. Ordinary prompts stay in the main chat and go straight to the primary agent.
71
71
 
72
- Ordinary chat can still directly implement repo changes. `/cook` is for the cases where you want workflow control rather than just implementation help.
72
+ Ordinary chat can still directly implement repo changes. `/cook` is for the cases where you want workflow control rather than just implementation help, and the primary agent should prepare the handoff before you run it.
73
73
 
74
- When you explicitly run `/cook`, it synthesizes a startup brief from recent discussion using primary-agent-style context, then asks you to **Start** or **Cancel** before rewriting canonical workflow state.
74
+ When you explicitly run `/cook`, it first checks for a fresh explicit primary-agent handoff. If one is missing, it calls a same-entry primary-agent handoff synthesis step from the current task context, then asks you to **Start** or **Cancel** before rewriting canonical workflow state.
75
75
 
76
- Optional explicit `/cook` capsules may still be used as compatibility startup intake, but they are not required for new-workflow or next-round entry.
76
+ Explicit `/cook` capsules are still valid startup intake, but they are no longer the only path because `/cook` can synthesize the primary-agent handoff in the same entry when needed.
77
77
 
78
78
  Important behavior:
79
79
  - `/cook` is an optional workflow boundary and manual entry point
80
- - startup and next-round entry stay confirm-first, deriving startup from explicit user `/cook` entry plus recent discussion when needed
81
- - active workflows resume from canonical `.agent/**` state unless `/cook` synthesizes or receives a concrete replacement mission
80
+ - startup and next-round entry stay confirm-first, using explicit primary-agent handoff data when present and otherwise running the primary-agent handoff synthesis step in the same `/cook` entry
81
+ - active workflows resume from canonical `.agent/**` state unless a concrete replacement handoff is available or synthesized in the same `/cook` entry
82
82
  - explicit slash commands other than `/cook` continue normally in the main chat
83
83
  - ordinary main-chat discussion may clarify, propose, or directly implement repo changes without entering workflow mode
84
84
 
@@ -94,13 +94,13 @@ I want to add login redirect handling and tests.
94
94
 
95
95
  ## What happens when you run `/cook`
96
96
 
97
- `/cook` first checks for a fresh explicit primary-agent handoff capsule as a compatibility intake path. If none is present, `/cook` synthesizes a startup brief from recent discussion using primary-agent-style context. New-workflow entry and done-workflow next-round entry still fail closed when that synthesis is too weak or planning-only. When a workflow is already active and no concrete replacement mission is available, `/cook` resumes from canonical `.agent/**` state. None of this prevents ordinary-chat implementation when you choose not to enter workflow mode.
97
+ `/cook` first checks for a fresh explicit primary-agent handoff capsule. New-workflow entry and done-workflow next-round entry use that handoff when it already exists; otherwise `/cook` calls a same-entry primary-agent handoff synthesis step, then immediately continues to Start / Cancel if the generated handoff is concrete enough. Active workflows still resume canonical state by default unless a concrete replacement handoff is available or synthesized in the same `/cook` entry. None of this prevents ordinary-chat implementation when you choose not to enter workflow mode.
98
98
 
99
99
  | Repo state | What you'll see |
100
100
  |---|---|
101
- | No workflow yet | `/cook` synthesizes a startup brief from recent discussion and asks you to choose **Start** or **Cancel**. A fresh explicit handoff capsule may still be used if present. Weak, unreliable, stale, planning-only, or non-startable intake still fails closed. |
102
- | Active workflow exists | Usually a resume of the current workflow from canonical `.agent/**` state. If `/cook` finds a different concrete replacement mission from a compatibility capsule or deferred synthesis, it shows a chooser first and only rewrites canonical state after you confirm the replacement. Ambiguous intake stays conservative. |
103
- | Previous workflow is `done` | `/cook` can synthesize the next implementation round from recent discussion behind **Start** or **Cancel**. Weak or planning-only next-round intake still fails closed. |
101
+ | No workflow yet | `/cook` consumes a fresh explicit primary-agent handoff when one already exists, or synthesizes one from the primary-agent view in the same entry, then asks you to choose **Start** or **Cancel**. Stale, planning-only, or non-startable handoffs still fail closed. |
102
+ | Active workflow exists | Usually a resume of the current workflow from canonical `.agent/**` state. If a concrete replacement handoff exists already or is synthesized in the same `/cook` entry and points to a different mission, `/cook` shows a chooser first and only rewrites canonical state after you confirm the replacement. Ambiguous or missing replacement handoff stays conservative. |
103
+ | Previous workflow is `done` | `/cook` can start the next implementation round from a fresh explicit primary-agent handoff or from the same-entry primary-agent handoff synthesis step behind **Start** or **Cancel**. Weak or planning-only next-round handoffs still fail closed. |
104
104
 
105
105
  ## Confirmation and fail-closed behavior
106
106
 
@@ -38,7 +38,7 @@ type ContextProposalAlternate = {
38
38
  analysis: ContextProposalAnalysis;
39
39
  goalText: string;
40
40
  basisPreview: string;
41
- source: "session" | "analyst" | "handoff_capsule" | "deferred_primary_agent_handoff";
41
+ source: "session" | "analyst" | "handoff_capsule";
42
42
  };
43
43
 
44
44
  type ContextProposal = ContextProposalAlternate & {
@@ -60,7 +60,7 @@ type CookContextProposalResult = {
60
60
  blockedFailureMessage?: string;
61
61
  };
62
62
 
63
- function buildCookStartupDerivationFailureMessage(deps: CompletionDriverDeps, prefix?: string): string {
63
+ function buildCookStartupBriefRequiredMessage(deps: CompletionDriverDeps, prefix?: string): string {
64
64
  const requirement = deps.structuredDiscussionFailureDetail;
65
65
  return prefix ? `${prefix} ${requirement}` : requirement;
66
66
  }
@@ -72,10 +72,9 @@ type ActiveWorkflowProposalAssessment = {
72
72
  blockedFailureMessage?: string;
73
73
  reason:
74
74
  | "matching_mission"
75
- | "no_replacement_proposal"
76
- | "explicit_handoff_replacement"
77
- | "deferred_replacement"
78
- | "replacement_not_startable";
75
+ | "missing_explicit_handoff"
76
+ | "fresh_explicit_handoff"
77
+ | "fresh_explicit_handoff_not_startable";
79
78
  };
80
79
 
81
80
  type ExistingWorkflowChooserOptions = {
@@ -183,6 +182,7 @@ function buildCookStructuredDiscussionFailureMessage(deps: CompletionDriverDeps,
183
182
  return prefix ? `${prefix} ${deps.structuredDiscussionFailureDetail}` : deps.structuredDiscussionFailureDetail;
184
183
  }
185
184
 
185
+
186
186
  export function completionContinuationFingerprint(snapshot: CompletionStateSnapshot): string | undefined {
187
187
  if (asString(snapshot.state?.continuation_policy) !== "continue") return undefined;
188
188
  const nextMandatoryRole = asString(snapshot.state?.next_mandatory_role);
@@ -321,23 +321,23 @@ async function assessActiveWorkflowProposalRouting(
321
321
  ): Promise<ActiveWorkflowProposalAssessment> {
322
322
  const currentMission = currentMissionAnchor(snapshot);
323
323
  const projectName = path.basename(snapshot.files.root);
324
- const derived = await deps.deriveCookContextProposal(ctx, projectName);
325
- if (derived.blockedFailureMessage) {
324
+ const proposalResult = await deps.deriveCookContextProposal(ctx, projectName);
325
+ if (proposalResult.blockedFailureMessage) {
326
326
  const assessment: ActiveWorkflowProposalAssessment = {
327
327
  action: "blocked",
328
328
  currentMissionAnchor: currentMission,
329
- blockedFailureMessage: derived.blockedFailureMessage,
330
- reason: "replacement_not_startable",
329
+ blockedFailureMessage: proposalResult.blockedFailureMessage,
330
+ reason: "fresh_explicit_handoff_not_startable",
331
331
  };
332
332
  deps.maybeWriteActiveWorkflowRoutingSnapshot(assessment);
333
333
  return assessment;
334
334
  }
335
- const proposal = derived.proposal;
335
+ const proposal = proposalResult.proposal;
336
336
  if (!proposal) {
337
337
  const assessment: ActiveWorkflowProposalAssessment = {
338
338
  action: "continue",
339
339
  currentMissionAnchor: currentMission,
340
- reason: "no_replacement_proposal",
340
+ reason: "missing_explicit_handoff",
341
341
  };
342
342
  deps.maybeWriteActiveWorkflowRoutingSnapshot(assessment);
343
343
  return assessment;
@@ -356,7 +356,7 @@ async function assessActiveWorkflowProposalRouting(
356
356
  action: "refocus",
357
357
  currentMissionAnchor: currentMission,
358
358
  proposal,
359
- reason: proposal.source === "handoff_capsule" ? "explicit_handoff_replacement" : "deferred_replacement",
359
+ reason: "fresh_explicit_handoff",
360
360
  };
361
361
  deps.maybeWriteActiveWorkflowRoutingSnapshot(assessment);
362
362
  return assessment;
@@ -536,14 +536,14 @@ export async function runCookEntry(
536
536
  if (!snapshot) {
537
537
  const root = findRepoRoot(cwd) ?? cwd;
538
538
  const projectName = path.basename(root);
539
- const derived = await deps.deriveCookStartupProposal(ctx, projectName);
539
+ const derived = await deps.deriveCookContextProposal(ctx, projectName);
540
540
  if (derived.blockedFailureMessage) {
541
541
  deps.emitCommandText(ctx, derived.blockedFailureMessage, "info");
542
542
  return;
543
543
  }
544
544
  const proposal = derived.proposal;
545
545
  if (!proposal) {
546
- deps.emitCommandText(ctx, buildCookStartupDerivationFailureMessage(deps), "info");
546
+ deps.emitCommandText(ctx, buildCookStartupBriefRequiredMessage(deps), "info");
547
547
  return;
548
548
  }
549
549
  const decision = await deps.confirmContextProposal(ctx, proposal, {
@@ -581,14 +581,14 @@ export async function runCookEntry(
581
581
  if (!goal) {
582
582
  if (workflowDone) {
583
583
  const projectName = path.basename(snapshot.files.root);
584
- const derived = await deps.deriveCookStartupProposal(ctx, projectName);
584
+ const derived = await deps.deriveCookContextProposal(ctx, projectName);
585
585
  if (derived.blockedFailureMessage) {
586
586
  deps.emitCommandText(ctx, derived.blockedFailureMessage, "info");
587
587
  return;
588
588
  }
589
589
  const proposal = derived.proposal;
590
590
  if (!proposal) {
591
- deps.emitCommandText(ctx, buildCookStartupDerivationFailureMessage(deps, "The previous completion workflow is already done."), "info");
591
+ deps.emitCommandText(ctx, buildCookStartupBriefRequiredMessage(deps, "The previous completion workflow is already done."), "info");
592
592
  return;
593
593
  }
594
594
  const decision = await deps.confirmContextProposal(ctx, proposal, {
@@ -610,13 +610,7 @@ export async function runCookEntry(
610
610
  buildAdvisoryStartupBrief({ proposal, analysis: decision.analysis }),
611
611
  );
612
612
  snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
613
- deps.emitCommandText(
614
- ctx,
615
- proposal.source === "handoff_capsule"
616
- ? `Started a new completion workflow round from explicit primary-agent handoff: ${decision.missionAnchor}`
617
- : `Started a new completion workflow round from deferred primary-agent handoff: ${decision.missionAnchor}`,
618
- "info",
619
- );
613
+ deps.emitCommandText(ctx, `Started a new completion workflow round from explicit primary-agent handoff: ${decision.missionAnchor}`, "info");
620
614
  } else {
621
615
  const assessment = await assessActiveWorkflowProposalRouting(ctx, snapshot, deps);
622
616
  if (assessment.action === "blocked") {
@@ -627,29 +621,20 @@ export async function runCookEntry(
627
621
  await resumeActiveWorkflowFromCanonicalState(pi, ctx, snapshot, deps);
628
622
  return;
629
623
  }
630
- const explicitReplacement = assessment.reason === "explicit_handoff_replacement";
631
- const deferredReplacement = assessment.reason === "deferred_replacement";
624
+ const explicitReplacement = assessment.reason === "fresh_explicit_handoff";
632
625
  const decision = await confirmExistingWorkflowProposal(ctx, snapshot, assessment.proposal, deps, {
633
626
  intro: explicitReplacement
634
627
  ? "A fresh explicit primary-agent handoff proposes replacing the current workflow. Choose how /cook should proceed:"
635
- : deferredReplacement
636
- ? "A deferred primary-agent handoff synthesized from your recent discussion proposes replacing the current workflow. Choose how /cook should proceed:"
637
- : "A replacement workflow is ready. Choose how /cook should proceed:",
628
+ : "A replacement workflow is ready. Choose how /cook should proceed:",
638
629
  proposedMissionLabel: explicitReplacement
639
630
  ? "Proposed mission from explicit primary-agent handoff"
640
- : deferredReplacement
641
- ? "Proposed mission from deferred primary-agent handoff"
642
- : "Proposed mission",
631
+ : "Proposed mission",
643
632
  refocusChoiceLabel: explicitReplacement
644
633
  ? "Start new workflow from explicit primary-agent handoff\n\nReview the proposed replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state."
645
- : deferredReplacement
646
- ? "Start new workflow from deferred primary-agent handoff\n\nReview the proposed replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state."
647
- : "Start new workflow\n\nReview the proposed replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state.",
634
+ : "Start new workflow\n\nReview the proposed replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state.",
648
635
  alternateChoiceLabel: explicitReplacement
649
636
  ? "Start alternate workflow from explicit primary-agent handoff\n\nReview this alternate replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state."
650
- : deferredReplacement
651
- ? "Start alternate workflow from deferred primary-agent handoff\n\nReview this alternate replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state."
652
- : undefined,
637
+ : undefined,
653
638
  comparison: "strict",
654
639
  });
655
640
  if (!decision) {
@@ -662,11 +647,9 @@ export async function runCookEntry(
662
647
  }
663
648
  const selectedProposal = decision.proposal;
664
649
  const proposalDecision = await deps.confirmContextProposal(ctx, selectedProposal, {
665
- title: explicitReplacement
650
+ title: assessment.reason === "fresh_explicit_handoff"
666
651
  ? "Start the replacement workflow from this explicit startup brief?"
667
- : deferredReplacement
668
- ? "Start the replacement workflow from this deferred startup brief?"
669
- : "Start the replacement workflow from this startup brief?",
652
+ : "Start the replacement workflow from this startup brief?",
670
653
  });
671
654
  if (!proposalDecision) {
672
655
  deps.emitCommandText(ctx, buildCookCancellationMessage("Cancelled replacement workflow proposal", deps), "info");
@@ -686,11 +669,9 @@ export async function runCookEntry(
686
669
  snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
687
670
  deps.emitCommandText(
688
671
  ctx,
689
- explicitReplacement
672
+ assessment.reason === "fresh_explicit_handoff"
690
673
  ? `Refocused completion mission from explicit primary-agent handoff to: ${proposalDecision.missionAnchor}`
691
- : deferredReplacement
692
- ? `Refocused completion mission from deferred primary-agent handoff to: ${proposalDecision.missionAnchor}`
693
- : `Refocused completion mission to: ${proposalDecision.missionAnchor}`,
674
+ : `Refocused completion mission to: ${proposalDecision.missionAnchor}`,
694
675
  "info",
695
676
  );
696
677
  }
@@ -17,7 +17,6 @@ import {
17
17
  assessMissionAnchor,
18
18
  collectRecentDiscussionEntries,
19
19
  collectRecentSessionMessages,
20
- deriveCookContextProposalFromRecentDiscussion,
21
20
  assessLatestCookHandoffProposal,
22
21
  finalizeContextProposalAnalysis,
23
22
  isWeakMissionAnchor,
@@ -25,7 +24,6 @@ import {
25
24
  missionAnchorsStrictlyEquivalent,
26
25
  normalizeMissionAnchorText,
27
26
  resolveContextProposalConfirmationAction,
28
- retagContextProposalSource,
29
27
  stripCodeBlocks,
30
28
  } from "./proposal";
31
29
  import type {
@@ -49,7 +47,7 @@ import {
49
47
  maybeWriteContextProposalSnapshot,
50
48
  } from "./prompt-surfaces";
51
49
  import { toolCallBlockReason } from "./policy-guards";
52
- import { analyzeContextProposalWithAgent, runCompletionRole } from "./role-runner";
50
+ import { analyzeContextProposalWithAgent, generateCookHandoffWithAgent, runCompletionRole } from "./role-runner";
53
51
  import {
54
52
  applyLiveRoleEvent,
55
53
  buildInlineRunningLines,
@@ -135,10 +133,9 @@ type ActiveWorkflowProposalAssessment = {
135
133
  blockedFailureMessage?: string;
136
134
  reason:
137
135
  | "matching_mission"
138
- | "no_replacement_proposal"
139
- | "explicit_handoff_replacement"
140
- | "deferred_replacement"
141
- | "replacement_not_startable";
136
+ | "missing_explicit_handoff"
137
+ | "fresh_explicit_handoff"
138
+ | "fresh_explicit_handoff_not_startable";
142
139
  };
143
140
 
144
141
  function completionTestWorkflowActionOverride(): "continue" | "refocus" | "cancel" | undefined {
@@ -212,7 +209,7 @@ function maybeWriteTestSnapshot(targetPath: string | undefined, content: string)
212
209
 
213
210
  const COOK_MAIN_CHAT_RERUN_GUIDANCE = "Discuss changes in the main chat and rerun /cook.";
214
211
  const COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL =
215
- "/cook failed closed because it could not derive a concrete startup brief from recent discussion. Clarify the mission, first slice, or verification intent in the main chat, then rerun /cook.";
212
+ "/cook failed closed because the primary-agent handoff step could not prepare a concrete startup handoff from the current task context. Clarify the mission, first slice, or verification intent in the main chat, then rerun /cook.";
216
213
 
217
214
  function isWorkflowDone(snapshot: CompletionStateSnapshot | undefined): boolean {
218
215
  return asString(snapshot?.state?.continuation_policy) === "done";
@@ -379,15 +376,37 @@ function stripCookHandoffBlocks(text: string): string {
379
376
  return text.replace(COOK_HANDOFF_BLOCK_REGEX, " ").replace(/\s+/g, " ").trim();
380
377
  }
381
378
 
382
- async function deriveCookRecentDiscussionProposal(
379
+ async function deriveCookStartupProposal(
383
380
  ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
384
381
  projectName: string,
385
- ): Promise<ContextProposal | undefined> {
382
+ ): Promise<CookContextProposalResult> {
383
+ const recentMessages = collectRecentSessionMessages(ctx, { isRecord, asString, asNumber, isStaleContextError });
384
+ const explicitHandoff = assessLatestCookHandoffProposal(recentMessages, projectName, {
385
+ asString,
386
+ asStringArray,
387
+ assessMissionAnchor,
388
+ normalizeMissionAnchorText,
389
+ isWeakMissionAnchor,
390
+ missionAnchorsStrictlyEquivalent,
391
+ stripCodeBlocks,
392
+ });
393
+ if (explicitHandoff.status === "startable") return { proposal: explicitHandoff.proposal };
394
+ if (explicitHandoff.status === "fresh_but_not_startable") {
395
+ return { blockedFailureMessage: explicitHandoff.message };
396
+ }
397
+ return {};
398
+ }
399
+
400
+ async function deriveCookContextProposal(
401
+ ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
402
+ projectName: string,
403
+ ): Promise<CookContextProposalResult> {
404
+ const explicit = await deriveCookStartupProposal(ctx, projectName);
405
+ if (explicit.proposal || explicit.blockedFailureMessage) return explicit;
386
406
  const recentMessages = collectRecentSessionMessages(ctx, { isRecord, asString, asNumber, isStaleContextError });
387
407
  const recentEntries = recentMessages
388
- .filter((entry) => (entry.role === "user" || entry.role === "custom") && !entry.isCommand)
389
- .filter((entry) => !/```cook_handoff\b/i.test(entry.text))
390
- .slice(0, 8)
408
+ .filter((entry) => !entry.isCommand && (entry.role === "user" || entry.role === "assistant" || entry.role === "custom" || entry.role === "summary"))
409
+ .slice(0, 12)
391
410
  .map((entry) => ({ role: entry.role, text: stripCookHandoffBlocks(entry.text) }))
392
411
  .filter((entry) => entry.text.length > 0);
393
412
  const snapshot = await loadCompletionSnapshot(getCtxCwd(ctx));
@@ -403,50 +422,22 @@ async function deriveCookRecentDiscussionProposal(
403
422
  `verification summary: ${asString(snapshot.verificationEvidence?.summary) ?? "(none)"}`,
404
423
  ]
405
424
  : [];
406
- const proposal = await deriveCookContextProposalFromRecentDiscussion(projectName, recentEntries, {
407
- asString,
408
- asStringArray,
409
- workflowContext: snapshot
410
- ? {
411
- currentMissionAnchor:
412
- asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? asString(snapshot.active?.mission_anchor),
413
- latestCompletedSlice: asString(snapshot.state?.latest_completed_slice),
414
- latestVerifiedSlice: asString(snapshot.state?.latest_verified_slice),
415
- activeSliceGoal: asString(snapshot.active?.goal),
416
- activeSliceWhyNow: asString(snapshot.active?.why_now),
417
- verificationGoal: asString(snapshot.verificationEvidence?.goal),
418
- verificationSummary: asString(snapshot.verificationEvidence?.summary),
419
- continuationPolicy: asString(snapshot.state?.continuation_policy),
420
- }
421
- : undefined,
422
- analyzeContextProposal: async (entries) =>
423
- await analyzeContextProposalWithAgent({
424
- ctx,
425
- projectName,
426
- recentEntries: entries,
427
- workflowContextLines,
428
- liveRoleActivityByRoot,
429
- completionStatusKey: COMPLETION_STATUS_KEY,
430
- safeUiCall,
431
- getCtxCwd,
432
- getCtxHasUI,
433
- getCtxUi,
434
- }),
435
- assessMissionAnchor,
436
- isWeakMissionAnchor,
437
- missionAnchorsStrictlyEquivalent,
438
- normalizeMissionAnchorText,
439
- stripCodeBlocks,
425
+ const raw = await generateCookHandoffWithAgent({
426
+ ctx,
427
+ projectName,
428
+ recentEntries,
429
+ workflowContextLines,
430
+ liveRoleActivityByRoot,
431
+ completionStatusKey: COMPLETION_STATUS_KEY,
432
+ safeUiCall,
433
+ getCtxCwd,
434
+ getCtxHasUI,
435
+ getCtxUi,
440
436
  });
441
- return retagContextProposalSource(proposal, "deferred_primary_agent_handoff");
442
- }
443
-
444
- async function deriveCookStartupProposal(
445
- ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
446
- projectName: string,
447
- ): Promise<CookContextProposalResult> {
448
- const recentMessages = collectRecentSessionMessages(ctx, { isRecord, asString, asNumber, isStaleContextError });
449
- const explicitHandoff = assessLatestCookHandoffProposal(recentMessages, projectName, {
437
+ if (!raw) return {};
438
+ const generated = assessLatestCookHandoffProposal([
439
+ { role: "assistant", text: raw, messageId: "generated-primary-agent-handoff", timestampMs: Date.now(), isCommand: false },
440
+ ], projectName, {
450
441
  asString,
451
442
  asStringArray,
452
443
  assessMissionAnchor,
@@ -455,18 +446,9 @@ async function deriveCookStartupProposal(
455
446
  missionAnchorsStrictlyEquivalent,
456
447
  stripCodeBlocks,
457
448
  });
458
- if (explicitHandoff.status === "startable") return { proposal: explicitHandoff.proposal };
459
- if (explicitHandoff.status === "fresh_but_not_startable") {
460
- return { blockedFailureMessage: explicitHandoff.message };
461
- }
462
- return { proposal: await deriveCookRecentDiscussionProposal(ctx, projectName) };
463
- }
464
-
465
- async function deriveCookContextProposal(
466
- ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
467
- projectName: string,
468
- ): Promise<CookContextProposalResult> {
469
- return await deriveCookStartupProposal(ctx, projectName);
449
+ if (generated.status === "startable") return { proposal: generated.proposal };
450
+ if (generated.status === "fresh_but_not_startable") return { blockedFailureMessage: generated.message };
451
+ return {};
470
452
  }
471
453
 
472
454
  async function confirmContextProposal(
@@ -942,7 +924,7 @@ export default function completionExtension(pi: ExtensionAPI) {
942
924
  structuredDiscussionFailureDetail: COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL,
943
925
  mainChatRerunGuidance: COOK_MAIN_CHAT_RERUN_GUIDANCE,
944
926
  cookCommandSpec: {
945
- description: "/cook workflow: optionally enter tracked workflow mode, synthesize a startup brief from explicit /cook entry, resume the current workflow from canonical state, or confirm a replacement mission",
927
+ description: "/cook workflow: start or replace workflow only from an explicit primary-agent handoff, or resume the current workflow from canonical state",
946
928
  },
947
929
  buildContextProposalContinuationReason,
948
930
  completionKickoff,
@@ -32,7 +32,8 @@ export function buildCookHandoffBoundaryReminder(): string {
32
32
  "/cook is optional workflow mode for resumability, review, audit, canonical .agent state, or deliberate multi-session control; it is not required just to edit repo files in ordinary chat.",
33
33
  "If the user wants direct implementation now, stay in ordinary chat and help directly instead of blocking on /cook.",
34
34
  "If the user asks follow-up questions or wants to keep refining scope, continue helping naturally in ordinary chat.",
35
- "If the user explicitly runs /cook, /cook will synthesize a startup brief from recent discussion using primary-agent-style context, then show Start/Cancel confirmation before canonical workflow state is rewritten.",
35
+ "If the user explicitly runs /cook, the extension should call a primary-agent handoff synthesis step from the current task context, then show Start/Cancel confirmation without making the user rerun /cook.",
36
+ "Do not expect /cook to infer or guess startup intent from recent discussion alone; /cook should use explicit primary-agent handoff data, whether it already exists or is synthesized in the same /cook entry.",
36
37
  "Only provide a preview startup brief or ```cook_handoff``` capsule in ordinary chat when the user explicitly asks for that preview behavior.",
37
38
  "Any preview capsule is startup intake for /cook only: do not present it as canonical .agent state, an active slice, or a persistent repo contract.",
38
39
  "When you continue in ordinary chat, do not pretend /cook already started and do not silently rewrite discussion into canonical workflow state.",
@@ -67,6 +67,8 @@ export type AnalyzeContextProposalWithAgentParams = {
67
67
  getCtxUi: <T extends { ui: any }>(ctx: T) => any | undefined;
68
68
  };
69
69
 
70
+ export type GenerateCookHandoffWithAgentParams = AnalyzeContextProposalWithAgentParams;
71
+
70
72
  const AGENT_HOME = path.join(os.homedir(), ".pi", "agent");
71
73
  const EXTENSION_DIR = typeof __dirname === "string" ? __dirname : process.cwd();
72
74
  const PACKAGE_ROOT_CANDIDATE = path.resolve(EXTENSION_DIR, "..", "..");
@@ -94,6 +96,18 @@ const CONTEXT_PROPOSAL_ANALYST_SYSTEM_PROMPT = [
94
96
  const STARTUP_ANALYST_ROLE = "cook-proposal-analyst";
95
97
  const ANALYST_HEARTBEAT_MS = 5_000;
96
98
 
99
+ const PRIMARY_AGENT_HANDOFF_SYSTEM_PROMPT = [
100
+ "You are the primary agent preparing an explicit /cook handoff after the user already chose workflow mode.",
101
+ "Return either exactly one fenced ```cook_handoff JSON block or one brief plain sentence explaining why no concrete handoff can be prepared.",
102
+ "If you can prepare a handoff, the JSON must use kind cook_handoff, source primary_agent, and handoff_kind implementation_workflow_handoff.",
103
+ "When the user has clearly accepted a concrete assistant-proposed slice, carry that slice forward into the handoff instead of broadening or re-guessing the mission.",
104
+ "Do not make /cook infer or rediscover the mission from recent discussion later; author the handoff now from the primary-agent view of the task.",
105
+ "Do not emit markdown commentary before or after the capsule.",
106
+ "If the task is not concrete enough for implementation workflow, do not invent the slice.",
107
+ "A valid implementation-ready handoff must include mission, scope, constraints or non_goals, acceptance, risks, notes, first_slice_goal, first_slice_non_goals, implementation_surfaces, verification_commands, and why_this_slice_first.",
108
+ ].join(" ");
109
+ const PRIMARY_AGENT_HANDOFF_ROLE = "cook-primary-agent-handoff";
110
+
97
111
  class StartupAnalystOverlay extends Container {
98
112
  private readonly border: DynamicBorder;
99
113
  private readonly title: Text;
@@ -309,6 +323,97 @@ async function runContextProposalAnalystSubprocess(params: AnalyzeContextProposa
309
323
  return await run();
310
324
  }
311
325
 
326
+ function serializeRecentConversationEntries(entries: RecentDiscussionEntry[]): string {
327
+ return entries
328
+ .slice()
329
+ .reverse()
330
+ .map((entry, index) => `[${index + 1}] ${entry.role.toUpperCase()}\n${entry.text}`)
331
+ .join("\n\n");
332
+ }
333
+
334
+ function buildPrimaryAgentHandoffPrompt(projectName: string, recentEntries: RecentDiscussionEntry[], workflowContextLines: string[] = []): string {
335
+ const lines = [
336
+ `Project: ${projectName}`,
337
+ "",
338
+ "Recent session transcript:",
339
+ serializeRecentConversationEntries(recentEntries),
340
+ ];
341
+ if (workflowContextLines.length > 0) lines.push("", "Canonical workflow context:", ...workflowContextLines);
342
+ lines.push(
343
+ "",
344
+ "Task:",
345
+ "The user explicitly invoked /cook. Prepare the primary-agent handoff that /cook should consume immediately for Start/Cancel confirmation.",
346
+ );
347
+ return lines.join("\n");
348
+ }
349
+
350
+ async function runPrimaryAgentHandoffSubprocess(params: GenerateCookHandoffWithAgentParams): Promise<string | undefined> {
351
+ const { ctx, projectName, recentEntries } = params;
352
+ const modelArg = contextProposalAnalystModelArg(ctx.model);
353
+ if (!modelArg) return undefined;
354
+ const cwd = params.getCtxCwd(ctx);
355
+ const runCwd = findCompletionRoot(cwd) ?? findRepoRoot(cwd) ?? cwd;
356
+ const prompt = buildPrimaryAgentHandoffPrompt(projectName, recentEntries, params.workflowContextLines ?? []);
357
+ const systemPromptTemp = await writeTempFile(runCwd, "pi-cook-primary-agent-handoff-", PRIMARY_AGENT_HANDOFF_SYSTEM_PROMPT);
358
+ const args: string[] = ["--mode", "json", "-p", "--no-session", "--no-extensions", "--append-system-prompt", systemPromptTemp.filePath, "--model", modelArg, prompt];
359
+ const invocation = getPiInvocation(args);
360
+ const liveActivity = createLiveRoleActivity(PRIMARY_AGENT_HANDOFF_ROLE);
361
+ liveActivity.progress = "Preparing primary-agent /cook handoff";
362
+ liveActivity.currentAction = "Authoring explicit startup handoff from current task context";
363
+ liveActivity.assistantSummary = liveActivity.progress;
364
+ try {
365
+ const output = await new Promise<string | undefined>((resolve) => {
366
+ const proc = spawn(invocation.command, invocation.args, {
367
+ cwd: runCwd,
368
+ env: process.env,
369
+ stdio: ["ignore", "pipe", "pipe"],
370
+ shell: false,
371
+ });
372
+ let buffer = "";
373
+ const messages: RoleMessage[] = [];
374
+ const processLine = (line: string) => {
375
+ if (!line.trim()) return;
376
+ try {
377
+ const event = JSON.parse(line) as JsonRecord;
378
+ applyLiveRoleEvent(liveActivity, event, messages);
379
+ } catch {
380
+ // ignore malformed lines
381
+ }
382
+ };
383
+ proc.stdout.on("data", (chunk) => {
384
+ buffer += chunk.toString();
385
+ const lines = buffer.split("\n");
386
+ buffer = lines.pop() ?? "";
387
+ for (const line of lines) processLine(line);
388
+ });
389
+ proc.stderr.on("data", () => {
390
+ // ignore stderr unless no assistant output arrives
391
+ });
392
+ proc.on("close", (code) => {
393
+ if (buffer.trim()) processLine(buffer);
394
+ resolve(code === 0 ? liveActivity.lastAssistantText?.trim() || undefined : undefined);
395
+ });
396
+ proc.on("error", () => resolve(undefined));
397
+ });
398
+ return output;
399
+ } finally {
400
+ await fsp.rm(systemPromptTemp.dir, { recursive: true, force: true });
401
+ }
402
+ }
403
+
404
+ export async function generateCookHandoffWithAgent(params: GenerateCookHandoffWithAgentParams): Promise<string | undefined> {
405
+ if (process.env.PI_COMPLETION_DISABLE_PRIMARY_HANDOFF_SYNTHESIS === "1") return undefined;
406
+ const testOutput = asString(process.env.PI_COMPLETION_PRIMARY_HANDOFF_OUTPUT);
407
+ if (testOutput) return testOutput;
408
+ if (params.recentEntries.length === 0) return undefined;
409
+ try {
410
+ return await runPrimaryAgentHandoffSubprocess(params);
411
+ } catch (error) {
412
+ console.warn("[completion] primary-agent handoff generation failed", error);
413
+ return undefined;
414
+ }
415
+ }
416
+
312
417
  export async function analyzeContextProposalWithAgent(params: AnalyzeContextProposalWithAgentParams): Promise<ContextProposal | undefined> {
313
418
  if (process.env.PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST === "1") return undefined;
314
419
  const testOutput = asString(process.env.PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linimin/pi-letscook",
3
- "version": "0.1.62",
3
+ "version": "0.1.64",
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,