@linimin/pi-letscook 0.1.59 → 0.1.61

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/mission.md CHANGED
@@ -3,6 +3,6 @@
3
3
  Project: pi-letscook
4
4
 
5
5
  Mission anchor:
6
- Implement the primary-agent /cook handoff pipeline refactor so /cook only starts implementation workflow from structurally startable handoffs, while preserving explicit handoff as the preferred startup-intake path.
6
+ Refactor the /cook startup boundary into the agreed mixed model: ordinary chat stays advisory-first by default with no default pre-/cook handoff capsule formation, while explicit /cook performs structured startup synthesis from recent discussion and preserves the approval-only Start/Cancel gate.
7
7
 
8
8
  This file is a tracked human-readable statement of the repo's completion mission. Re-grounders may refine this file when repo truth becomes clearer, but it must stay truthful to shipped behavior and the active completion objective.
package/CHANGELOG.md CHANGED
@@ -2,14 +2,13 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
- ## 0.1.59
5
+ ## 0.1.61
6
6
 
7
7
  ### Changed
8
8
 
9
- - relaxed the pre-`/cook` ordinary-chat boundary so the primary agent can keep discussing and refining requirements before explicit `/cook` instead of switching into a hard handoff-only refusal mode as soon as workflow-worthiness is detected
10
- - kept `/cook` as the only explicit workflow boundary, while teaching the pre-`/cook` prompt surfaces to recommend `/cook` advisory-first and only emit implementation-ready capsules once the first bounded slice is concrete enough
11
- - made fresh explicit `/cook` handoffs reusable from recent ordinary-chat discussion instead of requiring the immediately preceding turn, so Cancel can return users cleanly to ordinary discussion before they rerun `/cook`
12
- - kept bare `/cook` startup and done-workflow next-round entry fail-closed on missing or non-startable explicit handoffs, while active workflows still resume from canonical `.agent/**` state unless a fresh explicit handoff proposes replacement
9
+ - removed proactive primary-agent `/cook` prompting and default ordinary-chat `cook_handoff` emission so main chat stays advisory until the user explicitly runs `/cook`
10
+ - changed bare `/cook` startup and done-workflow next-round entry to synthesize a deferred primary-agent startup brief from recent discussion instead of requiring a pre-authored explicit handoff capsule
11
+ - kept active-workflow bare `/cook` resumable from canonical `.agent/**` state by default while allowing `/cook` to confirm a concrete replacement mission derived from explicit entry context
13
12
  - updated public parity and shipped package contents so the tracked `.agent` contract files are included in package tarballs and packaged smoke/release verification can scaffold canonical state truthfully
14
13
 
15
14
  ## 0.1.58
package/README.md CHANGED
@@ -30,10 +30,10 @@ Then run `/reload` in Pi.
30
30
  1. Install the package:
31
31
  `pi install npm:@linimin/pi-letscook`
32
32
  2. Run `/reload` in Pi.
33
- 3. In the main chat, describe the concrete repo change you want and let the primary agent help refine it until the first slice is ready for `/cook`.
34
- 4. Run `/cook`.
35
- 5. Review the startup brief and choose **Start** or **Cancel**.
36
- 6. Later, run `/cook` again to resume from canonical state or confirm an explicit replacement or next-round handoff.
33
+ 3. In the main chat, describe and refine the concrete repo change you want.
34
+ 4. When you want to enter workflow, run `/cook`.
35
+ 5. Review the synthesized startup brief and choose **Start** or **Cancel**.
36
+ 6. Later, run `/cook` again to resume from canonical state or confirm a synthesized replacement or next-round startup brief.
37
37
 
