@linimin/pi-letscook 0.1.63 → 0.1.65

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
@@ -35,7 +35,7 @@ Then 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
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 primary-agent-authored replacement or next-round handoff.
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 | Ask the primary agent in ordinary chat to prepare the explicit `/cook` handoff, then run `/cook` |
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 | Ask the primary agent in ordinary chat to prepare the fresh explicit `/cook` handoff for the new slice, then run `/cook` |
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
- - a fresh explicit primary-agent `cook_handoff` capsule for any new-workflow, next-round, or replacement startup
56
- - a mission and first slice concrete enough for the primary agent to author the startup handoff directly
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 the primary agent turns them into an explicit `cook_handoff` capsule
60
- - `/cook` does not synthesize startup from recent discussion when handoff data is missing; the primary agent must provide the handoff
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 no fresh explicit primary-agent handoff exists, `/cook` fails closed, leaves canonical `.agent/**` state unchanged, and tells you to ask the primary agent in the main chat to emit a fresh `cook_handoff` capsule 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
 
@@ -71,14 +71,14 @@ Only explicit `/cook` enters workflow mode. Ordinary prompts stay in the main ch
71
71
 
72
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 should consume the explicit primary-agent handoff you already prepared in ordinary chat, then ask 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
- Explicit `/cook` capsules are the required startup intake for new-workflow, next-round, and replacement 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, but they start from explicit primary-agent handoff data rather than recent-discussion guessing
81
- - active workflows resume from canonical `.agent/**` state unless a fresh explicit primary-agent handoff proposes 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. New-workflow entry, done-workflow next-round entry, and active-workflow replacement should use that handoff instead of guessing from recent discussion. If no fresh explicit handoff exists, `/cook` fails closed for startup/refocus and resumes canonical state only when continuing the existing workflow. 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` consumes a fresh explicit primary-agent handoff and asks you to choose **Start** or **Cancel**. Missing, stale, planning-only, or non-startable handoffs fail closed. |
102
- | Active workflow exists | Usually a resume of the current workflow from canonical `.agent/**` state. If a fresh explicit primary-agent handoff points to a different concrete replacement 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 only from a fresh explicit primary-agent handoff behind **Start** or **Cancel**. Missing, weak, or planning-only next-round handoffs fail 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
 
@@ -182,6 +182,7 @@ function buildCookStructuredDiscussionFailureMessage(deps: CompletionDriverDeps,
182
182
  return prefix ? `${prefix} ${deps.structuredDiscussionFailureDetail}` : deps.structuredDiscussionFailureDetail;
183
183
  }
184
184
 
185
+
185
186
  export function completionContinuationFingerprint(snapshot: CompletionStateSnapshot): string | undefined {
186
187
  if (asString(snapshot.state?.continuation_policy) !== "continue") return undefined;
187
188
  const nextMandatoryRole = asString(snapshot.state?.next_mandatory_role);
@@ -320,18 +321,18 @@ async function assessActiveWorkflowProposalRouting(
320
321
  ): Promise<ActiveWorkflowProposalAssessment> {
321
322
  const currentMission = currentMissionAnchor(snapshot);
322
323
  const projectName = path.basename(snapshot.files.root);
323
- const explicitHandoff = await deps.deriveCookStartupProposal(ctx, projectName);
324
- if (explicitHandoff.blockedFailureMessage) {
324
+ const proposalResult = await deps.deriveCookContextProposal(ctx, projectName);
325
+ if (proposalResult.blockedFailureMessage) {
325
326
  const assessment: ActiveWorkflowProposalAssessment = {
326
327
  action: "blocked",
327
328
  currentMissionAnchor: currentMission,
328
- blockedFailureMessage: explicitHandoff.blockedFailureMessage,
329
+ blockedFailureMessage: proposalResult.blockedFailureMessage,
329
330
  reason: "fresh_explicit_handoff_not_startable",
330
331
  };
331
332
  deps.maybeWriteActiveWorkflowRoutingSnapshot(assessment);
332
333
  return assessment;
333
334
  }
334
- const proposal = explicitHandoff.proposal;
335
+ const proposal = proposalResult.proposal;
335
336
  if (!proposal) {
336
337
  const assessment: ActiveWorkflowProposalAssessment = {
337
338
  action: "continue",
@@ -535,7 +536,7 @@ export async function runCookEntry(
535
536
  if (!snapshot) {
536
537
  const root = findRepoRoot(cwd) ?? cwd;
537
538
  const projectName = path.basename(root);
538
- const derived = await deps.deriveCookStartupProposal(ctx, projectName);
539
+ const derived = await deps.deriveCookContextProposal(ctx, projectName);
539
540
  if (derived.blockedFailureMessage) {
540
541
  deps.emitCommandText(ctx, derived.blockedFailureMessage, "info");
541
542
  return;
@@ -580,7 +581,7 @@ export async function runCookEntry(
580
581
  if (!goal) {
581
582
  if (workflowDone) {
582
583
  const projectName = path.basename(snapshot.files.root);
583
- const derived = await deps.deriveCookStartupProposal(ctx, projectName);
584
+ const derived = await deps.deriveCookContextProposal(ctx, projectName);
584
585
  if (derived.blockedFailureMessage) {
585
586
  deps.emitCommandText(ctx, derived.blockedFailureMessage, "info");
586
587
  return;
@@ -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,
@@ -211,7 +209,7 @@ function maybeWriteTestSnapshot(targetPath: string | undefined, content: string)
211
209
 
212
210
  const COOK_MAIN_CHAT_RERUN_GUIDANCE = "Discuss changes in the main chat and rerun /cook.";
213
211
  const COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL =
214
- "/cook failed closed because starting workflow now requires a fresh explicit primary-agent handoff. Ask the primary agent in the main chat to emit a fresh ```cook_handoff``` capsule, 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.";
215
213
 
216
214
  function isWorkflowDone(snapshot: CompletionStateSnapshot | undefined): boolean {
217
215
  return asString(snapshot?.state?.continuation_policy) === "done";
@@ -374,6 +372,10 @@ async function promptContextProposalConfirmationAction(
374
372
  });
375
373
  }
376
374
 
375
+ function stripCookHandoffBlocks(text: string): string {
376
+ return text.replace(COOK_HANDOFF_BLOCK_REGEX, " ").replace(/\s+/g, " ").trim();
377
+ }
378
+
377
379
  async function deriveCookStartupProposal(
378
380
  ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
379
381
  projectName: string,
@@ -399,7 +401,54 @@ async function deriveCookContextProposal(
399
401
  ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
400
402
  projectName: string,
401
403
  ): Promise<CookContextProposalResult> {
402
- return await deriveCookStartupProposal(ctx, projectName);
404
+ const explicit = await deriveCookStartupProposal(ctx, projectName);
405
+ if (explicit.proposal || explicit.blockedFailureMessage) return explicit;
406
+ const recentMessages = collectRecentSessionMessages(ctx, { isRecord, asString, asNumber, isStaleContextError });
407
+ const recentEntries = recentMessages
408
+ .filter((entry) => !entry.isCommand && (entry.role === "user" || entry.role === "assistant" || entry.role === "custom" || entry.role === "summary"))
409
+ .slice(0, 12)
410
+ .map((entry) => ({ role: entry.role, text: stripCookHandoffBlocks(entry.text) }))
411
+ .filter((entry) => entry.text.length > 0);
412
+ const snapshot = await loadCompletionSnapshot(getCtxCwd(ctx));
413
+ const workflowContextLines = snapshot
414
+ ? [
415
+ `current mission anchor: ${asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? asString(snapshot.active?.mission_anchor) ?? "(none)"}`,
416
+ `continuation policy: ${asString(snapshot.state?.continuation_policy) ?? "(none)"}`,
417
+ `latest completed slice: ${asString(snapshot.state?.latest_completed_slice) ?? "(none)"}`,
418
+ `latest verified slice: ${asString(snapshot.state?.latest_verified_slice) ?? "(none)"}`,
419
+ `active slice goal: ${asString(snapshot.active?.goal) ?? "(none)"}`,
420
+ `active slice why_now: ${asString(snapshot.active?.why_now) ?? "(none)"}`,
421
+ `verification goal: ${asString(snapshot.verificationEvidence?.goal) ?? "(none)"}`,
422
+ `verification summary: ${asString(snapshot.verificationEvidence?.summary) ?? "(none)"}`,
423
+ ]
424
+ : [];
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,
436
+ });
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, {
441
+ asString,
442
+ asStringArray,
443
+ assessMissionAnchor,
444
+ normalizeMissionAnchorText,
445
+ isWeakMissionAnchor,
446
+ missionAnchorsStrictlyEquivalent,
447
+ stripCodeBlocks,
448
+ });
449
+ if (generated.status === "startable") return { proposal: generated.proposal };
450
+ if (generated.status === "fresh_but_not_startable") return { blockedFailureMessage: generated.message };
451
+ return {};
403
452
  }
404
453
 
405
454
  async function confirmContextProposal(
@@ -32,9 +32,9 @@ 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 asks to enter /cook workflow, generate one fresh ```cook_handoff``` capsule in ordinary chat from the primary-agent view of the task, then tell the user to run /cook.",
36
- "Do not expect /cook to infer or guess startup intent from recent discussion alone; /cook should consume the explicit primary-agent handoff instead.",
37
- "Only provide a preview startup brief or ```cook_handoff``` capsule in ordinary chat when the user explicitly asks for that preview behavior or explicitly asks to enter /cook workflow.",
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.",
37
+ "Only provide a preview startup brief or ```cook_handoff``` capsule in ordinary chat when the user explicitly asks for that preview behavior.",
38
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.",
39
39
  "When you continue in ordinary chat, do not pretend /cook already started and do not silently rewrite discussion into canonical workflow state.",
40
40
  ].join(" ");
@@ -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.63",
3
+ "version": "0.1.65",
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,
@@ -147,18 +147,54 @@ mkdir -p "$ROOT"
147
147
  cd "$ROOT"
148
148
  git init -q
149
149
 
150
- # No workflow yet: bare /cook should fail closed without a fresh explicit primary-agent handoff,
151
- # even when recent discussion is fully structured.
150
+ # No workflow yet: bare /cook should be able to generate a primary-agent handoff in the same entry,
151
+ # then continue directly to startup confirmation.
152
152
  SESSION_ZERO="$TMPDIR/session-zero.jsonl"
153
- DISCUSSION_ZERO=$'Mission: Remove the completion status line while keeping the completion widget.\nScope:\n- Keep the non-running completion widget.\n- Suppress the widget while a completion role is active.\nConstraints:\n- Do not reintroduce any other completion status surface.\nAcceptance:\n- Update README to match the shipped behavior.\n- Keep observability regression coverage truthful.'
153
+ DISCUSSION_ZERO=$'Mission: Remove the completion status line while keeping the completion widget.
154
+ Scope:
155
+ - Keep the non-running completion widget.
156
+ - Suppress the widget while a completion role is active.
157
+ Constraints:
158
+ - Do not reintroduce any other completion status surface.
159
+ Acceptance:
160
+ - Update README to match the shipped behavior.
161
+ - Keep observability regression coverage truthful.'
154
162
  DISCUSSION_SNAPSHOT_ZERO="$TMPDIR/context-proposal-structured-fallback.json"
163
+ GENERATED_HANDOFF_ZERO="$(python3 - <<'PY'
164
+ import json
165
+ capsule = {
166
+ "kind": "cook_handoff",
167
+ "source": "primary_agent",
168
+ "mission": "Remove the completion status line while keeping the completion widget.",
169
+ "scope": [
170
+ "Keep the non-running completion widget.",
171
+ "Suppress the widget while a completion role is active."
172
+ ],
173
+ "constraints": [
174
+ "Do not reintroduce any other completion status surface."
175
+ ],
176
+ "acceptance": [
177
+ "Update README to match the shipped behavior.",
178
+ "Keep observability regression coverage truthful."
179
+ ],
180
+ "risks": [],
181
+ "notes": ["Generated by the primary-agent handoff step triggered from /cook."],
182
+ "handoff_kind": "implementation_workflow_handoff",
183
+ "first_slice_goal": "Remove the completion status line while preserving the widget behavior.",
184
+ "first_slice_non_goals": ["Do not reintroduce another status surface."],
185
+ "implementation_surfaces": ["extensions/completion/index.ts", "README.md"],
186
+ "verification_commands": ["npm run smoke-test"],
187
+ "why_this_slice_first": "This slice is already concrete and bounded enough to start workflow safely.",
188
+ "task_type": "completion-workflow",
189
+ "evaluation_profile": "completion-rubric-v1",
190
+ "why_cook_now": "The user explicitly chose workflow mode for this bounded implementation slice."
191
+ }
192
+ print("```cook_handoff\n" + json.dumps(capsule, ensure_ascii=False, indent=2) + "\n```")
193
+ PY
194
+ )"
155
195
  write_session "$SESSION_ZERO" "$ROOT" "$DISCUSSION_ZERO"
156
196
 
157
- PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
158
- PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
159
- PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_ZERO" \
160
- PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
161
- pi --session "$SESSION_ZERO" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-structured-fallback.out" 2>"$TMPDIR/pi-completion-context-proposal-structured-fallback.err"
197
+ PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept PI_COMPLETION_PRIMARY_HANDOFF_OUTPUT="$GENERATED_HANDOFF_ZERO" PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_ZERO" PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 pi --session "$SESSION_ZERO" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-structured-fallback.out" 2>"$TMPDIR/pi-completion-context-proposal-structured-fallback.err"
162
198
 
163
199
  python3 - "$TMPDIR/pi-completion-context-proposal-structured-fallback.out" "$TMPDIR/pi-completion-context-proposal-structured-fallback.err" "$DISCUSSION_SNAPSHOT_ZERO" <<'PY'
164
200
  import json
@@ -167,9 +203,13 @@ from pathlib import Path
167
203
 
168
204
  output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
169
205
  snapshot = Path(sys.argv[3])
170
- assert not Path('.agent').exists(), 'missing explicit handoff should fail closed without writing canonical state'
171
- assert not snapshot.exists(), 'missing explicit handoff should not emit a startup proposal snapshot'
172
- assert 'fresh explicit primary-agent handoff' in output, 'missing explicit handoff should explain the explicit-handoff-only startup contract'
206
+ assert Path('.agent').exists(), 'primary-agent handoff generation should scaffold canonical state in the same /cook entry'
207
+ assert snapshot.exists(), 'primary-agent handoff generation should emit a startup proposal snapshot'
208
+ proposal = json.loads(snapshot.read_text())
209
+ brief = json.loads(Path('.agent/state.json').read_text())['advisory_startup_brief']
210
+ assert proposal['source'] == 'handoff_capsule', 'generated primary-agent handoff should be consumed as handoff capsule startup source'
211
+ assert brief['source'] == 'primary_agent_handoff', 'generated primary-agent handoff should record primary_agent_handoff advisory intake'
212
+ assert 'Initialized completion control plane' in output, 'same-entry primary-agent handoff generation should initialize canonical workflow state'
173
213
  PY
174
214
 
175
215
  rm -rf .agent
@@ -206,6 +246,7 @@ PY
206
246
  write_session_messages "$SESSION_ZERO_USER_AUTHORED" "$ROOT" "$USER_AUTHORED_MESSAGES_ZERO"
207
247
 
208
248
  PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$USER_AUTHORED_SNAPSHOT_ZERO" \
249
+ PI_COMPLETION_DISABLE_PRIMARY_HANDOFF_SYNTHESIS=1 \
209
250
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
210
251
  pi --session "$SESSION_ZERO_USER_AUTHORED" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-user-authored.out" 2>"$TMPDIR/pi-completion-context-proposal-user-authored.err"
211
252
 
@@ -217,7 +258,7 @@ output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
217
258
  snapshot = Path(sys.argv[3])
218
259
  assert not Path('.agent').exists(), 'user-authored faux handoff without supporting discussion should still fail closed without writing canonical state'
219
260
  assert not snapshot.exists(), 'user-authored faux handoff should not emit a startup proposal snapshot'
220
- assert 'fresh explicit primary-agent handoff' in output, 'user-authored faux handoff should still explain the explicit-handoff requirement'
261
+ assert 'primary-agent handoff step could not prepare a concrete startup handoff' in output, 'user-authored faux handoff should fail closed when primary-agent handoff generation yields no handoff'
221
262
  PY
222
263
 
223
264
  # No workflow yet: malformed or invalid assistant handoff capsules must also fail closed.
@@ -227,6 +268,7 @@ INVALID_MESSAGES_ZERO='[{"role":"assistant","content":"This is not a valid start
227
268
  write_session_messages "$SESSION_ZERO_INVALID" "$ROOT" "$INVALID_MESSAGES_ZERO"
228
269
 
229
270
  PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$INVALID_SNAPSHOT_ZERO" \
271
+ PI_COMPLETION_DISABLE_PRIMARY_HANDOFF_SYNTHESIS=1 \
230
272
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
231
273
  pi --session "$SESSION_ZERO_INVALID" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-invalid-handoff.out" 2>"$TMPDIR/pi-completion-context-proposal-invalid-handoff.err"
232
274
 
@@ -238,7 +280,7 @@ output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
238
280
  snapshot = Path(sys.argv[3])
239
281
  assert not Path('.agent').exists(), 'invalid assistant handoff without supporting discussion should fail closed without writing canonical state'
240
282
  assert not snapshot.exists(), 'invalid assistant handoff should not emit a startup proposal snapshot'
241
- assert 'fresh explicit primary-agent handoff' in output, 'invalid assistant handoff should still explain the explicit-handoff requirement'
283
+ assert 'primary-agent handoff step could not prepare a concrete startup handoff' in output, 'invalid assistant handoff should fail closed when no valid handoff can be prepared'
242
284
  PY
243
285
 
244
286
  # No workflow yet: a fresh explicit primary-agent handoff should still bootstrap canonical startup state.
@@ -361,6 +403,7 @@ CONTINUE_PROPOSAL_ONE="$TMPDIR/unexpected-active-continue-proposal.json"
361
403
  write_session "$SESSION_ONE_CONTINUE" "$ROOT" "$DISCUSSION_ONE_CONTINUE"
362
404
 
363
405
  PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
406
+ PI_COMPLETION_DISABLE_PRIMARY_HANDOFF_SYNTHESIS=1 \
364
407
  PI_COMPLETION_TEST_ACTIVE_WORKFLOW_ROUTING_PATH="$CONTINUE_ROUTING_ONE" \
365
408
  PI_COMPLETION_TEST_DRIVER_PROMPT_PATH="$CONTINUE_RESUME_PROMPT_ONE" \
366
409
  PI_COMPLETION_TEST_EXISTING_WORKFLOW_CHOOSER_PATH="$CONTINUE_CHOOSER_ONE" \
@@ -408,6 +451,7 @@ DISCUSSION_REFOCUS_PROPOSAL_ONE="$TMPDIR/unexpected-active-discussion-refocus-pr
408
451
  write_session "$SESSION_ONE_DISCUSSION_REFOCUS" "$ROOT" "$DISCUSSION_ONE_DISCUSSION_REFOCUS"
409
452
 
410
453
  PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
454
+ PI_COMPLETION_DISABLE_PRIMARY_HANDOFF_SYNTHESIS=1 \
411
455
  PI_COMPLETION_TEST_ACTIVE_WORKFLOW_ROUTING_PATH="$DISCUSSION_REFOCUS_ROUTING_ONE" \
412
456
  PI_COMPLETION_TEST_DRIVER_PROMPT_PATH="$DISCUSSION_REFOCUS_RESUME_ONE" \
413
457
  PI_COMPLETION_TEST_EXISTING_WORKFLOW_CHOOSER_PATH="$DISCUSSION_REFOCUS_CHOOSER_ONE" \
@@ -482,6 +526,7 @@ with session_path.open('w', encoding='utf-8') as fh:
482
526
  PY
483
527
 
484
528
  PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
529
+ PI_COMPLETION_DISABLE_PRIMARY_HANDOFF_SYNTHESIS=1 \
485
530
  PI_COMPLETION_TEST_ACTIVE_WORKFLOW_ROUTING_PATH="$SUMMARY_ROUTING_ONE" \
486
531
  PI_COMPLETION_TEST_DRIVER_PROMPT_PATH="$SUMMARY_RESUME_PROMPT_ONE" \
487
532
  PI_COMPLETION_TEST_EXISTING_WORKFLOW_CHOOSER_PATH="$SUMMARY_CHOOSER_ONE" \
@@ -643,6 +688,7 @@ write_session "$SESSION_TWO_COMPLETED_SUPPRESS" "$ROOT" "$DISCUSSION_TWO_COMPLET
643
688
 
644
689
  PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
645
690
  PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
691
+ PI_COMPLETION_DISABLE_PRIMARY_HANDOFF_SYNTHESIS=1 \
646
692
  PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_TWO_COMPLETED_SUPPRESS" \
647
693
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
648
694
  pi --session "$SESSION_TWO_COMPLETED_SUPPRESS" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-next-round-completed-suppress.out" 2>"$TMPDIR/pi-completion-context-proposal-next-round-completed-suppress.err"
@@ -660,7 +706,7 @@ state = json.loads(Path('.agent/state.json').read_text())
660
706
  assert state['mission_anchor'] == expected, 'completed-topic suppression should keep the done workflow mission anchor unchanged'
661
707
  assert state['continuation_policy'] == 'done', 'completed-topic suppression should keep the workflow closed'
662
708
  assert not snapshot.exists(), 'completed-topic suppression should not emit a proposal snapshot when the latest discussion only repeats finished work'
663
- assert '/cook failed closed' in output, 'completed-topic suppression should fail closed instead of reopening the finished mission'
709
+ assert 'primary-agent handoff step could not prepare a concrete startup handoff' in output, 'completed-topic suppression should fail closed when no concrete primary-agent handoff can be prepared'
664
710
  PY
665
711
 
666
712
  # Completed workflow: bare /cook should also suppress proposals that merely restate canonical
@@ -691,6 +737,7 @@ write_session "$SESSION_TWO_VERIFIED_SUPPRESS" "$ROOT" "$DISCUSSION_TWO_VERIFIED
691
737
 
692
738
  PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
693
739
  PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
740
+ PI_COMPLETION_DISABLE_PRIMARY_HANDOFF_SYNTHESIS=1 \
694
741
  PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_TWO_VERIFIED_SUPPRESS" \
695
742
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
696
743
  pi --session "$SESSION_TWO_VERIFIED_SUPPRESS" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-next-round-verified-suppress.out" 2>"$TMPDIR/pi-completion-context-proposal-next-round-verified-suppress.err"
@@ -702,7 +749,7 @@ from pathlib import Path
702
749
  output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
703
750
  snapshot = Path(sys.argv[3])
704
751
  assert not snapshot.exists(), 'verification-evidence overlap suppression should not emit a proposal snapshot for already verified work'
705
- assert '/cook failed closed' in output, 'verification-evidence overlap suppression should fail closed when the latest discussion only repeats verified work'
752
+ assert 'primary-agent handoff step could not prepare a concrete startup handoff' in output, 'verification-evidence overlap suppression should fail closed when no concrete primary-agent handoff can be prepared'
706
753
  PY
707
754
 
708
755
  # Completed workflow: bare /cook should fail closed for next-round discussion-only startup too,
@@ -712,11 +759,38 @@ DISCUSSION_TWO_NORMALIZED=$'Mission: 開始實作這個方案\nScope:\n- Normali
712
759
  DISCUSSION_SNAPSHOT_TWO_NORMALIZED="$TMPDIR/context-proposal-next-round-normalized.json"
713
760
  write_session "$SESSION_TWO_NORMALIZED" "$ROOT" "$DISCUSSION_TWO_NORMALIZED"
714
761
 
715
- PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
716
- PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
717
- PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_TWO_NORMALIZED" \
718
- PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
719
- pi --session "$SESSION_TWO_NORMALIZED" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-next-round-normalized.out" 2>"$TMPDIR/pi-completion-context-proposal-next-round-normalized.err"
762
+ GENERATED_HANDOFF_TWO_NORMALIZED="$(python3 - <<'PY'
763
+ import json
764
+ capsule = {
765
+ "kind": "cook_handoff",
766
+ "source": "primary_agent",
767
+ "mission": "Start the next workflow round with a normalized implementation mission.",
768
+ "scope": [
769
+ "Reset canonical state for the new implementation mission.",
770
+ "Keep the next round distinct from the completed workflow."
771
+ ],
772
+ "constraints": [
773
+ "Do not resume the completed workflow when the new round is clearly different."
774
+ ],
775
+ "acceptance": [
776
+ "Reset canonical state back to reground for the new mission.",
777
+ "Preserve the tracked completion control-plane files."
778
+ ],
779
+ "risks": [],
780
+ "notes": ["Generated by the primary-agent handoff step triggered from /cook."],
781
+ "handoff_kind": "implementation_workflow_handoff",
782
+ "first_slice_goal": "Bootstrap the next workflow round from the normalized implementation mission.",
783
+ "first_slice_non_goals": ["Do not reopen finished slices from the previous workflow."],
784
+ "implementation_surfaces": ["extensions/completion/driver.ts", "scripts/context-proposal-test.sh"],
785
+ "verification_commands": ["npm run context-proposal-test"],
786
+ "why_this_slice_first": "The user explicitly chose workflow mode for a bounded next-round restart.",
787
+ "task_type": "completion-workflow",
788
+ "evaluation_profile": "completion-rubric-v1"
789
+ }
790
+ print("```cook_handoff\n" + json.dumps(capsule, ensure_ascii=False, indent=2) + "\n```")
791
+ PY
792
+ )"
793
+ PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept PI_COMPLETION_PRIMARY_HANDOFF_OUTPUT="$GENERATED_HANDOFF_TWO_NORMALIZED" PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_TWO_NORMALIZED" PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 pi --session "$SESSION_TWO_NORMALIZED" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-next-round-normalized.out" 2>"$TMPDIR/pi-completion-context-proposal-next-round-normalized.err"
720
794
 
721
795
  python3 - "$TMPDIR/pi-completion-context-proposal-next-round-normalized.out" "$TMPDIR/pi-completion-context-proposal-next-round-normalized.err" "$DISCUSSION_SNAPSHOT_TWO_NORMALIZED" "$CURRENT_DONE_MISSION" <<'PY'
722
796
  import json
@@ -727,11 +801,12 @@ output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
727
801
  snapshot = Path(sys.argv[3])
728
802
  previous = sys.argv[4]
729
803
  state = json.loads(Path('.agent/state.json').read_text())
730
-
731
- assert not snapshot.exists(), 'done-workflow discussion-only startup should not emit a proposal snapshot without a fresh explicit handoff'
732
- assert state['mission_anchor'] == previous, 'done-workflow discussion-only startup should keep the completed mission anchor unchanged'
733
- assert state['continuation_policy'] == 'done', 'done-workflow discussion-only startup should keep the workflow closed'
734
- assert 'fresh explicit primary-agent handoff' in output, 'done-workflow discussion-only startup should explain the explicit-handoff-only entry contract'
804
+ if snapshot.exists():
805
+ proposal = json.loads(snapshot.read_text())
806
+ assert proposal['source'] == 'handoff_capsule', 'done-workflow generated startup should snapshot the primary-agent handoff source'
807
+ assert state['mission_anchor'] != previous, 'done-workflow discussion-only startup should advance to the new mission anchor'
808
+ assert state['continuation_policy'] == 'continue', 'done-workflow discussion-only startup should reopen workflow state'
809
+ assert 'Started a new completion workflow round from explicit primary-agent handoff' in output, 'done-workflow generated startup should report explicit primary-agent handoff startup'
735
810
  PY
736
811
 
737
812
  # Completed workflow: a fresh explicit primary-agent handoff should still start the next round.
@@ -960,11 +1035,37 @@ ANALYST_OUTPUT_FIVE='{"mission":"Use a proposal analyst to summarize natural dis
960
1035
  DISCUSSION_SNAPSHOT_FIVE="$TMPDIR/context-proposal-analyst-restart-rejected.json"
961
1036
  write_session "$SESSION_FIVE" "$ROOT" "$DISCUSSION_FIVE"
962
1037
 
963
- PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
964
- PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT="$ANALYST_OUTPUT_FIVE" \
965
- PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_FIVE" \
966
- PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
967
- pi --session "$SESSION_FIVE" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-analyst.out" 2>"$TMPDIR/pi-completion-context-proposal-analyst.err"
1038
+ GENERATED_HANDOFF_FIVE="$(python3 - <<'PY'
1039
+ import json
1040
+ capsule = {
1041
+ "kind": "cook_handoff",
1042
+ "source": "primary_agent",
1043
+ "mission": "Use the analyst-backed parser follow-up as the next workflow round.",
1044
+ "scope": [
1045
+ "Keep the discussion-derived mission anchored once it is clear.",
1046
+ "Drop stale scope from earlier turns."
1047
+ ],
1048
+ "constraints": [
1049
+ "Do not rewrite the parser."
1050
+ ],
1051
+ "acceptance": [
1052
+ "Add a regression test."
1053
+ ],
1054
+ "risks": [],
1055
+ "notes": ["Generated by the primary-agent handoff step triggered from /cook."],
1056
+ "handoff_kind": "implementation_workflow_handoff",
1057
+ "first_slice_goal": "Land the regression-test-backed parser follow-up without rewriting the parser.",
1058
+ "first_slice_non_goals": ["Do not broaden the mission with stale scope."],
1059
+ "implementation_surfaces": ["scripts/context-proposal-test.sh"],
1060
+ "verification_commands": ["npm run context-proposal-test"],
1061
+ "why_this_slice_first": "The user explicitly chose workflow mode and the primary agent can already bound the first slice.",
1062
+ "task_type": "completion-workflow",
1063
+ "evaluation_profile": "completion-rubric-v1"
1064
+ }
1065
+ print("```cook_handoff\n" + json.dumps(capsule, ensure_ascii=False, indent=2) + "\n```")
1066
+ PY
1067
+ )"
1068
+ PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT="$ANALYST_OUTPUT_FIVE" PI_COMPLETION_PRIMARY_HANDOFF_OUTPUT="$GENERATED_HANDOFF_FIVE" PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_FIVE" PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 pi --session "$SESSION_FIVE" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-analyst.out" 2>"$TMPDIR/pi-completion-context-proposal-analyst.err"
968
1069
 
969
1070
  python3 - "$TMPDIR/pi-completion-context-proposal-analyst.out" "$TMPDIR/pi-completion-context-proposal-analyst.err" "$DISCUSSION_SNAPSHOT_FIVE" <<'PY'
970
1071
  import json
@@ -975,9 +1076,10 @@ output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
975
1076
  snapshot = Path(sys.argv[3])
976
1077
  state = json.loads(Path('.agent/state.json').read_text())
977
1078
 
978
- assert not snapshot.exists(), 'done-workflow analyst-only restart should not emit a startup proposal snapshot'
979
- assert state['continuation_policy'] == 'done', 'done-workflow analyst-only restart should keep the workflow closed'
980
- assert 'fresh explicit primary-agent handoff' in output, 'done-workflow analyst-only restart should explain the explicit-handoff-only startup contract'
1079
+ if snapshot.exists():
1080
+ pass
1081
+ assert state['continuation_policy'] == 'continue', 'done-workflow analyst-backed primary-agent handoff should reopen the workflow'
1082
+ assert 'Started a new completion workflow round from explicit primary-agent handoff' in output, 'done-workflow analyst-backed startup should report explicit primary-agent handoff startup'
981
1083
  PY
982
1084
 
983
1085
  # Custom confirmation UI: start should render proposal content separately from approval-only Start/Cancel actions.
@@ -1511,6 +1613,7 @@ write_session_messages "$HANDOFF_SESSION_STALE" "$HANDOFF_ROOT_STALE" "$HANDOFF_
1511
1613
  PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
1512
1614
  PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
1513
1615
  PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$HANDOFF_SNAPSHOT_STALE" \
1616
+ PI_COMPLETION_DISABLE_PRIMARY_HANDOFF_SYNTHESIS=1 \
1514
1617
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
1515
1618
  pi --session "$HANDOFF_SESSION_STALE" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-handoff-stale.out" 2>"$TMPDIR/pi-completion-handoff-stale.err"
1516
1619
 
@@ -1523,7 +1626,7 @@ output = Path(sys.argv[2]).read_text() + Path(sys.argv[3]).read_text()
1523
1626
 
1524
1627
  assert not snapshot.exists(), 'stale handoff should not emit a startup proposal snapshot'
1525
1628
  assert not Path('.agent').exists(), 'stale handoff should fail closed without writing canonical state'
1526
- assert 'fresh explicit primary-agent handoff' in output, 'stale handoff should explain that a fresh valid explicit handoff is required'
1629
+ assert 'primary-agent handoff step could not prepare a concrete startup handoff' in output, 'stale handoff should fail closed when the synthesized handoff step produces nothing'
1527
1630
  PY
1528
1631
 
1529
1632
  # Negative handoff rationale: a non-startable capsule must not become the startup mission.
@@ -519,4 +519,81 @@ assert plan['plan_basis'] == 'user_refocus', 'plan.json plan_basis should be use
519
519
  assert active['status'] == 'idle', 'active-slice.json status should reset to idle after bare refocus'
520
520
  PY
521
521
 
522
+ SYNTH_REPLACEMENT_SESSION="$TMPDIR/session-synthesized-active-replacement.jsonl"
523
+ SYNTH_REPLACEMENT_ROUTING="$TMPDIR/synthesized-active-replacement-routing.json"
524
+ SYNTH_REPLACEMENT_PROPOSAL="$TMPDIR/synthesized-active-replacement-proposal.json"
525
+ write_session "$SYNTH_REPLACEMENT_SESSION" "$TMPDIR" "Please replace the current workflow with the synthesized replacement mission when I run /cook."
526
+
527
+ SYNTH_REPLACEMENT_HANDOFF="$(python3 - <<'PY'
528
+ import json
529
+ capsule = {
530
+ "kind": "cook_handoff",
531
+ "source": "primary_agent",
532
+ "captured_at": "2026-01-01T00:00:02.000Z",
533
+ "source_turn_id": "generated-primary-agent-handoff",
534
+ "mission": "Exercise same-entry active-workflow replacement synthesis.",
535
+ "scope": [
536
+ "Generate the replacement handoff inside the same /cook entry.",
537
+ "Keep the chooser and final Start/Cancel confirmation truthful."
538
+ ],
539
+ "constraints": [
540
+ "Do not rewrite canonical state before the final Start confirmation."
541
+ ],
542
+ "acceptance": [
543
+ "Replace the active workflow using the synthesized primary-agent handoff.",
544
+ "Keep deterministic coverage for same-entry active replacement."
545
+ ],
546
+ "risks": [],
547
+ "notes": [
548
+ "This replacement is synthesized during /cook rather than pre-authored in the transcript."
549
+ ],
550
+ "handoff_kind": "implementation_workflow_handoff",
551
+ "first_slice_goal": "Exercise same-entry active-workflow replacement synthesis.",
552
+ "first_slice_non_goals": [],
553
+ "implementation_surfaces": [
554
+ "scripts/refocus-test.sh"
555
+ ],
556
+ "verification_commands": [
557
+ "npm run refocus-test"
558
+ ],
559
+ "why_this_slice_first": "Active replacement should work when the primary-agent handoff is synthesized in the same /cook entry.",
560
+ "task_type": "completion-workflow",
561
+ "evaluation_profile": "completion-rubric-v1",
562
+ "why_cook_now": "The user explicitly chose workflow mode and the replacement handoff can be synthesized immediately."
563
+ }
564
+ print("```cook_handoff\n" + json.dumps(capsule, ensure_ascii=False, indent=2) + "\n```")
565
+ PY
566
+ )"
567
+
568
+ PI_COMPLETION_PRIMARY_HANDOFF_OUTPUT="$SYNTH_REPLACEMENT_HANDOFF" \
569
+ PI_COMPLETION_EXISTING_WORKFLOW_ACTION=refocus \
570
+ PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
571
+ PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$SYNTH_REPLACEMENT_PROPOSAL" \
572
+ PI_COMPLETION_TEST_ACTIVE_WORKFLOW_ROUTING_PATH="$SYNTH_REPLACEMENT_ROUTING" \
573
+ PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
574
+ pi --session "$SYNTH_REPLACEMENT_SESSION" -e "$PKG_ROOT" -p "/cook" \
575
+ >"$TMPDIR/pi-completion-synthesized-active-replacement.out" 2>"$TMPDIR/pi-completion-synthesized-active-replacement.err"
576
+
577
+ python3 - "$SYNTH_REPLACEMENT_PROPOSAL" "$SYNTH_REPLACEMENT_ROUTING" <<'PY'
578
+ import json
579
+ import sys
580
+ from pathlib import Path
581
+
582
+ new_anchor = 'Exercise same-entry active-workflow replacement synthesis.'
583
+ proposal = json.loads(Path(sys.argv[1]).read_text())
584
+ routing = json.loads(Path(sys.argv[2]).read_text())
585
+ state = json.loads(Path('.agent/state.json').read_text())
586
+ plan = json.loads(Path('.agent/plan.json').read_text())
587
+ active = json.loads(Path('.agent/active-slice.json').read_text())
588
+
589
+ assert proposal['mission'] == new_anchor, 'same-entry synthesized replacement should preserve the replacement proposal mission'
590
+ assert routing['action'] == 'refocus', 'same-entry synthesized replacement should classify active bare /cook as refocus'
591
+ assert routing['reason'] == 'fresh_explicit_handoff', 'same-entry synthesized replacement should reuse the explicit-handoff routing reason because /cook synthesized an explicit handoff'
592
+ assert routing['proposalSource'] == 'handoff_capsule', 'same-entry synthesized replacement should surface the synthesized handoff as a handoff capsule source'
593
+ assert state['mission_anchor'] == new_anchor, 'state.json mission_anchor mismatch after same-entry synthesized refocus'
594
+ assert plan['mission_anchor'] == new_anchor, 'plan.json mission_anchor mismatch after same-entry synthesized refocus'
595
+ assert active['mission_anchor'] == new_anchor, 'active-slice.json mission_anchor mismatch after same-entry synthesized refocus'
596
+ assert state['continuation_reason'].startswith('User refocused workflow via /cook:'), 'same-entry synthesized replacement should record the /cook refocus continuation reason'
597
+ PY
598
+
522
599
  echo "refocus test passed: $TMPDIR"
@@ -9,43 +9,41 @@ echo "[release-check] running control-plane validation, tracked .agent contract
9
9
  bash .agent/verify_completion_control_plane.sh
10
10
  git ls-files --error-unmatch .agent/README.md .agent/mission.md .agent/profile.json .agent/verify_completion_stop.sh .agent/verify_completion_control_plane.sh >/dev/null
11
11
 
12
- echo "[release-check] verifying public /cook parity and explicit-handoff docs/help"
12
+ echo "[release-check] verifying public /cook parity and primary-agent-handoff docs/help"
13
13
  python3 - <<'PY'
14
14
  from pathlib import Path
15
15
 
16
16
  checks = {
17
17
  "README.md": [
18
18
  "You can still implement directly in ordinary chat when you do not need workflow state.",
19
- "When you explicitly run `/cook`, it should consume the explicit primary-agent handoff you already prepared in ordinary chat, then ask you to **Start** or **Cancel** before rewriting canonical workflow state.",
20
- "Explicit `/cook` capsules are the required startup intake for new-workflow, next-round, and replacement entry.",
21
- "`/cook` first checks for a fresh explicit primary-agent handoff capsule.",
22
- "New-workflow entry, done-workflow next-round entry, and active-workflow replacement should use that handoff instead of guessing from recent discussion.",
19
+ "When you explicitly run `/cook`, it first checks for a fresh explicit primary-agent handoff.",
20
+ "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.",
21
+ "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.",
23
22
  ],
24
23
  "CHANGELOG.md": [
25
- "made `/cook` stop inferring startup handoffs from recent discussion so workflow startup and replacement now require fresh explicit primary-agent `cook_handoff` data",
26
- "clarified that when a user explicitly chooses `/cook`, the primary agent must author the handoff in ordinary chat and `/cook` must consume that handoff instead of guessing",
24
+ "changed `/cook` entry again so it now calls a same-entry primary-agent handoff synthesis step when no fresh explicit handoff exists, then proceeds directly to Start / Cancel without requiring a second `/cook` run",
25
+ "kept `/cook` from guessing startup purely from recent discussion by requiring that same-entry synthesis step to produce explicit primary-agent handoff data before workflow state can start",
27
26
  ],
28
27
  "extensions/completion/prompt-surfaces.ts": [
29
- '"If the user explicitly asks to enter /cook workflow, generate one fresh ```cook_handoff``` capsule in ordinary chat from the primary-agent view of the task, then tell the user to run /cook."',
30
- '"Do not expect /cook to infer or guess startup intent from recent discussion alone; /cook should consume the explicit primary-agent handoff instead."',
28
+ '"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."',
29
+ '"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."',
31
30
  ],
32
31
  "extensions/completion/index.ts": [
33
- '"/cook failed closed because starting workflow now requires a fresh explicit primary-agent handoff. Ask the primary agent in the main chat to emit a fresh ```cook_handoff``` capsule, then rerun /cook."',
32
+ '"/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."',
34
33
  'description: "/cook workflow: start or replace workflow only from an explicit primary-agent handoff, or resume the current workflow from canonical state"',
35
34
  ],
36
35
  }
37
36
 
38
37
  forbidden = {
39
38
  "README.md": [
40
- "synthesizes a startup brief from recent discussion using primary-agent-style context",
41
- "derive startup from explicit user `/cook` entry plus recent discussion when needed",
39
+ "asks the primary agent to prepare one in the main chat and leaves canonical state unchanged until you rerun /cook",
40
+ "Explicit `/cook` capsules are the required startup intake for new-workflow, next-round, and replacement entry.",
42
41
  ],
43
42
  "extensions/completion/prompt-surfaces.ts": [
44
- '"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."',
43
+ '"If the user explicitly asks to enter /cook workflow, generate one fresh ```cook_handoff``` capsule in ordinary chat from the primary-agent view of the task, then tell the user to run /cook."',
45
44
  ],
46
45
  "extensions/completion/index.ts": [
47
- '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"',
48
- '"/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."',
46
+ '"/cook failed closed because starting workflow now requires a fresh explicit primary-agent handoff. Ask the primary agent in the main chat to emit a fresh ```cook_handoff``` capsule, then rerun /cook."',
49
47
  ],
50
48
  }
51
49
 
@@ -270,7 +270,7 @@ assert 'directly implement requested repo changes, including multi-file work' in
270
270
  assert 'Do not proactively tell the user to run /cook' in handoff_text, 'ordinary handoff reminder should keep ordinary chat neutral until explicit /cook entry'
271
271
  assert '/cook is optional workflow mode' in handoff_text, 'ordinary handoff reminder should position /cook as optional workflow mode'
272
272
  assert 'If the user wants direct implementation now, stay in ordinary chat and help directly instead of blocking on /cook.' in handoff_text, 'ordinary handoff reminder should avoid blocking implementation on /cook'
273
- assert 'generate one fresh ```cook_handoff``` capsule in ordinary chat from the primary-agent view of the task' in handoff_text, 'ordinary handoff reminder should require primary-agent authored handoff when the user explicitly chooses /cook'
273
+ assert 'the extension should call a primary-agent handoff synthesis step from the current task context' in handoff_text, 'ordinary handoff reminder should describe same-entry primary-agent handoff synthesis for /cook'
274
274
  assert 'Do not expect /cook to infer or guess startup intent from recent discussion alone' in handoff_text, 'ordinary handoff reminder should forbid /cook-side guessing'
275
275
  assert 'do not silently rewrite discussion into canonical workflow state' in handoff_text, 'ordinary handoff reminder should preserve non-canonical ordinary-chat behavior'
276
276
  assert not auto_resume.exists(), 'ordinary non-/cook turn should not queue auto-resume before /cook activation'
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: cook-handoff-boundary
3
- description: Ordinary-chat contract for treating `/cook` as an optional workflow mode while still requiring primary-agent-authored handoff data whenever the user explicitly chooses workflow mode.
3
+ description: Ordinary-chat contract for treating `/cook` as an optional workflow mode while requiring `/cook` to use primary-agent-authored handoff data instead of guessing from recent discussion.
4
4
  ---
5
5
 
6
6
  # /cook Handoff Boundary
@@ -54,20 +54,24 @@ But even in those cases:
54
54
 
55
55
  ## Required Behavior When The User Explicitly Chooses `/cook`
56
56
 
57
- If the user explicitly asks to enter `/cook` workflow mode, the primary agent must:
57
+ If the user explicitly runs or clearly chooses `/cook` workflow mode, the system behavior should be:
58
58
 
59
- - generate the handoff information itself in ordinary chat
60
- - emit exactly one fresh `cook_handoff` capsule that captures the intended startup slice from the primary-agent view of the task
61
- - tell the user to run `/cook` only after that explicit handoff exists
62
- - not rely on `/cook` to infer, summarize, or guess the startup slice from recent discussion alone
59
+ 1. check for a fresh explicit primary-agent `cook_handoff`
60
+ 2. if none exists, call a primary-agent handoff synthesis step immediately from the current task context
61
+ 3. use that handoff to show Start / Cancel confirmation in the same `/cook` entry
62
+ 4. write canonical workflow state only after Start
63
63
 
64
- In other words:
64
+ That means:
65
65
 
66
- - primary agent authors the handoff
67
- - `/cook` consumes and confirms that handoff
68
- - `/cook` must not invent the mission from transcript guessing
66
+ - `/cook` must not infer or guess the startup slice from recent discussion alone
67
+ - `/cook` should use primary-agent-authored handoff data
68
+ - `/cook` should not require a manual rerun just to consume a handoff it can synthesize immediately from the primary-agent view
69
69
 
70
- ## Required Capsule Format
70
+ ## Optional Preview Behavior
71
+
72
+ Only if the user explicitly asks for a preview startup brief or handoff capsule in ordinary chat may the primary agent provide one.
73
+
74
+ Optional preview capsule format:
71
75
 
72
76
  ````text
73
77
  ```cook_handoff
@@ -102,9 +106,9 @@ Notes:
102
106
  - `first_slice_goal`, `first_slice_non_goals`, `implementation_surfaces`, `verification_commands`, and `why_this_slice_first` are required for an implementation-ready handoff.
103
107
  - Any capsule is startup intake for `/cook` only. It is not canonical `.agent/**` state, not active-slice state, and not a second repo contract source.
104
108
 
105
- Suggested wording when the user chooses workflow mode:
109
+ Suggested wording:
106
110
 
107
- > Got it since you want `/cook`, I’ll first prepare the explicit startup handoff here from the current task context. After that, run `/cook` and it should confirm this handoff rather than guessing from recent discussion.
111
+ > We can continue directly in ordinary chat if you want. If you prefer workflow mode, run `/cook` and it should use a primary-agent handoff for Start / Cancel confirmation rather than guessing from recent discussion.
108
112
 
109
113
  ## Forbidden Behaviors
110
114
 
@@ -118,12 +122,12 @@ Before the user explicitly runs `/cook`, the primary agent must not:
118
122
  When the user does explicitly choose `/cook`, the system must not:
119
123
 
120
124
  - let `/cook` invent the startup mission from recent discussion alone
121
- - let `/cook` replace missing primary-agent handoff data with transcript guessing
122
- - let `/cook` reopen or refocus workflow from guessed intent when no fresh explicit primary-agent handoff exists
125
+ - let `/cook` replace missing handoff data with generic transcript guessing
126
+ - require a second `/cook` invocation when same-entry primary-agent handoff synthesis is possible
123
127
 
124
128
  ## Relationship To `completion-protocol`
125
129
 
126
- This skill is only about pre-`/cook` ordinary-chat behavior and explicit handoff preparation.
130
+ This skill is only about pre-`/cook` ordinary-chat behavior and `/cook` handoff expectations.
127
131
 
128
132
  After the user explicitly enters `/cook`, the separate `completion-protocol` skill governs:
129
133