@linimin/pi-letscook 0.1.66 → 0.1.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,7 +18,9 @@ import {
18
18
  collectRecentDiscussionEntries,
19
19
  collectRecentSessionMessages,
20
20
  assessLatestCookHandoffProposal,
21
+ deriveCookContextProposalFromRecentDiscussion,
21
22
  finalizeContextProposalAnalysis,
23
+ hasStructuredContextProposalSignal,
22
24
  isWeakMissionAnchor,
23
25
  missionAnchorsLikelyEquivalent,
24
26
  missionAnchorsStrictlyEquivalent,
@@ -47,7 +49,8 @@ import {
47
49
  maybeWriteContextProposalSnapshot,
48
50
  } from "./prompt-surfaces";
49
51
  import { toolCallBlockReason } from "./policy-guards";
50
- import { analyzeContextProposalWithAgent, generateCookHandoffWithAgent, runCompletionRole } from "./role-runner";
52
+ import { analyzeContextProposalWithAgent, runCompletionRole } from "./role-runner";
53
+ import { generateCookHandoffWithAgent } from "./role-runner";
51
54
  import {
52
55
  applyLiveRoleEvent,
53
56
  buildInlineRunningLines,
@@ -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 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
+ "/cook failed closed because the startup-plan step could not prepare a concrete workflow startup plan from the current task context. Clarify the mission, scope, acceptance, 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";
@@ -418,10 +421,23 @@ async function deriveCookContextProposal(
418
421
  `latest verified slice: ${asString(snapshot.state?.latest_verified_slice) ?? "(none)"}`,
419
422
  `active slice goal: ${asString(snapshot.active?.goal) ?? "(none)"}`,
420
423
  `active slice why_now: ${asString(snapshot.active?.why_now) ?? "(none)"}`,
424
+ `approved startup plan summary: ${asString(snapshot.startupPlan?.goal_text) ?? "(none)"}`,
421
425
  `verification goal: ${asString(snapshot.verificationEvidence?.goal) ?? "(none)"}`,
422
426
  `verification summary: ${asString(snapshot.verificationEvidence?.summary) ?? "(none)"}`,
423
427
  ]
424
428
  : [];
429
+ const workflowContext = snapshot
430
+ ? {
431
+ currentMissionAnchor: asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? asString(snapshot.active?.mission_anchor),
432
+ latestCompletedSlice: asString(snapshot.state?.latest_completed_slice),
433
+ latestVerifiedSlice: asString(snapshot.state?.latest_verified_slice),
434
+ activeSliceGoal: asString(snapshot.active?.goal),
435
+ activeSliceWhyNow: asString(snapshot.active?.why_now),
436
+ verificationGoal: asString(snapshot.verificationEvidence?.goal),
437
+ verificationSummary: asString(snapshot.verificationEvidence?.summary),
438
+ continuationPolicy: asString(snapshot.state?.continuation_policy),
439
+ }
440
+ : undefined;
425
441
  const raw = await generateCookHandoffWithAgent({
426
442
  ctx,
427
443
  projectName,
@@ -434,20 +450,49 @@ async function deriveCookContextProposal(
434
450
  getCtxHasUI,
435
451
  getCtxUi,
436
452
  });
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 };
453
+ if (raw) {
454
+ const generated = assessLatestCookHandoffProposal([
455
+ { role: "assistant", text: raw, messageId: "generated-primary-agent-handoff", timestampMs: Date.now(), isCommand: false },
456
+ ], projectName, {
457
+ asString,
458
+ asStringArray,
459
+ assessMissionAnchor,
460
+ normalizeMissionAnchorText,
461
+ isWeakMissionAnchor,
462
+ missionAnchorsStrictlyEquivalent,
463
+ stripCodeBlocks,
464
+ });
465
+ if (generated.status === "startable") return { proposal: generated.proposal };
466
+ if (generated.status === "fresh_but_not_startable") return { blockedFailureMessage: generated.message };
467
+ }
468
+ const canFallbackToDiscussionProposal =
469
+ recentEntries.length >= 2 || recentEntries.some((entry) => hasStructuredContextProposalSignal(entry.text, stripCodeBlocks));
470
+ if (canFallbackToDiscussionProposal) {
471
+ const fromDiscussion = await deriveCookContextProposalFromRecentDiscussion(projectName, recentEntries, {
472
+ asString,
473
+ asStringArray,
474
+ assessMissionAnchor,
475
+ normalizeMissionAnchorText,
476
+ isWeakMissionAnchor,
477
+ missionAnchorsStrictlyEquivalent,
478
+ stripCodeBlocks,
479
+ analyzeContextProposal: async (candidateEntries) =>
480
+ await analyzeContextProposalWithAgent({
481
+ ctx,
482
+ projectName,
483
+ recentEntries: candidateEntries,
484
+ workflowContextLines,
485
+ liveRoleActivityByRoot,
486
+ completionStatusKey: COMPLETION_STATUS_KEY,
487
+ safeUiCall,
488
+ getCtxCwd,
489
+ getCtxHasUI,
490
+ getCtxUi,
491
+ }),
492
+ workflowContext,
493
+ });
494
+ if (fromDiscussion) return { proposal: fromDiscussion };
495
+ }
451
496
  return {};
452
497
  }
453
498
 
@@ -485,13 +530,19 @@ async function confirmContextProposal(
485
530
  async function scaffoldCompletionFiles(
486
531
  root: string,
487
532
  missionAnchor: string,
488
- options?: { analysis?: ContextProposalAnalysis; continuationReason?: string; advisoryStartupBrief?: JsonRecord },
533
+ options?: {
534
+ analysis?: ContextProposalAnalysis;
535
+ continuationReason?: string;
536
+ advisoryStartupBrief?: JsonRecord;
537
+ approvedStartupPlan?: JsonRecord;
538
+ },
489
539
  ) {
490
540
  const routing = finalizeContextProposalAnalysis(options?.analysis, [missionAnchor]);
491
541
  return await scaffoldCompletionFilesOnDisk(root, missionAnchor, {
492
542
  analysis: { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile },
493
543
  continuationReason: options?.continuationReason,
494
544
  advisoryStartupBrief: options?.advisoryStartupBrief,
545
+ approvedStartupPlan: options?.approvedStartupPlan,
495
546
  });
496
547
  }
497
548
 
@@ -654,6 +705,20 @@ function verificationEvidenceContext(snapshot: CompletionStateSnapshot) {
654
705
  };
655
706
  }
656
707
 
708
+ function startupPlanContext(snapshot: CompletionStateSnapshot) {
709
+ const startupPlan = snapshot.startupPlan;
710
+ return {
711
+ path: path.relative(snapshot.files.root, snapshot.files.startupPlanPath) || ".agent/startup-plan.json",
712
+ status: startupPlan ? "present" : "missing",
713
+ source: asString(startupPlan?.source),
714
+ plannedSurfaces: asStringArray(startupPlan?.planned_surfaces),
715
+ verificationIntent: asStringArray(startupPlan?.verification_intent),
716
+ summary:
717
+ asString(startupPlan?.goal_text) ??
718
+ (startupPlan ? "Approved startup plan is present but its goal_text is missing." : "Approved startup plan is missing."),
719
+ };
720
+ }
721
+
657
722
  function buildEvaluationRoleContextLines(snapshot: CompletionStateSnapshot, role: RubricEvaluationRole): string[] {
658
723
  return buildExtractedEvaluationRoleContextLines(snapshot, role, {
659
724
  asString,
@@ -684,6 +749,7 @@ function composeSystemReminder(snapshot: CompletionStateSnapshot, sliceHistory:
684
749
  const exactActiveContract = activeCarriesExactHandoff(snapshot.active);
685
750
  const activeContractDrift = activeSliceContractDriftSummary(snapshot);
686
751
  const evidence = verificationEvidenceContext(snapshot);
752
+ const startupPlan = startupPlanContext(snapshot);
687
753
  const activePriorityLine = activePriority !== undefined ? `Active slice priority: ${activePriority}` : undefined;
688
754
  const activeWhyNowLine = activeWhyNow ? `Active slice why_now: ${activeWhyNow}` : undefined;
689
755
  const implementationSurfacesLine =
@@ -713,6 +779,7 @@ function composeSystemReminder(snapshot: CompletionStateSnapshot, sliceHistory:
713
779
  implementationSurfacesLine,
714
780
  verificationCommandsLine,
715
781
  evidence,
782
+ startupPlan,
716
783
  evaluationRoleReminderText: isRubricEvaluationRole(nextRole) ? buildEvaluationRoleReminderText(snapshot, nextRole) : undefined,
717
784
  });
718
785
  }
@@ -732,17 +799,19 @@ function buildPostCompactionDriverInstructions(snapshot: CompletionStateSnapshot
732
799
  const exactActiveContract = activeCarriesExactHandoff(snapshot.active);
733
800
  const activeContractDrift = activeSliceContractDriftSummary(snapshot);
734
801
  const evidence = verificationEvidenceContext(snapshot);
802
+ const startupPlan = startupPlanContext(snapshot);
735
803
  const lines = [
736
804
  "POST-COMPACTION RECOVERY MODE is active.",
737
805
  `Compaction marker time: ${markerAt}`,
738
806
  "Treat the previous conversation as lossy continuity support only.",
739
- "Before taking any substantive action, re-read .agent/state.json, .agent/plan.json, .agent/active-slice.json, .agent/slice-history.jsonl, .agent/stop-check-history.jsonl, and .agent/verification-evidence.json from disk.",
807
+ "Before taking any substantive action, re-read .agent/state.json, .agent/startup-plan.json, .agent/plan.json, .agent/active-slice.json, .agent/slice-history.jsonl, .agent/stop-check-history.jsonl, and .agent/verification-evidence.json from disk.",
740
808
  `Canonical task_type is currently: ${taskType}`,
741
809
  `Canonical evaluation_profile is currently: ${evaluationProfile}`,
742
810
  `Canonical next mandatory role is currently: ${nextRole}`,
743
811
  `Canonical next mandatory action is currently: ${nextAction}`,
744
812
  `Canonical continuation policy is currently: ${continuation}`,
745
813
  `Canonical active slice is currently: ${activeSliceId}`,
814
+ `Canonical approved startup plan is currently: ${startupPlan.path} (${startupPlan.status})`,
746
815
  `Canonical verification evidence artifact is currently: ${evidence.path} (${evidence.status})`,
747
816
  "Do not trust pre-compaction memory over canonical files.",
748
817
  "If the canonical state is ambiguous, inconsistent, missing, or stale after re-reading it, your first mandatory action is to dispatch completion-regrounder rather than guessing.",
@@ -757,6 +826,10 @@ function buildPostCompactionDriverInstructions(snapshot: CompletionStateSnapshot
757
826
  if (activeWhyNow) lines.push(`Canonical active-slice why_now is currently: ${activeWhyNow}`);
758
827
  if (implementationSurfaces.length > 0) lines.push(`Canonical implementation surfaces are currently: ${implementationSurfaces.join(", ")}`);
759
828
  if (verificationCommands.length > 0) lines.push(`Canonical verification commands are currently: ${verificationCommands.join(" | ")}`);
829
+ if (startupPlan.source) lines.push(`Canonical approved startup plan source is currently: ${startupPlan.source}`);
830
+ if (startupPlan.plannedSurfaces.length > 0) lines.push(`Canonical approved startup plan surfaces are currently: ${startupPlan.plannedSurfaces.join(" | ")}`);
831
+ if (startupPlan.verificationIntent.length > 0) lines.push(`Canonical approved startup plan verification intent is currently: ${startupPlan.verificationIntent.join(" | ")}`);
832
+ lines.push(`Canonical approved startup plan summary is currently: ${startupPlan.summary}`);
760
833
  if (evidence.subjectType) lines.push(`Canonical verification evidence subject is currently: ${evidence.subjectType}`);
761
834
  if (evidence.outcome) lines.push(`Canonical verification evidence outcome is currently: ${evidence.outcome}`);
762
835
  if (evidence.recordedAt) lines.push(`Canonical verification evidence recorded_at is currently: ${evidence.recordedAt}`);
@@ -847,6 +920,7 @@ function composeResumeCapsule(snapshot: CompletionStateSnapshot, sliceHistory: J
847
920
  const verificationCommands = asStringArray(snapshot.active?.verification_commands);
848
921
  const remainingBefore = asStringArray(snapshot.active?.remaining_contract_ids_before);
849
922
  const evidence = verificationEvidenceContext(snapshot);
923
+ const startupPlan = startupPlanContext(snapshot);
850
924
  const implementationSurfacesLine =
851
925
  implementationSurfaces.length > 0 ? `- implementation_surfaces: ${implementationSurfaces.join(" | ")}` : undefined;
852
926
  const verificationCommandsLine =
@@ -867,6 +941,7 @@ function composeResumeCapsule(snapshot: CompletionStateSnapshot, sliceHistory: J
867
941
  activeSliceMatchesPlan: activeSliceMatchesPlan(snapshot),
868
942
  activeSliceContractDrift: activeSliceContractDriftSummary(snapshot),
869
943
  implementerHandoffSnapshot: handoffSnapshotState(snapshot.active),
944
+ startupPlan,
870
945
  evidence,
871
946
  activeSlice: {
872
947
  sliceId: asString(snapshot.active?.slice_id) ?? asString(snapshot.activeSlice?.slice_id),
@@ -904,11 +979,11 @@ function completionKickoff(
904
979
  : intent === "refocus" && missionAnchor
905
980
  ? `Updated canonical mission anchor:\n${missionAnchor}\n\nWorkflow intent:\n- The user explicitly refocused the workflow before this kickoff.\n- Re-read canonical .agent/** state and continue from the refocused mission.\n\n`
906
981
  : "";
907
- return `/skill:completion-protocol Start or continue the completion workflow for this repo.\n\nBefore acting, read:\n- ${SKILL_PATH}\n- ${REFERENCE_PATH}\n\nCanonical routing profile:\n- task_type: ${taskType}\n- evaluation_profile: ${evaluationProfile}\n\nUser goal:\n${goal}\n\n${intentBlock}Driver instructions:\n- Canonical truth is in .agent/**. Re-read .agent/state.json, .agent/plan.json, .agent/active-slice.json, and .agent/verification-evidence.json before acting when they exist.\n- If tracked completion contract files are missing or onboarding is required, invoke completion_role with role completion-bootstrapper.\n- Otherwise follow the mandatory dispatch rules from completion-protocol.\n- For selected, in-progress, committed, or done slices, treat .agent/active-slice.json as the canonical implementation contract and route to completion-regrounder if it drifts from the selected plan slice or the exact handoff is unclear.\n- Consume .agent/verification-evidence.json instead of temp-only verification summaries when it is populated.\n- Use completion_role for all completion-* role work. Do not directly implement tracked product changes yourself.\n- Continue dispatching mandatory roles while continuation_policy == continue.\n- Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.`;
982
+ return `/skill:completion-protocol Start or continue the completion workflow for this repo.\n\nBefore acting, read:\n- ${SKILL_PATH}\n- ${REFERENCE_PATH}\n\nCanonical routing profile:\n- task_type: ${taskType}\n- evaluation_profile: ${evaluationProfile}\n\nUser goal:\n${goal}\n\n${intentBlock}Driver instructions:\n- Canonical truth is in .agent/**. Re-read .agent/state.json, .agent/startup-plan.json, .agent/plan.json, .agent/active-slice.json, and .agent/verification-evidence.json before acting when they exist.\n- Treat .agent/startup-plan.json as the approved startup plan captured at /cook. completion-regrounder should use it as planning input, then derive canonical slices from current repo truth.\n- If tracked completion contract files are missing or onboarding is required, invoke completion_role with role completion-bootstrapper.\n- Otherwise follow the mandatory dispatch rules from completion-protocol.\n- For selected, in-progress, committed, or done slices, treat .agent/active-slice.json as the canonical implementation contract and route to completion-regrounder if it drifts from the selected plan slice or the exact handoff is unclear.\n- Consume .agent/verification-evidence.json instead of temp-only verification summaries when it is populated.\n- Use completion_role for all completion-* role work. Do not directly implement tracked product changes yourself.\n- Continue dispatching mandatory roles while continuation_policy == continue.\n- Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.`;
908
983
  }
909
984
 
910
985
  function completionResumePrompt(taskType: string, evaluationProfile: string): string {
911
- return `/skill:completion-protocol Resume the completion workflow from canonical state.\n\nBefore acting, read:\n- ${SKILL_PATH}\n- ${REFERENCE_PATH}\n\nCanonical routing profile:\n- task_type: ${taskType}\n- evaluation_profile: ${evaluationProfile}\n\nResume instructions:\n- Re-read .agent/state.json, .agent/plan.json, .agent/active-slice.json, and .agent/verification-evidence.json before acting.\n- If canonical state is missing, invalid, contradictory, stale, or ambiguous, route to completion-regrounder first.\n- For selected, in-progress, committed, or done slices, treat .agent/active-slice.json as the canonical implementation contract and route to completion-regrounder if it drifts from the selected plan slice or the exact handoff is unclear.\n- Consume .agent/verification-evidence.json instead of temp-only verification summaries when it is populated.\n- Continue from next_mandatory_role and next_mandatory_action.\n- Use completion_role for all completion-* role work.\n- Continue dispatching mandatory roles while continuation_policy == continue.\n- Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.`;
986
+ return `/skill:completion-protocol Resume the completion workflow from canonical state.\n\nBefore acting, read:\n- ${SKILL_PATH}\n- ${REFERENCE_PATH}\n\nCanonical routing profile:\n- task_type: ${taskType}\n- evaluation_profile: ${evaluationProfile}\n\nResume instructions:\n- Re-read .agent/state.json, .agent/startup-plan.json, .agent/plan.json, .agent/active-slice.json, and .agent/verification-evidence.json before acting.\n- Treat .agent/startup-plan.json as the approved workflow plan captured at /cook, then let completion-regrounder reconcile or rebuild canonical slices from repo truth when needed.\n- If canonical state is missing, invalid, contradictory, stale, or ambiguous, route to completion-regrounder first.\n- For selected, in-progress, committed, or done slices, treat .agent/active-slice.json as the canonical implementation contract and route to completion-regrounder if it drifts from the selected plan slice or the exact handoff is unclear.\n- Consume .agent/verification-evidence.json instead of temp-only verification summaries when it is populated.\n- Continue from next_mandatory_role and next_mandatory_action.\n- Use completion_role for all completion-* role work.\n- Continue dispatching mandatory roles while continuation_policy == continue.\n- Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.`;
912
987
  }
913
988
 
914
989
  export default function completionExtension(pi: ExtensionAPI) {
@@ -924,7 +999,7 @@ export default function completionExtension(pi: ExtensionAPI) {
924
999
  structuredDiscussionFailureDetail: COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL,
925
1000
  mainChatRerunGuidance: COOK_MAIN_CHAT_RERUN_GUIDANCE,
926
1001
  cookCommandSpec: {
927
- description: "/cook workflow: start or replace workflow only from an explicit primary-agent handoff, or resume the current workflow from canonical state",
1002
+ description: "/cook workflow: capture the approved startup plan into .agent, let completion-regrounder split canonical slices, or resume the current workflow from canonical state",
928
1003
  },
929
1004
  buildContextProposalContinuationReason,
930
1005
  completionKickoff,
@@ -24,6 +24,26 @@ export type AdvisoryStartupBrief = {
24
24
  evaluation_profile?: string;
25
25
  };
26
26
 
27
+ export type ApprovedStartupPlan = {
28
+ artifact_type: "completion-startup-plan";
29
+ schema_version: 1;
30
+ status: "approved";
31
+ source: AdvisoryStartupBrief["source"];
32
+ captured_at: string;
33
+ mission_anchor: string;
34
+ goal_text: string;
35
+ task_type?: string;
36
+ evaluation_profile?: string;
37
+ scope: string[];
38
+ constraints: string[];
39
+ acceptance: string[];
40
+ risks: string[];
41
+ notes: string[];
42
+ planned_surfaces: string[];
43
+ verification_intent: string[];
44
+ sequencing_hints: string[];
45
+ };
46
+
27
47
  export function buildCookHandoffBoundaryReminder(): string {
28
48
  return [
29
49
  "You are in ordinary main chat unless the user explicitly runs /cook.",
@@ -33,10 +53,11 @@ export function buildCookHandoffBoundaryReminder(): string {
33
53
  "In ordinary chat, do not load or follow completion-protocol, and do not call completion_role.",
34
54
  "If the user wants direct implementation now, stay in ordinary chat and help directly instead of blocking on /cook.",
35
55
  "If the user asks follow-up questions or wants to keep refining scope, continue helping naturally in ordinary chat.",
36
- "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.",
37
- "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.",
38
- "Only provide a preview startup brief or ```cook_handoff``` capsule in ordinary chat when the user explicitly asks for that preview behavior.",
56
+ "If the user explicitly runs /cook, the extension should call a primary-agent startup-plan synthesis step from the current task context, show Start/Cancel confirmation in the same /cook entry, and only write the approved plan into .agent after Start.",
57
+ "Do not expect /cook to infer or guess startup intent from recent discussion alone; /cook should use primary-agent-authored startup-plan data, whether it already exists as preview intake or is synthesized in the same /cook entry.",
58
+ "Only provide a preview startup plan or ```cook_handoff``` capsule in ordinary chat when the user explicitly asks for that preview behavior.",
39
59
  "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.",
60
+ "When /cook starts, the approved startup plan should be written into .agent and then handed to completion-regrounder so canonical slices can be derived from repo truth.",
40
61
  "When you continue in ordinary chat, do not pretend /cook already started and do not silently rewrite discussion into canonical workflow state.",
41
62
  ].join(" ");
42
63
  }
@@ -88,6 +109,27 @@ function buildAdvisoryStartupBriefNotes(analysis: ContextProposalAnalysis): stri
88
109
  return notes.length > 0 ? notes : ["No additional operator notes were derived from recent discussion."];
89
110
  }
90
111
 
112
+ function startupPlanSourceForProposal(source: ContextProposal["source"]): AdvisoryStartupBrief["source"] {
113
+ return source === "handoff_capsule"
114
+ ? "primary_agent_handoff"
115
+ : source === "deferred_primary_agent_handoff"
116
+ ? "deferred_primary_agent_handoff"
117
+ : "recent_discussion";
118
+ }
119
+
120
+ function extractDelimitedNoteValues(notes: string[], prefix: string): string[] {
121
+ return notes.flatMap((note) => {
122
+ if (!note.startsWith(prefix)) return [];
123
+ return note.slice(prefix.length).split("|").map((item) => item.trim()).filter(Boolean);
124
+ });
125
+ }
126
+
127
+ function extractSequencingHints(notes: string[]): string[] {
128
+ return notes.filter((note) =>
129
+ note.startsWith("First slice goal:") || note.startsWith("First slice non-goals:") || note.startsWith("Why this slice first:"),
130
+ );
131
+ }
132
+
91
133
  export function buildAdvisoryStartupBrief(args: {
92
134
  proposal: Pick<ContextProposal, "goalText" | "mission" | "scope" | "constraints" | "acceptance" | "source">;
93
135
  analysis: ContextProposalAnalysis;
@@ -95,12 +137,7 @@ export function buildAdvisoryStartupBrief(args: {
95
137
  }): AdvisoryStartupBrief {
96
138
  return {
97
139
  kind: "startup_brief",
98
- source:
99
- args.proposal.source === "handoff_capsule"
100
- ? "primary_agent_handoff"
101
- : args.proposal.source === "deferred_primary_agent_handoff"
102
- ? "deferred_primary_agent_handoff"
103
- : "recent_discussion",
140
+ source: startupPlanSourceForProposal(args.proposal.source),
104
141
  confirmed: true,
105
142
  captured_at: args.capturedAt ?? new Date().toISOString(),
106
143
  goal_text: args.proposal.goalText,
@@ -115,6 +152,81 @@ export function buildAdvisoryStartupBrief(args: {
115
152
  };
116
153
  }
117
154
 
155
+ export function buildApprovedStartupPlan(args: {
156
+ proposal: Pick<ContextProposal, "goalText" | "mission" | "scope" | "constraints" | "acceptance" | "source">;
157
+ analysis: ContextProposalAnalysis;
158
+ missionAnchor?: string;
159
+ capturedAt?: string;
160
+ }): ApprovedStartupPlan {
161
+ const capturedAt = args.capturedAt ?? new Date().toISOString();
162
+ const notes = buildAdvisoryStartupBriefNotes(args.analysis);
163
+ return {
164
+ artifact_type: "completion-startup-plan",
165
+ schema_version: 1,
166
+ status: "approved",
167
+ source: startupPlanSourceForProposal(args.proposal.source),
168
+ captured_at: capturedAt,
169
+ mission_anchor: args.missionAnchor ?? args.proposal.mission,
170
+ goal_text: args.proposal.goalText,
171
+ task_type: args.analysis.taskType,
172
+ evaluation_profile: args.analysis.evaluationProfile,
173
+ scope: [...args.proposal.scope],
174
+ constraints: [...args.proposal.constraints],
175
+ acceptance: [...args.proposal.acceptance],
176
+ risks: [...args.analysis.risks],
177
+ notes: [...notes],
178
+ planned_surfaces: extractDelimitedNoteValues(notes, "Implementation surfaces:"),
179
+ verification_intent: extractDelimitedNoteValues(notes, "Verification commands:"),
180
+ sequencing_hints: extractSequencingHints(notes),
181
+ };
182
+ }
183
+
184
+ export function buildApprovedStartupPlanMarkdown(plan: ApprovedStartupPlan): string {
185
+ const lines = [
186
+ "# Approved Startup Plan",
187
+ "",
188
+ `Mission anchor: ${plan.mission_anchor}`,
189
+ `Source: ${plan.source}`,
190
+ `Captured at: ${plan.captured_at}`,
191
+ ];
192
+ if (plan.task_type) lines.push(`Task type: ${plan.task_type}`);
193
+ if (plan.evaluation_profile) lines.push(`Evaluation profile: ${plan.evaluation_profile}`);
194
+ lines.push("", "## Goal", "", plan.goal_text);
195
+ if (plan.scope.length > 0) {
196
+ lines.push("", "## Scope", "");
197
+ for (const item of plan.scope) lines.push(`- ${item}`);
198
+ }
199
+ if (plan.constraints.length > 0) {
200
+ lines.push("", "## Constraints", "");
201
+ for (const item of plan.constraints) lines.push(`- ${item}`);
202
+ }
203
+ if (plan.acceptance.length > 0) {
204
+ lines.push("", "## Acceptance", "");
205
+ for (const item of plan.acceptance) lines.push(`- ${item}`);
206
+ }
207
+ if (plan.risks.length > 0) {
208
+ lines.push("", "## Risks", "");
209
+ for (const item of plan.risks) lines.push(`- ${item}`);
210
+ }
211
+ if (plan.planned_surfaces.length > 0) {
212
+ lines.push("", "## Planned surfaces", "");
213
+ for (const item of plan.planned_surfaces) lines.push(`- ${item}`);
214
+ }
215
+ if (plan.verification_intent.length > 0) {
216
+ lines.push("", "## Verification intent", "");
217
+ for (const item of plan.verification_intent) lines.push(`- ${item}`);
218
+ }
219
+ if (plan.sequencing_hints.length > 0) {
220
+ lines.push("", "## Sequencing hints", "");
221
+ for (const item of plan.sequencing_hints) lines.push(`- ${item}`);
222
+ }
223
+ if (plan.notes.length > 0) {
224
+ lines.push("", "## Notes", "");
225
+ for (const item of plan.notes) lines.push(`- ${item}`);
226
+ }
227
+ return `${lines.join("\n")}\n`;
228
+ }
229
+
118
230
  export function buildContextProposalCritiqueText(analysis: ContextProposalAnalysis): string {
119
231
  const lines: string[] = [];
120
232
  if (analysis.critique.length > 0) {
@@ -147,7 +259,7 @@ export function buildContextProposalCritiqueText(analysis: ContextProposalAnalys
147
259
  for (const item of analysis.suppressedNegatedTopics) lines.push(`- ${item}`);
148
260
  }
149
261
  if (lines.length === 0) {
150
- return "No additional operator notes or risks were derived for this startup brief.";
262
+ return "No additional operator notes or risks were derived for this startup plan.";
151
263
  }
152
264
  return lines.join("\n");
153
265
  }
@@ -196,7 +308,7 @@ export function buildContextProposalConfirmationActions(mainChatRerunGuidance: s
196
308
  {
197
309
  id: "start",
198
310
  label: "Start",
199
- description: "Accept this startup brief and let /cook write or refocus canonical workflow state.",
311
+ description: "Accept this startup plan and let /cook write or refocus canonical workflow state.",
200
312
  },
201
313
  {
202
314
  id: "cancel",
@@ -216,8 +328,8 @@ export function buildContextProposalConfirmationLayout(args: {
216
328
  }): ContextProposalConfirmationLayout {
217
329
  return {
218
330
  title: args.title,
219
- intro: "Review the startup brief (mission, scope, constraints, acceptance, and notes/risks) plus the routing details before /cook writes canonical workflow state. This gate is approval-only: either Start it as-is or Cancel, discuss changes in the main chat, and rerun /cook.",
220
- proposalHeading: "Startup brief",
331
+ intro: "Review the startup plan (mission, scope, constraints, acceptance, and notes/risks) plus the routing details before /cook writes canonical workflow state. This gate is approval-only: either Start it as-is or Cancel, discuss changes in the main chat, and rerun /cook.",
332
+ proposalHeading: "Startup plan",
221
333
  proposalBody: buildContextProposalDisplayText(args.proposal),
222
334
  critiqueHeading: "Notes and risks",
223
335
  critiqueBody: buildContextProposalCritiqueText(args.analysis),
@@ -417,6 +529,15 @@ type CompletionVerificationEvidenceSummary = {
417
529
  summary: string;
418
530
  };
419
531
 
532
+ type CompletionStartupPlanSummary = {
533
+ path: string;
534
+ status: string;
535
+ source?: string;
536
+ plannedSurfaces: string[];
537
+ verificationIntent: string[];
538
+ summary: string;
539
+ };
540
+
420
541
  export function buildSystemReminder(args: {
421
542
  missionAnchor?: string;
422
543
  taskType?: string;
@@ -440,11 +561,12 @@ export function buildSystemReminder(args: {
440
561
  implementationSurfacesLine?: string;
441
562
  verificationCommandsLine?: string;
442
563
  evidence: CompletionVerificationEvidenceSummary;
564
+ startupPlan: CompletionStartupPlanSummary;
443
565
  evaluationRoleReminderText?: string;
444
566
  }): string {
445
567
  const lines = [
446
568
  "Completion workflow detected.",
447
- "Canonical truth lives in .agent/state.json, .agent/plan.json, .agent/active-slice.json, .agent/slice-history.jsonl, .agent/stop-check-history.jsonl, and .agent/verification-evidence.json.",
569
+ "Canonical truth lives in .agent/state.json, .agent/startup-plan.json, .agent/plan.json, .agent/active-slice.json, .agent/slice-history.jsonl, .agent/stop-check-history.jsonl, and .agent/verification-evidence.json.",
448
570
  `Mission anchor: ${args.missionAnchor ?? "(unknown)"}`,
449
571
  `Task type: ${args.taskType ?? "(missing)"}`,
450
572
  `Evaluation profile: ${args.evaluationProfile ?? "(missing)"}`,
@@ -461,6 +583,7 @@ export function buildSystemReminder(args: {
461
583
  "Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.",
462
584
  "If canonical state is stale, invalid, ambiguous, or missing, route to completion-regrounder.",
463
585
  "When recovering from compaction, prefer a deterministic restart from canonical files over conversational inference.",
586
+ `Approved startup plan: ${args.startupPlan.path} (${args.startupPlan.status})`,
464
587
  ];
465
588
  if (args.exactActiveContract) {
466
589
  lines.push("Selected/in-progress/committed/done .agent/active-slice.json is the canonical implementation contract.");
@@ -474,6 +597,14 @@ export function buildSystemReminder(args: {
474
597
  else if (args.implementationSurfaces.length > 0) lines.push(`Active implementation surfaces: ${args.implementationSurfaces.join(", ")}`);
475
598
  if (args.verificationCommandsLine) lines.push(args.verificationCommandsLine);
476
599
  else if (args.verificationCommands.length > 0) lines.push(`Active verification commands: ${args.verificationCommands.join(" | ")}`);
600
+ if (args.startupPlan.source) lines.push(`Approved startup plan source: ${args.startupPlan.source}`);
601
+ if (args.startupPlan.plannedSurfaces.length > 0) {
602
+ lines.push(`Approved startup plan surfaces: ${args.startupPlan.plannedSurfaces.join(" | ")}`);
603
+ }
604
+ if (args.startupPlan.verificationIntent.length > 0) {
605
+ lines.push(`Approved startup plan verification intent: ${args.startupPlan.verificationIntent.join(" | ")}`);
606
+ }
607
+ lines.push(`Approved startup plan summary: ${args.startupPlan.summary}`);
477
608
  lines.push(`Verification evidence artifact: ${args.evidence.path} (${args.evidence.status})`);
478
609
  if (args.evidence.subjectType) lines.push(`Verification evidence subject: ${args.evidence.subjectType}`);
479
610
  if (args.evidence.outcome) lines.push(`Verification evidence outcome: ${args.evidence.outcome}`);
@@ -502,6 +633,7 @@ export function buildResumeCapsule(args: {
502
633
  activeSliceMatchesPlan: "yes" | "no" | "unknown";
503
634
  activeSliceContractDrift: string;
504
635
  implementerHandoffSnapshot: "present" | "missing_or_unclear";
636
+ startupPlan: CompletionStartupPlanSummary;
505
637
  evidence: CompletionVerificationEvidenceSummary;
506
638
  activeSlice: {
507
639
  sliceId?: string;
@@ -544,6 +676,14 @@ export function buildResumeCapsule(args: {
544
676
  `implementer_handoff_snapshot: ${args.implementerHandoffSnapshot}`,
545
677
  `history_counts: reviewed=${args.history.reviewed}, audited=${args.history.audited}, accepted=${args.history.accepted}, reopened=${args.history.reopened}, judgments=${args.history.judgments}`,
546
678
  "",
679
+ "startup_plan:",
680
+ `- path: ${args.startupPlan.path}`,
681
+ `- status: ${args.startupPlan.status}`,
682
+ `- source: ${args.startupPlan.source ?? "(missing)"}`,
683
+ `- planned_surfaces: ${args.startupPlan.plannedSurfaces.length > 0 ? args.startupPlan.plannedSurfaces.join(" | ") : "(none)"}`,
684
+ `- verification_intent: ${args.startupPlan.verificationIntent.length > 0 ? args.startupPlan.verificationIntent.join(" | ") : "(none)"}`,
685
+ `- summary: ${args.startupPlan.summary}`,
686
+ "",
547
687
  "verification_evidence:",
548
688
  `- path: ${args.evidence.path}`,
549
689
  `- status: ${args.evidence.status}`,
@@ -591,8 +731,9 @@ export function buildResumeCapsule(args: {
591
731
  "- Treat this block as continuity support derived from canonical .agent state.",
592
732
  "- For selected/in-progress/committed/done slices, .agent/active-slice.json is the canonical implementation contract and the selected plan slice must mirror it exactly.",
593
733
  "- Preserve exact slice_id, goal, contract_ids, acceptance criteria, blocked_on, priority, why_now, implementation surfaces, verification commands, locked notes, must-fix findings, basis_commit, and before-slice counters where still true.",
734
+ "- .agent/startup-plan.json is the approved workflow plan captured at /cook entry. completion-regrounder must treat it as planning input, then reconcile canonical slices against repo truth instead of copying it blindly into plan.json.",
594
735
  "- When populated, .agent/verification-evidence.json is the durable canonical verification record for the selected slice or current HEAD and should be consumed instead of temp-only artifacts or conversational summaries.",
595
- "- After compaction, re-read .agent/state.json, .agent/plan.json, .agent/active-slice.json, .agent/slice-history.jsonl, .agent/stop-check-history.jsonl, and .agent/verification-evidence.json before resuming long-running completion work.",
736
+ "- After compaction, re-read .agent/state.json, .agent/startup-plan.json, .agent/plan.json, .agent/active-slice.json, .agent/slice-history.jsonl, .agent/stop-check-history.jsonl, and .agent/verification-evidence.json before resuming long-running completion work.",
596
737
  "- Invoke completion-regrounder before continuing when requires_reground is true or unknown.",
597
738
  "- Invoke completion-regrounder before continuing when next_mandatory_role or next_mandatory_action is unknown or ambiguous.",
598
739
  "- Invoke completion-regrounder before continuing when active_slice_matches_plan is no, active_slice_contract_drift_fields is not none, or implementer_handoff_snapshot is missing_or_unclear.",
@@ -60,11 +60,11 @@ export type CookHandoffCapsule = {
60
60
  risks: string[];
61
61
  notes: string[];
62
62
  handoff_kind: "implementation_workflow_handoff";
63
- first_slice_goal: string;
63
+ first_slice_goal?: string;
64
64
  first_slice_non_goals: string[];
65
65
  implementation_surfaces: string[];
66
66
  verification_commands: string[];
67
- why_this_slice_first: string;
67
+ why_this_slice_first?: string;
68
68
  task_type?: string;
69
69
  evaluation_profile?: string;
70
70
  why_cook_now?: string;
@@ -1279,7 +1279,7 @@ function parseCookHandoffCapsulesFromText(
1279
1279
  const mission = deps.asString(parsed.mission);
1280
1280
  const firstSliceGoal = deps.asString(parsed.first_slice_goal ?? parsed.firstSliceGoal);
1281
1281
  const whyThisSliceFirst = deps.asString(parsed.why_this_slice_first ?? parsed.whyThisSliceFirst);
1282
- if (!mission || !firstSliceGoal || !whyThisSliceFirst) continue;
1282
+ if (!mission) continue;
1283
1283
  const scope = deps.asStringArray(parsed.scope);
1284
1284
  const constraints = deps.asStringArray(parsed.constraints);
1285
1285
  const nonGoals = deps.asStringArray(parsed.non_goals ?? parsed.nonGoals);
@@ -1325,12 +1325,12 @@ function buildCookHandoffBasisPreview(capsule: CookHandoffCapsule): string {
1325
1325
  ...capsule.constraints,
1326
1326
  ...capsule.non_goals,
1327
1327
  ...capsule.acceptance,
1328
- `first_slice_goal: ${capsule.first_slice_goal}`,
1329
- ...capsule.first_slice_non_goals.map((item) => `first_slice_non_goals: ${item}`),
1330
- ...capsule.implementation_surfaces.map((item) => `implementation_surfaces: ${item}`),
1331
- ...capsule.verification_commands.map((item) => `verification_commands: ${item}`),
1332
- `why_this_slice_first: ${capsule.why_this_slice_first}`,
1333
1328
  ];
1329
+ if (capsule.first_slice_goal) parts.push(`first_slice_goal: ${capsule.first_slice_goal}`);
1330
+ parts.push(...capsule.first_slice_non_goals.map((item) => `first_slice_non_goals: ${item}`));
1331
+ parts.push(...capsule.implementation_surfaces.map((item) => `implementation_surfaces: ${item}`));
1332
+ parts.push(...capsule.verification_commands.map((item) => `verification_commands: ${item}`));
1333
+ if (capsule.why_this_slice_first) parts.push(`why_this_slice_first: ${capsule.why_this_slice_first}`);
1334
1334
  if (capsule.why_cook_now) parts.push(`why_cook_now: ${capsule.why_cook_now}`);
1335
1335
  return parts.join("\n").trim();
1336
1336
  }
@@ -1363,21 +1363,21 @@ function cookHandoffStartabilityFailures(
1363
1363
  else if (!cookHandoffAcceptanceIsRepoChangeOriented(capsule)) {
1364
1364
  failures.push("acceptance is not anchored to concrete repo changes or verification");
1365
1365
  }
1366
- const firstSliceGoal = deps.normalizeMissionAnchorText(capsule.first_slice_goal);
1367
- if (!firstSliceGoal || deps.isWeakMissionAnchor(firstSliceGoal) || COOK_HANDOFF_NEGATIVE_MISSION_REGEX.test(firstSliceGoal)) {
1368
- failures.push("first_slice_goal is not a bounded implementation slice");
1369
- } else if (hasExplicitPlanningOnlyDeliverable([capsule.first_slice_goal]) || hasClearNoImplementationSignal([capsule.first_slice_goal])) {
1370
- failures.push("first_slice_goal is planning-only instead of a repo-change slice");
1366
+ if (capsule.first_slice_goal) {
1367
+ const firstSliceGoal = deps.normalizeMissionAnchorText(capsule.first_slice_goal);
1368
+ if (!firstSliceGoal || deps.isWeakMissionAnchor(firstSliceGoal) || COOK_HANDOFF_NEGATIVE_MISSION_REGEX.test(firstSliceGoal)) {
1369
+ failures.push("first_slice_goal is not a useful sequencing hint");
1370
+ } else if (hasExplicitPlanningOnlyDeliverable([capsule.first_slice_goal]) || hasClearNoImplementationSignal([capsule.first_slice_goal])) {
1371
+ failures.push("first_slice_goal is planning-only instead of a repo-change sequencing hint");
1372
+ }
1371
1373
  }
1372
- if (capsule.implementation_surfaces.length === 0) failures.push("implementation_surfaces is empty");
1373
- if (capsule.verification_commands.length === 0) failures.push("verification_commands is empty");
1374
1374
  return failures;
1375
1375
  }
1376
1376
 
1377
1377
  function buildNonStartableCookHandoffMessage(failures: string[]): string {
1378
1378
  return [
1379
- "/cook failed closed because a fresh explicit primary-agent handoff exists, but it is not concrete enough to start implementation workflow yet.",
1380
- "Tighten the handoff in the main chat so it names a bounded first implementation slice, repo-change-oriented acceptance, implementation_surfaces, and verification_commands, then rerun /cook.",
1379
+ "/cook failed closed because a fresh explicit primary-agent startup plan exists, but it is not concrete enough to seed workflow planning yet.",
1380
+ "Tighten the startup plan in the main chat so it captures a concrete mission, repo-change-oriented acceptance, and truthful verification intent, then rerun /cook.",
1381
1381
  `Blocking details: ${failures.join("; ")}.`,
1382
1382
  ].join(" ");
1383
1383
  }
@@ -1435,11 +1435,11 @@ function buildContextProposalFromCookHandoffCapsule(
1435
1435
  evaluationProfile: capsule.evaluation_profile,
1436
1436
  critique: [
1437
1437
  ...capsule.notes,
1438
- `First slice goal: ${capsule.first_slice_goal}`,
1438
+ ...(capsule.first_slice_goal ? [`First slice goal: ${capsule.first_slice_goal}`] : []),
1439
1439
  ...(capsule.first_slice_non_goals.length > 0 ? [`First slice non-goals: ${capsule.first_slice_non_goals.join(" | ")}`] : []),
1440
1440
  ...(capsule.implementation_surfaces.length > 0 ? [`Implementation surfaces: ${capsule.implementation_surfaces.join(" | ")}`] : []),
1441
1441
  ...(capsule.verification_commands.length > 0 ? [`Verification commands: ${capsule.verification_commands.join(" | ")}`] : []),
1442
- `Why this slice first: ${capsule.why_this_slice_first}`,
1442
+ ...(capsule.why_this_slice_first ? [`Why this slice first: ${capsule.why_this_slice_first}`] : []),
1443
1443
  ...(capsule.why_cook_now ? [`Primary-agent /cook handoff rationale: ${capsule.why_cook_now}`] : []),
1444
1444
  ],
1445
1445
  risks: capsule.risks,
@@ -1455,11 +1455,11 @@ function buildContextProposalFromCookHandoffCapsule(
1455
1455
  ...capsule.scope,
1456
1456
  ...constraints,
1457
1457
  ...capsule.acceptance,
1458
- capsule.first_slice_goal,
1458
+ ...(capsule.first_slice_goal ? [capsule.first_slice_goal] : []),
1459
1459
  ...capsule.first_slice_non_goals,
1460
1460
  ...capsule.implementation_surfaces,
1461
1461
  ...capsule.verification_commands,
1462
- capsule.why_this_slice_first,
1462
+ ...(capsule.why_this_slice_first ? [capsule.why_this_slice_first] : []),
1463
1463
  ],
1464
1464
  ),
1465
1465
  goalText,