38
38
  ```text
39
39
  /cook
@@ -43,20 +43,20 @@ Then run `/reload` in Pi.
43
43
 
44
44
  | If you want to... | Do this |
45
45
  |---|---|
46
- | Start a long-running task | Discuss the concrete repo change in the main chat, wait for a fresh primary-agent handoff, then run `/cook` |
46
+ | Start a long-running task | Discuss the concrete repo change in the main chat, then run `/cook` when you want workflow to begin |
47
47
  | Continue the current workflow | Run `/cook` |
48
- | Refocus or start the next round | Discuss the new concrete repo change in the main chat, wait for a fresh primary-agent handoff, then run `/cook` |
48
+ | 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 |
49
49
 
50
50
  ## What `/cook` expects
51
51
 
52
- - a fresh valid explicit primary-agent `/cook` handoff capsule from recent ordinary-chat discussion whenever `/cook` is starting a new workflow or the next round after a completed workflow
53
- - for that handoff capsule to start workflow immediately, it must already be implementation-startable: a bounded `first_slice_goal`, repo-change-oriented acceptance, `implementation_surfaces`, `verification_commands`, and `why_this_slice_first`
54
- - enough detail in the main chat for the primary agent to form that bounded handoff capsule before you run `/cook`
52
+ - enough recent main-chat discussion for `/cook` to synthesize a concrete startup brief when you explicitly invoke it
53
+ - a mission that is concrete enough to anchor bounded repo work rather than planning-only discussion
54
+ - acceptance and verification intent that can support a truthful first workflow round
55
55
  - README/CHANGELOG updates still count as concrete repo changes
56
- - assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts still do not count unless they include the explicit structured `/cook` handoff capsule
57
- - recent main-chat discussion can still validate or supplement an accepted explicit handoff, but it no longer starts a new workflow by itself
56
+ - 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
57
+ - 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
58
58
 
59
- If no fresh valid handoff exists for new-workflow or next-round entry, `/cook` fails closed, leaves canonical `.agent/**` state unchanged, and tells you to get an explicit primary-agent handoff in the main chat before rerunning `/cook`.
59
+ 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`.
60
60
 
61
61
  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.
62
62
 
@@ -66,40 +66,38 @@ If you pass inline arguments to `/cook`, it also fails closed and tells you to m
66
66
 
67
67
  Only explicit `/cook` enters the workflow. Ordinary prompts stay in the main chat and go straight to the primary agent.
68
68
 
69
- If a task has clearly matured into completion-workflow scope, the primary agent should recommend `/cook` instead of starting long-running implementation directly in ordinary chat.
69
+ If a task has clearly matured into completion-workflow scope, the primary agent should still avoid starting long-running implementation directly in ordinary chat.
70
70
 
71
- Before you explicitly run `/cook`, the conversation can still stay in ordinary chat: the primary agent may keep answering follow-up questions and refining requirements rather than switching into a hard handoff-only refusal mode.
71
+ Ordinary chat remains advisory until you explicitly run `/cook`. At that point `/cook` synthesizes a startup brief from recent discussion using primary-agent-style context, then asks you to **Start** or **Cancel** before rewriting canonical workflow state.
72
72
 
73
- That handoff should include an explicit structured `/cook` capsule in the assistant reply once the first slice is implementation-ready, so `/cook` can confirm the already-formed mission instead of re-deriving it from broad ambient context.
74
-
75
- The capsule is still advisory startup intake, not canonical workflow state, and new-workflow or next-round entry only proceeds when it already names the first bounded slice, repo-change-oriented acceptance, implementation surfaces, and verification commands.
73
+ Optional explicit `/cook` capsules may still be used as compatibility startup intake, but they are no longer the default path and are not required for new-workflow or next-round entry.
76
74
 
77
75
  Important behavior:
78
76
  - `/cook` is the canonical workflow boundary and manual entry point
79
- - startup and next-round entry stay confirm-first and require a fresh valid explicit primary-agent handoff
80
- - active workflows resume from canonical `.agent/**` state unless a fresh valid explicit handoff proposes a replacement
77
+ - startup and next-round entry stay confirm-first, but they now derive startup from explicit user `/cook` entry plus recent discussion when needed
78
+ - active workflows resume from canonical `.agent/**` state unless `/cook` synthesizes or receives a concrete replacement mission
81
79
  - explicit slash commands other than `/cook` continue normally in the main chat
82
- - ordinary main-chat discussion may clarify or propose, but mature long-running implementation should be handed off to `/cook`
80
+ - ordinary main-chat discussion may clarify or propose, but mature long-running implementation still must not start before explicit `/cook`
83
81
 
84
82
  ## Typical examples
85
83
 
86
- Start a new workflow after a fresh primary-agent handoff:
84
+ Start a new workflow from recent main-chat discussion:
87
85
 
88
86
  ```text
89
87
  I want to add login redirect handling and tests.
90
- # let the primary agent hand you off to /cook
88
+ # discuss/refine in main chat
91
89
  /cook
92
90
  ```
93
91
 
94
92
  ## What happens when you run `/cook`
95
93
 
96
- `/cook` first looks for a fresh explicit primary-agent handoff capsule from recent ordinary-chat discussion. New-workflow entry and done-workflow next-round entry start only when that capsule is fresh, valid, and implementation-startable; otherwise `/cook` fails closed instead of deriving startup from recent discussion. When a workflow is already active and no fresh valid explicit handoff is present, `/cook` resumes from canonical `.agent/**` state instead of deriving replacement startup from recent discussion.
94
+ `/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.
97
95
 
98
96
  | Repo state | What you'll see |
99
97
  |---|---|
100
- | No workflow yet | If a fresh explicit handoff capsule exists and is implementation-startable, you get a startup brief built from that handoff and choose **Start** or **Cancel**. Otherwise `/cook` fails closed, leaves canonical state unchanged, and tells you to get a fresh explicit primary-agent handoff. Weak, unreliable, stale, planning-only, or non-startable explicit-handoff intake also fails closed. |
101
- | Active workflow exists | Usually a resume of the current workflow from canonical `.agent/**` state. If a fresh explicit handoff capsule points to a different concrete repo change, `/cook` shows a chooser first and only rewrites canonical state after you confirm the replacement. Ambiguous intake stays conservative. |
102
- | Previous workflow is `done` | A fresh explicit handoff capsule can still start the next implementation round behind **Start** or **Cancel**. Without one, `/cook` fails closed instead of deriving the next round from recent discussion. |
98
+ | 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. |
99
+ | 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. |
100
+ | 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. |
103
101
 
104
102
  ## Confirmation and fail-closed behavior
105
103
 
@@ -107,9 +105,9 @@ I want to add login redirect handling and tests.
107
105
 
108
106
  - startup, next-round, and refocus proposals are approval-only
109
107
  - actions are **Start** and **Cancel**
110
- - **Cancel** is side-effect free: canonical workflow state stays unchanged, so you can discuss changes in the main chat and rerun `/cook`
108
+ - **Cancel** is side-effect free: discuss changes in the main chat and rerun `/cook`
111
109
  - weak, ambiguous, stale, invalid, assistant-produced, or planning-only intake does not start a workflow
112
- - when a fresh explicit handoff suggests replacing an active workflow, `/cook` shows a chooser before any canonical state rewrite
110
+ - when a concrete replacement mission suggests replacing an active workflow, `/cook` shows a chooser before any canonical state rewrite
113
111
 
114
112
  When you accept startup or refocus, `/cook` persists the chosen workflow state in canonical `.agent/**` files before the re-ground round begins.
115
113
 
@@ -38,7 +38,7 @@ type ContextProposalAlternate = {
38
38
  analysis: ContextProposalAnalysis;
39
39
  goalText: string;
40
40
  basisPreview: string;
41
- source: "session" | "analyst" | "handoff_capsule";
41
+ source: "session" | "analyst" | "handoff_capsule" | "deferred_primary_agent_handoff";
42
42
  };
43
43
 
44
44
  type ContextProposal = ContextProposalAlternate & {
@@ -60,9 +60,8 @@ type CookContextProposalResult = {
60
60
  blockedFailureMessage?: string;
61
61
  };
62
62
 
63
- function buildCookExplicitHandoffRequiredMessage(deps: CompletionDriverDeps, prefix?: string): string {
64
- const requirement =
65
- "/cook failed closed because starting a new completion workflow now requires a fresh valid explicit primary-agent handoff. Ask the primary agent to emit a fresh ```cook_handoff``` capsule in the main chat, then rerun /cook.";
63
+ function buildCookStartupDerivationFailureMessage(deps: CompletionDriverDeps, prefix?: string): string {
64
+ const requirement = deps.structuredDiscussionFailureDetail;
66
65
  return prefix ? `${prefix} ${requirement}` : requirement;
67
66
  }
68
67
 
@@ -73,9 +72,10 @@ type ActiveWorkflowProposalAssessment = {
73
72
  blockedFailureMessage?: string;
74
73
  reason:
75
74
  | "matching_mission"
76
- | "missing_explicit_handoff"
77
- | "fresh_explicit_handoff"
78
- | "fresh_explicit_handoff_not_startable";
75
+ | "no_replacement_proposal"
76
+ | "explicit_handoff_replacement"
77
+ | "deferred_replacement"
78
+ | "replacement_not_startable";
79
79
  };
80
80
 
81
81
  type ExistingWorkflowChooserOptions = {
@@ -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 explicitHandoff = await deps.deriveCookStartupProposal(ctx, projectName);
325
- if (explicitHandoff.blockedFailureMessage) {
324
+ const derived = await deps.deriveCookContextProposal(ctx, projectName);
325
+ if (derived.blockedFailureMessage) {
326
326
  const assessment: ActiveWorkflowProposalAssessment = {
327
327
  action: "blocked",
328
328
  currentMissionAnchor: currentMission,
329
- blockedFailureMessage: explicitHandoff.blockedFailureMessage,
330
- reason: "fresh_explicit_handoff_not_startable",
329
+ blockedFailureMessage: derived.blockedFailureMessage,
330
+ reason: "replacement_not_startable",
331
331
  };
332
332
  deps.maybeWriteActiveWorkflowRoutingSnapshot(assessment);
333
333
  return assessment;
334
334
  }
335
- const proposal = explicitHandoff.proposal;
335
+ const proposal = derived.proposal;
336
336
  if (!proposal) {
337
337
  const assessment: ActiveWorkflowProposalAssessment = {
338
338
  action: "continue",
339
339
  currentMissionAnchor: currentMission,
340
- reason: "missing_explicit_handoff",
340
+ reason: "no_replacement_proposal",
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: "fresh_explicit_handoff",
359
+ reason: proposal.source === "handoff_capsule" ? "explicit_handoff_replacement" : "deferred_replacement",
360
360
  };
361
361
  deps.maybeWriteActiveWorkflowRoutingSnapshot(assessment);
362
362
  return assessment;
@@ -543,7 +543,7 @@ export async function runCookEntry(
543
543
  }
544
544
  const proposal = derived.proposal;
545
545
  if (!proposal) {
546
- deps.emitCommandText(ctx, buildCookExplicitHandoffRequiredMessage(deps), "info");
546
+ deps.emitCommandText(ctx, buildCookStartupDerivationFailureMessage(deps), "info");
547
547
  return;
548
548
  }
549
549
  const decision = await deps.confirmContextProposal(ctx, proposal, {
@@ -588,7 +588,7 @@ export async function runCookEntry(
588
588
  }
589
589
  const proposal = derived.proposal;
590
590
  if (!proposal) {
591
- deps.emitCommandText(ctx, buildCookExplicitHandoffRequiredMessage(deps, "The previous completion workflow is already done."), "info");
591
+ deps.emitCommandText(ctx, buildCookStartupDerivationFailureMessage(deps, "The previous completion workflow is already done."), "info");
592
592
  return;
593
593
  }
594
594
  const decision = await deps.confirmContextProposal(ctx, proposal, {
@@ -610,7 +610,13 @@ 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(ctx, `Started a new completion workflow round from explicit primary-agent handoff: ${decision.missionAnchor}`, "info");
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
+ );
614
620
  } else {
615
621
  const assessment = await assessActiveWorkflowProposalRouting(ctx, snapshot, deps);
616
622
  if (assessment.action === "blocked") {
@@ -621,20 +627,29 @@ export async function runCookEntry(
621
627
  await resumeActiveWorkflowFromCanonicalState(pi, ctx, snapshot, deps);
622
628
  return;
623
629
  }
624
- const explicitReplacement = assessment.reason === "fresh_explicit_handoff";
630
+ const explicitReplacement = assessment.reason === "explicit_handoff_replacement";
631
+ const deferredReplacement = assessment.reason === "deferred_replacement";
625
632
  const decision = await confirmExistingWorkflowProposal(ctx, snapshot, assessment.proposal, deps, {
626
633
  intro: explicitReplacement
627
634
  ? "A fresh explicit primary-agent handoff proposes replacing the current workflow. Choose how /cook should proceed:"
628
- : "A replacement workflow is ready. 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:",
629
638
  proposedMissionLabel: explicitReplacement
630
639
  ? "Proposed mission from explicit primary-agent handoff"
631
- : "Proposed mission",
640
+ : deferredReplacement
641
+ ? "Proposed mission from deferred primary-agent handoff"
642
+ : "Proposed mission",
632
643
  refocusChoiceLabel: explicitReplacement
633
644
  ? "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."
634
- : "Start new workflow\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.",
635
648
  alternateChoiceLabel: explicitReplacement
636
649
  ? "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."
637
- : undefined,
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,
638
653
  comparison: "strict",
639
654
  });
640
655
  if (!decision) {
@@ -647,9 +662,11 @@ export async function runCookEntry(
647
662
  }
648
663
  const selectedProposal = decision.proposal;
649
664
  const proposalDecision = await deps.confirmContextProposal(ctx, selectedProposal, {
650
- title: assessment.reason === "fresh_explicit_handoff"
665
+ title: explicitReplacement
651
666
  ? "Start the replacement workflow from this explicit startup brief?"
652
- : "Start the replacement workflow from this startup brief?",
667
+ : deferredReplacement
668
+ ? "Start the replacement workflow from this deferred startup brief?"
669
+ : "Start the replacement workflow from this startup brief?",
653
670
  });
654
671
  if (!proposalDecision) {
655
672
  deps.emitCommandText(ctx, buildCookCancellationMessage("Cancelled replacement workflow proposal", deps), "info");
@@ -669,9 +686,11 @@ export async function runCookEntry(
669
686
  snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
670
687
  deps.emitCommandText(
671
688
  ctx,
672
- assessment.reason === "fresh_explicit_handoff"
689
+ explicitReplacement
673
690
  ? `Refocused completion mission from explicit primary-agent handoff to: ${proposalDecision.missionAnchor}`
674
- : `Refocused completion mission to: ${proposalDecision.missionAnchor}`,
691
+ : deferredReplacement
692
+ ? `Refocused completion mission from deferred primary-agent handoff to: ${proposalDecision.missionAnchor}`
693
+ : `Refocused completion mission to: ${proposalDecision.missionAnchor}`,
675
694
  "info",
676
695
  );
677
696
  }
@@ -25,6 +25,7 @@ import {
25
25
  missionAnchorsStrictlyEquivalent,
26
26
  normalizeMissionAnchorText,
27
27
  resolveContextProposalConfirmationAction,
28
+ retagContextProposalSource,
28
29
  stripCodeBlocks,
29
30
  } from "./proposal";
30
31
  import type {
@@ -107,6 +108,7 @@ type RubricEvaluationRole = (typeof RUBRIC_EVALUATION_ROLES)[number];
107
108
  const liveRoleActivityByRoot = new Map<string, LiveRoleActivity>();
108
109
  const activatedCompletionRoutingRoots = new Set<string>();
109
110
  const LIVE_ROLE_HEARTBEAT_MS = 5_000;
111
+ const COOK_HANDOFF_BLOCK_REGEX = /```cook_handoff\s*[\s\S]*?```/giu;
110
112
 
111
113
  function asBoolean(value: unknown): boolean | undefined {
112
114
  return typeof value === "boolean" ? value : undefined;
@@ -133,9 +135,10 @@ type ActiveWorkflowProposalAssessment = {
133
135
  blockedFailureMessage?: string;
134
136
  reason:
135
137
  | "matching_mission"
136
- | "missing_explicit_handoff"
137
- | "fresh_explicit_handoff"
138
- | "fresh_explicit_handoff_not_startable";
138
+ | "no_replacement_proposal"
139
+ | "explicit_handoff_replacement"
140
+ | "deferred_replacement"
141
+ | "replacement_not_startable";
139
142
  };
140
143
 
141
144
  function completionTestWorkflowActionOverride(): "continue" | "refocus" | "cancel" | undefined {
@@ -209,7 +212,7 @@ function maybeWriteTestSnapshot(targetPath: string | undefined, content: string)
209
212
 
210
213
  const COOK_MAIN_CHAT_RERUN_GUIDANCE = "Discuss changes in the main chat and rerun /cook.";
211
214
  const COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL =
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.";
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.";
213
216
 
214
217
  function isWorkflowDone(snapshot: CompletionStateSnapshot | undefined): boolean {
215
218
  return asString(snapshot?.state?.continuation_policy) === "done";
@@ -372,6 +375,72 @@ async function promptContextProposalConfirmationAction(
372
375
  });
373
376
  }
374
377
 
378
+ function stripCookHandoffBlocks(text: string): string {
379
+ return text.replace(COOK_HANDOFF_BLOCK_REGEX, " ").replace(/\s+/g, " ").trim();
380
+ }
381
+
382
+ async function deriveCookRecentDiscussionProposal(
383
+ ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
384
+ projectName: string,
385
+ ): Promise<ContextProposal | undefined> {
386
+ const recentMessages = collectRecentSessionMessages(ctx, { isRecord, asString, asNumber, isStaleContextError });
387
+ 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)
391
+ .map((entry) => ({ role: entry.role, text: stripCookHandoffBlocks(entry.text) }))
392
+ .filter((entry) => entry.text.length > 0);
393
+ const snapshot = await loadCompletionSnapshot(getCtxCwd(ctx));
394
+ const workflowContextLines = snapshot
395
+ ? [
396
+ `current mission anchor: ${asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? asString(snapshot.active?.mission_anchor) ?? "(none)"}`,
397
+ `continuation policy: ${asString(snapshot.state?.continuation_policy) ?? "(none)"}`,
398
+ `latest completed slice: ${asString(snapshot.state?.latest_completed_slice) ?? "(none)"}`,
399
+ `latest verified slice: ${asString(snapshot.state?.latest_verified_slice) ?? "(none)"}`,
400
+ `active slice goal: ${asString(snapshot.active?.goal) ?? "(none)"}`,
401
+ `active slice why_now: ${asString(snapshot.active?.why_now) ?? "(none)"}`,
402
+ `verification goal: ${asString(snapshot.verificationEvidence?.goal) ?? "(none)"}`,
403
+ `verification summary: ${asString(snapshot.verificationEvidence?.summary) ?? "(none)"}`,
404
+ ]
405
+ : [];
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,
440
+ });
441
+ return retagContextProposalSource(proposal, "deferred_primary_agent_handoff");
442
+ }
443
+
375
444
  async function deriveCookStartupProposal(
376
445
  ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
377
446
  projectName: string,
@@ -390,70 +459,14 @@ async function deriveCookStartupProposal(
390
459
  if (explicitHandoff.status === "fresh_but_not_startable") {
391
460
  return { blockedFailureMessage: explicitHandoff.message };
392
461
  }
393
- return {};
462
+ return { proposal: await deriveCookRecentDiscussionProposal(ctx, projectName) };
394
463
  }
395
464
 
396
465
  async function deriveCookContextProposal(
397
466
  ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
398
467
  projectName: string,
399
468
  ): Promise<CookContextProposalResult> {
400
- const recentMessages = collectRecentSessionMessages(ctx, { isRecord, asString, asNumber, isStaleContextError });
401
- const recentEntries = recentMessages
402
- .filter((entry) => (entry.role === "user" || entry.role === "custom") && !entry.isCommand)
403
- .slice(0, 8)
404
- .map((entry) => ({ role: entry.role, text: entry.text }));
405
- const snapshot = await loadCompletionSnapshot(getCtxCwd(ctx));
406
- const workflowContextLines = snapshot
407
- ? [
408
- `current mission anchor: ${asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? asString(snapshot.active?.mission_anchor) ?? "(none)"}`,
409
- `continuation policy: ${asString(snapshot.state?.continuation_policy) ?? "(none)"}`,
410
- `latest completed slice: ${asString(snapshot.state?.latest_completed_slice) ?? "(none)"}`,
411
- `latest verified slice: ${asString(snapshot.state?.latest_verified_slice) ?? "(none)"}`,
412
- `active slice goal: ${asString(snapshot.active?.goal) ?? "(none)"}`,
413
- `active slice why_now: ${asString(snapshot.active?.why_now) ?? "(none)"}`,
414
- `verification goal: ${asString(snapshot.verificationEvidence?.goal) ?? "(none)"}`,
415
- `verification summary: ${asString(snapshot.verificationEvidence?.summary) ?? "(none)"}`,
416
- ]
417
- : [];
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
- };
469
+ return await deriveCookStartupProposal(ctx, projectName);
457
470
  }
458
471
 
459
472
  async function confirmContextProposal(
@@ -929,7 +942,7 @@ export default function completionExtension(pi: ExtensionAPI) {
929
942
  structuredDiscussionFailureDetail: COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL,
930
943
  mainChatRerunGuidance: COOK_MAIN_CHAT_RERUN_GUIDANCE,
931
944
  cookCommandSpec: {
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",
945
+ description: "/cook workflow: synthesize a startup brief when the user explicitly enters /cook, resume the current workflow from canonical state, or confirm a replacement mission from explicit /cook entry",
933
946
  },
934
947
  buildContextProposalContinuationReason,
935
948
  completionKickoff,
@@ -10,7 +10,7 @@ import type {
10
10
 
11
11
  export type AdvisoryStartupBrief = {
12
12
  kind: "startup_brief";
13
- source: "recent_discussion" | "primary_agent_handoff";
13
+ source: "recent_discussion" | "primary_agent_handoff" | "deferred_primary_agent_handoff";
14
14
  confirmed: true;
15
15
  captured_at: string;
16
16
  goal_text: string;
@@ -27,18 +27,15 @@ 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, propose implementation approaches, and refine scope with the user.",
30
+ "Use ordinary chat to clarify requirements, discuss tradeoffs, propose implementation approaches, and refine scope naturally.",
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 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.",
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.",
32
+ "Do not proactively tell the user to run /cook just because a task looks workflow-worthy, and do not emit a ```cook_handoff``` capsule by default in ordinary chat.",
33
+ "Even when the task has matured into workflow-level implementation work, ordinary chat remains ordinary chat until the user explicitly runs /cook.",
34
+ "Before that explicit /cook entry, 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.",
35
+ "If the user asks follow-up questions or wants to keep refining scope, continue helping in ordinary chat instead of steering them into workflow mode.",
36
+ "Once 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.",
37
+ "Only provide a preview startup brief or ```cook_handoff``` capsule in ordinary chat when the user explicitly asks for that preview behavior.",
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.",
42
39
  "If the task is still ordinary Q&A, lightweight brainstorming, or a tiny one-off fix, continue normally without forcing /cook.",
43
40
  ].join(" ");
44
41
  }
@@ -97,7 +94,12 @@ export function buildAdvisoryStartupBrief(args: {
97
94
  }): AdvisoryStartupBrief {
98
95
  return {
99
96
  kind: "startup_brief",
100
- source: args.proposal.source === "handoff_capsule" ? "primary_agent_handoff" : "recent_discussion",
97
+ source:
98
+ args.proposal.source === "handoff_capsule"
99
+ ? "primary_agent_handoff"
100
+ : args.proposal.source === "deferred_primary_agent_handoff"
101
+ ? "deferred_primary_agent_handoff"
102
+ : "recent_discussion",
101
103
  confirmed: true,
102
104
  captured_at: args.capturedAt ?? new Date().toISOString(),
103
105
  goal_text: args.proposal.goalText,
@@ -27,7 +27,7 @@ export type ContextProposalAlternate = {
27
27
  analysis: ContextProposalAnalysis;
28
28
  goalText: string;
29
29
  basisPreview: string;
30
- source: "session" | "analyst" | "handoff_capsule";
30
+ source: "session" | "analyst" | "handoff_capsule" | "deferred_primary_agent_handoff";
31
31
  };
32
32
 
33
33
  export type ContextProposal = ContextProposalAlternate & {
@@ -1246,6 +1246,7 @@ export function extractContextProposalFromStructuredSession(
1246
1246
 
1247
1247
  const COOK_HANDOFF_BLOCK_REGEX = /```cook_handoff\s*([\s\S]*?)```/giu;
1248
1248
  const COOK_HANDOFF_MAX_AGE_MS = 45 * 60 * 1000;
1249
+ const COOK_HANDOFF_MAX_LATER_NON_COMMAND_MESSAGES = 2;
1249
1250
  const COOK_HANDOFF_NEGATIVE_MISSION_REGEX =
1250
1251
  /(?:\b(?:do not|don't|dont|not|never|avoid|skip|refuse|recognize that|suppress|ignore|block|prevent)\b|(?:不要|別|别|勿|禁止|避免|忽略|阻止))/iu;
1251
1252
  const COOK_HANDOFF_WORKFLOW_ONLY_ACCEPTANCE_REGEX =
@@ -1388,6 +1389,19 @@ function isStartableCookHandoffCapsule(
1388
1389
  return cookHandoffStartabilityFailures(capsule, deps).length === 0;
1389
1390
  }
1390
1391
 
1392
+ function laterMessagesInvalidateCookHandoff(
1393
+ laterMessages: RecentSessionMessage[],
1394
+ deps: Pick<ProposalParseDeps, "stripCodeBlocks">,
1395
+ ): boolean {
1396
+ const laterNonCommandMessages = laterMessages.filter((entry) => !entry.isCommand);
1397
+ if (laterNonCommandMessages.length > COOK_HANDOFF_MAX_LATER_NON_COMMAND_MESSAGES) return true;
1398
+ return laterNonCommandMessages.some((entry) => {
1399
+ if (entry.role === "summary") return false;
1400
+ if (!hasRecentDiscussionImplementationIntent(entry.text, deps.stripCodeBlocks)) return false;
1401
+ return true;
1402
+ });
1403
+ }
1404
+
1391
1405
  function cookHandoffIsFreshEnough(capsule: CookHandoffCapsule, laterMessages: RecentSessionMessage[]): boolean {
1392
1406
  const capturedAtMs = Date.parse(capsule.captured_at);
1393
1407
  if (!Number.isFinite(capturedAtMs)) return false;
@@ -1470,6 +1484,7 @@ export function assessLatestCookHandoffProposal(
1470
1484
  const capsule = capsules[capsuleIndex];
1471
1485
  const laterMessages = recentMessages.slice(0, index);
1472
1486
  if (!cookHandoffIsFreshEnough(capsule, laterMessages)) continue;
1487
+ if (laterMessagesInvalidateCookHandoff(laterMessages, deps)) continue;
1473
1488
  const failures = cookHandoffStartabilityFailures(capsule, deps);
1474
1489
  if (failures.length > 0) {
1475
1490
  return {
@@ -1515,6 +1530,18 @@ export async function deriveCookContextProposalFromRecentDiscussion(
1515
1530
  return undefined;
1516
1531
  }
1517
1532
 
1533
+ export function retagContextProposalSource(
1534
+ proposal: ContextProposal | undefined,
1535
+ source: ContextProposalAlternate["source"],
1536
+ ): ContextProposal | undefined {
1537
+ if (!proposal) return undefined;
1538
+ return {
1539
+ ...proposal,
1540
+ source,
1541
+ alternateProposals: proposal.alternateProposals.map((alternate) => ({ ...alternate, source })),
1542
+ };
1543
+ }
1544
+
1518
1545
  export function resolveContextProposalConfirmationAction(
1519
1546
  proposal: ContextProposal,
1520
1547
  action: ContextProposalConfirmAction,