@linimin/pi-letscook 0.1.36 → 0.1.39

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.
@@ -463,6 +463,20 @@ type ExistingWorkflowDecision =
463
463
  | { action: "continue"; currentMissionAnchor: string }
464
464
  | { action: "refocus"; currentMissionAnchor: string; missionAnchor: string };
465
465
 
466
+ type ActiveWorkflowProposalAssessment = {
467
+ action: "continue" | "refocus" | "unclear";
468
+ currentMissionAnchor: string;
469
+ proposal?: ContextProposal;
470
+ reason: "matching_mission" | "clear_refocus" | "missing_proposal" | "ambiguous_discussion";
471
+ };
472
+
473
+ type ExistingWorkflowChooserOptions = {
474
+ intro?: string;
475
+ proposedMissionLabel?: string;
476
+ refocusChoiceLabel?: string;
477
+ comparison?: "semantic" | "strict";
478
+ };
479
+
466
480
  function completionTestWorkflowActionOverride(): "continue" | "refocus" | "cancel" | undefined {
467
481
  const raw = process.env.PI_COMPLETION_EXISTING_WORKFLOW_ACTION?.trim().toLowerCase();
468
482
  return raw === "continue" || raw === "refocus" || raw === "cancel" ? raw : undefined;
@@ -494,6 +508,10 @@ function completionTestContextProposalSnapshotPath(): string | undefined {
494
508
  return asString(process.env.PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH);
495
509
  }
496
510
 
511
+ function completionTestActiveWorkflowRoutingSnapshotPath(): string | undefined {
512
+ return asString(process.env.PI_COMPLETION_TEST_ACTIVE_WORKFLOW_ROUTING_PATH);
513
+ }
514
+
497
515
  function completionTestDriverPromptPath(): string | undefined {
498
516
  return asString(process.env.PI_COMPLETION_TEST_DRIVER_PROMPT_PATH);
499
517
  }
@@ -521,11 +539,19 @@ function maybeWriteTestSnapshot(targetPath: string | undefined, content: string)
521
539
  }
522
540
 
523
541
  const COOK_MAIN_CHAT_RERUN_GUIDANCE = "Discuss changes in the main chat and rerun /cook.";
542
+ const COOK_INLINE_ARG_REJECTION_MESSAGE =
543
+ "Inline /cook arguments are no longer supported. Clarify the mission in the main chat and rerun bare /cook.";
544
+ const COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL =
545
+ "Bare /cook failed closed because recent discussion did not contain a clear structured Mission/Scope/Constraints/Acceptance proposal. Add that structure in the main chat and rerun bare /cook.";
524
546
 
525
547
  function buildCookCancellationMessage(prefix: string): string {
526
548
  return `${prefix}. ${COOK_MAIN_CHAT_RERUN_GUIDANCE}`;
527
549
  }
528
550
 
551
+ function buildCookStructuredDiscussionFailureMessage(prefix?: string): string {
552
+ return prefix ? `${prefix} ${COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL}` : COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL;
553
+ }
554
+
529
555
  function shouldDisableContextProposalAnalyst(): boolean {
530
556
  return process.env.PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST === "1";
531
557
  }
@@ -729,6 +755,88 @@ function matchContextProposalRoutingHint(
729
755
  return label === "tasktype" ? { field: "taskType", value } : { field: "evaluationProfile", value };
730
756
  }
731
757
 
758
+ const CONTEXT_PROPOSAL_GENERIC_PLANNING_MISSION_REGEX =
759
+ /(?:\b(?:start(?:ing)?|begin|continue|continu(?:e|ing)|resume|implement(?:ing)?|execute|execut(?:e|ing)|carry out|work on|ship|build(?:ing)?)\b.*\b(?:this|that|the|current|latest)\s+(?:plan|proposal|spec(?:ification)?|design(?: doc(?:ument)?)?|migration plan)\b|(?:開始|著手|繼續|继续|恢復|恢复)?(?:實作|实现|執行|执行|落地|完成)(?:這個|这个|此|該|该)?(?:方案|計畫|计划|提案|規劃|规划|設計|设计))/iu;
760
+ const CONTEXT_PROPOSAL_PLANNING_ONLY_DELIVERABLE_REGEX =
761
+ /(?:\b(?:write|draft|prepare|create|produce|share|deliver|document|review)\b.*\b(?:plan|spec(?:ification)?|design(?: doc(?:ument)?)?|migration plan|proposal)\b|(?:撰寫|撰写|編寫|编写|起草|準備|准备|產出|产出|整理|分享|交付|審查|审查).*(?:計畫|计划|規格|规格|設計文件|设计文档|提案|方案))/iu;
762
+ const CONTEXT_PROPOSAL_NO_CODE_DOCS_ONLY_REGEX =
763
+ /(?:\b(?:docs? only|documentation only|no code(?: changes?)?|without code(?: changes?)?|do not implement|don't implement|planning only|proposal only|spec only|design[- ]doc only|no runtime changes?)\b|(?:只改文件|僅文件|仅文件|不改(?:動)?代碼|不改代码|不要實作|不要实现|只規劃|只规划|僅規劃|仅规划|不改(?:動)?執行|不改运行))/iu;
764
+ const CONTEXT_PROPOSAL_SUPPORT_ONLY_DOCS_REGEX =
765
+ /(?:^(?:update|edit|document|refresh|write|add)\s+(?:the\s+)?(?:readme|docs?|documentation)\b|^(?:更新|補充|补充|撰寫|撰写|新增)\s*(?:README|文件|文檔|文档))/iu;
766
+ const CONTEXT_PROPOSAL_IMPLEMENTATION_SOURCE_REGEX =
767
+ /(?:\b(?:normalize|fix|update|add|remove|restore|refactor|ship|support|wire|route|rewrite|replace|preserve|filter|separate|refresh|reroute|suppress|align|convert|reconcile|repair|correct|implement|build|land|block|allow|keep)\b|(?:修正|修復|修复|更新|新增|移除|恢復|恢复|重構|重构|調整|调整|正規化|规范化|規範化|过滤|過濾|分離|分离|刷新|替換|替换|抑制|對齊|对齐|實作|实现|落地|修補|修补|阻止|允許|允许|轉換|转换|保留|保持))/iu;
768
+
769
+ function contextProposalBodyTexts(proposal: Pick<ContextProposal, "scope" | "constraints" | "acceptance">): string[] {
770
+ return [...proposal.scope, ...proposal.constraints, ...proposal.acceptance];
771
+ }
772
+
773
+ function isGenericPlanningMissionAnchor(text: string): boolean {
774
+ const normalized = normalizeMissionAnchorText(text);
775
+ if (!normalized) return false;
776
+ return CONTEXT_PROPOSAL_GENERIC_PLANNING_MISSION_REGEX.test(normalized);
777
+ }
778
+
779
+ function hasExplicitPlanningOnlyDeliverable(texts: string[]): boolean {
780
+ return texts.some((text) => CONTEXT_PROPOSAL_PLANNING_ONLY_DELIVERABLE_REGEX.test(normalizeProposalLine(text)));
781
+ }
782
+
783
+ function hasClearNoCodeOrDocsOnlySignal(texts: string[]): boolean {
784
+ return texts.some((text) => CONTEXT_PROPOSAL_NO_CODE_DOCS_ONLY_REGEX.test(normalizeProposalLine(text)));
785
+ }
786
+
787
+ function isSupportOnlyDocumentationItem(text: string): boolean {
788
+ return CONTEXT_PROPOSAL_SUPPORT_ONLY_DOCS_REGEX.test(normalizeProposalLine(text));
789
+ }
790
+
791
+ function isImplementationMissionSourceCandidate(text: string): boolean {
792
+ const normalized = normalizeProposalLine(text);
793
+ if (!normalized) return false;
794
+ if (hasExplicitPlanningOnlyDeliverable([normalized])) return false;
795
+ if (hasClearNoCodeOrDocsOnlySignal([normalized])) return false;
796
+ if (isSupportOnlyDocumentationItem(normalized)) return false;
797
+ return CONTEXT_PROPOSAL_IMPLEMENTATION_SOURCE_REGEX.test(normalized);
798
+ }
799
+
800
+ function hasSupportOnlyDocumentationDeliverable(proposal: Pick<ContextProposal, "scope" | "acceptance">): boolean {
801
+ const deliverableTexts = [...proposal.scope, ...proposal.acceptance].map((item) => normalizeProposalLine(item)).filter(Boolean);
802
+ if (deliverableTexts.length === 0) return false;
803
+ return deliverableTexts.every((item) => isSupportOnlyDocumentationItem(item));
804
+ }
805
+
806
+ function pickImplementationMissionSource(proposal: Pick<ContextProposal, "scope" | "constraints" | "acceptance">): string | undefined {
807
+ for (const item of proposal.scope) {
808
+ if (isImplementationMissionSourceCandidate(item)) return item;
809
+ }
810
+ for (const item of proposal.acceptance) {
811
+ if (isImplementationMissionSourceCandidate(item)) return item;
812
+ }
813
+ return undefined;
814
+ }
815
+
816
+ function finalizeContextProposal(proposal: ContextProposal, projectName: string): ContextProposal | undefined {
817
+ if (!isGenericPlanningMissionAnchor(proposal.mission)) return proposal;
818
+ const bodyTexts = contextProposalBodyTexts(proposal);
819
+ if (hasExplicitPlanningOnlyDeliverable(bodyTexts) || hasClearNoCodeOrDocsOnlySignal(bodyTexts) || hasSupportOnlyDocumentationDeliverable(proposal)) {
820
+ return proposal;
821
+ }
822
+ const missionSource = pickImplementationMissionSource(proposal);
823
+ if (!missionSource) return undefined;
824
+ const nextMission = assessMissionAnchor(missionSource, projectName).derived;
825
+ const normalizedNextMission = normalizeMissionAnchorText(nextMission);
826
+ if (!normalizedNextMission || isWeakMissionAnchor(normalizedNextMission)) return undefined;
827
+ if (missionAnchorsStrictlyEquivalent(nextMission, proposal.mission)) return proposal;
828
+ return {
829
+ ...proposal,
830
+ mission: nextMission,
831
+ goalText: buildContextProposalGoalText({
832
+ mission: nextMission,
833
+ scope: proposal.scope,
834
+ constraints: proposal.constraints,
835
+ acceptance: proposal.acceptance,
836
+ }),
837
+ };
838
+ }
839
+
732
840
  const MISSION_SCOPE_FILTER_STOPWORDS = new Set([
733
841
  "a",
734
842
  "an",
@@ -785,6 +893,108 @@ function isSessionScopeItemMissionRelevant(item: string, mission: string): boole
785
893
  return overlap.some((token) => token.length >= 6 || /[\p{Script=Han}]/u.test(token));
786
894
  }
787
895
 
896
+ function missionAnchorSemanticTokens(text: string): string[] {
897
+ return [...new Set(missionScopeFilterTokens(normalizeMissionAnchorText(text).toLowerCase()))];
898
+ }
899
+
900
+ function missionAnchorOrderedTokenOverlapRatio(leftTokens: string[], rightTokens: string[]): number {
901
+ if (leftTokens.length === 0 || rightTokens.length === 0) return 0;
902
+ const dp = new Array(rightTokens.length + 1).fill(0);
903
+ for (const leftToken of leftTokens) {
904
+ let previous = 0;
905
+ for (let index = 0; index < rightTokens.length; index += 1) {
906
+ const nextPrevious = dp[index + 1];
907
+ if (leftToken === rightTokens[index]) {
908
+ dp[index + 1] = previous + 1;
909
+ } else {
910
+ dp[index + 1] = Math.max(dp[index + 1], dp[index]);
911
+ }
912
+ previous = nextPrevious;
913
+ }
914
+ }
915
+ return dp[rightTokens.length] / Math.max(leftTokens.length, rightTokens.length);
916
+ }
917
+
918
+ function missionAnchorBigramOverlapRatio(leftTokens: string[], rightTokens: string[]): number {
919
+ if (leftTokens.length < 2 || rightTokens.length < 2) return 0;
920
+ const leftBigrams = new Set(leftTokens.slice(0, -1).map((token, index) => `${token} ${leftTokens[index + 1]}`));
921
+ const rightBigrams = new Set(rightTokens.slice(0, -1).map((token, index) => `${token} ${rightTokens[index + 1]}`));
922
+ if (leftBigrams.size === 0 || rightBigrams.size === 0) return 0;
923
+ let overlap = 0;
924
+ for (const bigram of leftBigrams) {
925
+ if (rightBigrams.has(bigram)) overlap += 1;
926
+ }
927
+ return overlap / Math.max(leftBigrams.size, rightBigrams.size);
928
+ }
929
+
930
+ function missionAnchorsStrictlyEquivalent(left: string, right: string): boolean {
931
+ return normalizeMissionAnchorText(left).toLowerCase() === normalizeMissionAnchorText(right).toLowerCase();
932
+ }
933
+
934
+ const MISSION_NEGATION_CUE_REGEX = /(?:^|[^\p{L}\p{N}_])(?:no|not|without|never|cannot|don['’]?t)(?=$|[^\p{L}\p{N}_])/u;
935
+
936
+ function missionAnchorHasNegationCue(text: string): boolean {
937
+ return MISSION_NEGATION_CUE_REGEX.test(text);
938
+ }
939
+
940
+ function missionAnchorsLikelyEquivalent(left: string, right: string): boolean {
941
+ const normalizedLeft = normalizeMissionAnchorText(left).toLowerCase();
942
+ const normalizedRight = normalizeMissionAnchorText(right).toLowerCase();
943
+ if (!normalizedLeft || !normalizedRight) return false;
944
+ const leftHasNegationCue = missionAnchorHasNegationCue(normalizedLeft);
945
+ const rightHasNegationCue = missionAnchorHasNegationCue(normalizedRight);
946
+ if (leftHasNegationCue !== rightHasNegationCue) return false;
947
+ if (normalizedLeft === normalizedRight) return true;
948
+ if (!leftHasNegationCue && (normalizedLeft.includes(normalizedRight) || normalizedRight.includes(normalizedLeft))) return true;
949
+ const leftTokens = missionAnchorSemanticTokens(normalizedLeft);
950
+ const rightTokens = missionAnchorSemanticTokens(normalizedRight);
951
+ if (leftTokens.length === 0 || rightTokens.length === 0) return false;
952
+ const rightSet = new Set(rightTokens);
953
+ const overlap = leftTokens.filter((token) => rightSet.has(token));
954
+ if (overlap.length < 3) return false;
955
+ const maxLen = Math.max(leftTokens.length, rightTokens.length);
956
+ if (overlap.length / maxLen < 0.75) return false;
957
+ if (missionAnchorOrderedTokenOverlapRatio(leftTokens, rightTokens) < 0.75) return false;
958
+ if (Math.min(leftTokens.length, rightTokens.length) < 4) return true;
959
+ return missionAnchorBigramOverlapRatio(leftTokens, rightTokens) >= 0.5;
960
+ }
961
+
962
+ function shouldTreatBareActiveWorkflowProposalAsClearRefocus(proposal: ContextProposal): boolean {
963
+ if (proposal.source === "session") {
964
+ return proposal.scope.length > 0 && proposal.constraints.length > 0 && proposal.acceptance.length > 0;
965
+ }
966
+ return (
967
+ proposal.scope.length > 0 &&
968
+ proposal.constraints.length > 0 &&
969
+ proposal.acceptance.length > 0 &&
970
+ proposal.analysis.possibleNoise.length === 0
971
+ );
972
+ }
973
+
974
+ function maybeWriteActiveWorkflowRoutingSnapshot(assessment: ActiveWorkflowProposalAssessment): void {
975
+ const snapshotPath = completionTestActiveWorkflowRoutingSnapshotPath();
976
+ if (!snapshotPath) return;
977
+ maybeWriteTestSnapshot(
978
+ snapshotPath,
979
+ `${JSON.stringify(
980
+ {
981
+ mode: "bare",
982
+ action: assessment.action,
983
+ reason: assessment.reason,
984
+ currentMissionAnchor: assessment.currentMissionAnchor,
985
+ proposedMissionAnchor: assessment.proposal?.mission ?? null,
986
+ proposalSource: assessment.proposal?.source ?? null,
987
+ possibleNoise: assessment.proposal?.analysis.possibleNoise ?? [],
988
+ scope: assessment.proposal?.scope ?? [],
989
+ constraints: assessment.proposal?.constraints ?? [],
990
+ acceptance: assessment.proposal?.acceptance ?? [],
991
+ },
992
+ null,
993
+ 2,
994
+ )}\n`,
995
+ );
996
+ }
997
+
788
998
  const CONTEXT_PROPOSAL_ANALYST_SYSTEM_PROMPT = [
789
999
  "You analyze recent /cook startup discussion and return a strict JSON object.",
790
1000
  "Do not emit markdown, code fences, or commentary.",
@@ -797,7 +1007,6 @@ const CONTEXT_PROPOSAL_ANALYST_SYSTEM_PROMPT = [
797
1007
  "risks must contain concrete failure modes or regressions that the later workflow should keep in view.",
798
1008
  "task_type and evaluation_profile should be candidate routing hints only; reuse the existing completion vocabulary when it clearly fits instead of inventing new schema names.",
799
1009
  "possible_noise should list discussion points that look stale, weakly related, or unsafe to promote into scope.",
800
- "When an explicit goal is provided, keep the mission anchored to that goal instead of replacing it with a broader or different mission.",
801
1010
  "When discussion is insufficient, prefer empty arrays and a low confidence value over invention.",
802
1011
  ].join(" ");
803
1012
 
@@ -852,11 +1061,7 @@ function extractJsonObjectFromText(text: string): string | undefined {
852
1061
  return unfenced.slice(start, end + 1);
853
1062
  }
854
1063
 
855
- function parseContextProposalAnalystOutput(
856
- raw: string,
857
- projectName: string,
858
- explicitGoal?: string,
859
- ): ContextProposal | undefined {
1064
+ function parseContextProposalAnalystOutput(raw: string, projectName: string): ContextProposal | undefined {
860
1065
  const jsonText = extractJsonObjectFromText(raw);
861
1066
  if (!jsonText) return undefined;
862
1067
  let parsed: unknown;
@@ -866,8 +1071,7 @@ function parseContextProposalAnalystOutput(
866
1071
  return undefined;
867
1072
  }
868
1073
  if (!isRecord(parsed)) return undefined;
869
- const explicit = explicitGoal ? parseContextProposal(explicitGoal, projectName) : undefined;
870
- const missionSource = explicit?.mission ?? explicitGoal ?? asString(parsed.mission);
1074
+ const missionSource = asString(parsed.mission);
871
1075
  if (!missionSource) return undefined;
872
1076
  const assessment = assessMissionAnchor(missionSource, projectName);
873
1077
  const normalizedMission = normalizeMissionAnchorText(missionSource);
@@ -876,31 +1080,28 @@ function parseContextProposalAnalystOutput(
876
1080
  const scope = uniqueProposalItems(asStringArray(parsed.scope));
877
1081
  const constraints = uniqueProposalItems(asStringArray(parsed.constraints));
878
1082
  const acceptance = uniqueProposalItems(asStringArray(parsed.acceptance));
879
- const analysis = mergeContextProposalAnalysis(
880
- [
881
- explicit?.analysis,
882
- buildContextProposalAnalysis({
883
- taskType: parsed.task_type ?? parsed.taskType,
884
- evaluationProfile: parsed.evaluation_profile ?? parsed.evaluationProfile,
885
- critique: asStringArray(parsed.critique),
886
- risks: asStringArray(parsed.risks ?? parsed.risk),
887
- possibleNoise: asStringArray(parsed.possible_noise ?? parsed.possibleNoise),
888
- hintTexts: [raw, mission, ...scope, ...constraints, ...acceptance],
889
- }),
890
- ],
891
- [explicitGoal ?? "", raw, mission, ...scope, ...constraints, ...acceptance],
892
- );
1083
+ const analysis = buildContextProposalAnalysis({
1084
+ taskType: parsed.task_type ?? parsed.taskType,
1085
+ evaluationProfile: parsed.evaluation_profile ?? parsed.evaluationProfile,
1086
+ critique: asStringArray(parsed.critique),
1087
+ risks: asStringArray(parsed.risks ?? parsed.risk),
1088
+ possibleNoise: asStringArray(parsed.possible_noise ?? parsed.possibleNoise),
1089
+ hintTexts: [raw, mission, ...scope, ...constraints, ...acceptance],
1090
+ });
893
1091
  const goalText = buildContextProposalGoalText({ mission, scope, constraints, acceptance });
894
- return {
895
- mission,
896
- scope,
897
- constraints,
898
- acceptance,
899
- analysis,
900
- goalText,
901
- basisPreview: raw.replace(/\s+/g, " ").trim(),
902
- source: "analyst",
903
- };
1092
+ return finalizeContextProposal(
1093
+ {
1094
+ mission,
1095
+ scope,
1096
+ constraints,
1097
+ acceptance,
1098
+ analysis,
1099
+ goalText,
1100
+ basisPreview: raw.replace(/\s+/g, " ").trim(),
1101
+ source: "analyst",
1102
+ },
1103
+ projectName,
1104
+ );
904
1105
  }
905
1106
 
906
1107
  function contextProposalAnalystModelArg(model: unknown): string | undefined {
@@ -910,17 +1111,9 @@ function contextProposalAnalystModelArg(model: unknown): string | undefined {
910
1111
  return provider && id ? `${provider}/${id}` : undefined;
911
1112
  }
912
1113
 
913
- function buildContextProposalAnalystPrompt(projectName: string, recentEntries: RecentDiscussionEntry[], explicitGoal?: string): string {
1114
+ function buildContextProposalAnalystPrompt(projectName: string, recentEntries: RecentDiscussionEntry[]): string {
914
1115
  const discussion = serializeRecentDiscussionEntries(recentEntries);
915
- return [
916
- `Project: ${projectName}`,
917
- explicitGoal
918
- ? `Explicit goal (keep this mission anchor):\n${explicitGoal}`
919
- : "Explicit goal: none provided; infer the current mission from the discussion.",
920
- "",
921
- "Recent discussion:",
922
- discussion,
923
- ].join("\n");
1116
+ return [`Project: ${projectName}`, "Infer the current mission from the discussion.", "", "Recent discussion:", discussion].join("\n");
924
1117
  }
925
1118
 
926
1119
  function contextProposalAnalystProgressLines(activity: LiveRoleActivity): string[] {
@@ -949,14 +1142,13 @@ async function runContextProposalAnalystSubprocess(
949
1142
  ctx: { cwd: string; hasUI: boolean; ui: any; model?: any },
950
1143
  projectName: string,
951
1144
  recentEntries: RecentDiscussionEntry[],
952
- explicitGoal?: string,
953
1145
  ): Promise<string | undefined> {
954
1146
  const modelArg = contextProposalAnalystModelArg(ctx.model);
955
1147
  if (!modelArg) return undefined;
956
1148
  const cwd = getCtxCwd(ctx);
957
1149
  const runCwd = findCompletionRoot(cwd) ?? findRepoRoot(cwd) ?? cwd;
958
1150
  const rootKey = completionRootKey(undefined, cwd);
959
- const prompt = buildContextProposalAnalystPrompt(projectName, recentEntries, explicitGoal);
1151
+ const prompt = buildContextProposalAnalystPrompt(projectName, recentEntries);
960
1152
  const systemPromptTemp = await writeTempFile("pi-cook-proposal-analyst-", CONTEXT_PROPOSAL_ANALYST_SYSTEM_PROMPT);
961
1153
  const analystRole = "cook-proposal-analyst";
962
1154
  const args: string[] = ["--mode", "json", "-p", "--no-session", "--append-system-prompt", systemPromptTemp.filePath, "--model", modelArg, prompt];
@@ -1058,18 +1250,17 @@ async function analyzeContextProposalWithAgent(
1058
1250
  ctx: { cwd: string; hasUI: boolean; ui: any; model?: any; modelRegistry?: any },
1059
1251
  projectName: string,
1060
1252
  recentEntries: RecentDiscussionEntry[],
1061
- explicitGoal?: string,
1062
1253
  ): Promise<ContextProposal | undefined> {
1063
1254
  if (shouldDisableContextProposalAnalyst()) return undefined;
1064
1255
  const testOutput = completionTestContextProposalAnalystOutput();
1065
1256
  if (testOutput) {
1066
- return parseContextProposalAnalystOutput(testOutput, projectName, explicitGoal);
1257
+ return parseContextProposalAnalystOutput(testOutput, projectName);
1067
1258
  }
1068
1259
  if (recentEntries.length === 0) return undefined;
1069
1260
  try {
1070
- const raw = await runContextProposalAnalystSubprocess(ctx, projectName, recentEntries, explicitGoal);
1261
+ const raw = await runContextProposalAnalystSubprocess(ctx, projectName, recentEntries);
1071
1262
  if (!raw) return undefined;
1072
- return parseContextProposalAnalystOutput(raw, projectName, explicitGoal);
1263
+ return parseContextProposalAnalystOutput(raw, projectName);
1073
1264
  } catch (error) {
1074
1265
  console.warn("[completion] context proposal analyst failed", error);
1075
1266
  return undefined;
@@ -1431,57 +1622,111 @@ function parseContextProposal(text: string, projectName: string): ContextProposa
1431
1622
  hintTexts: [cleaned, mission, ...scope, ...constraints, ...acceptance, ...critique, ...risks],
1432
1623
  });
1433
1624
  const goalText = buildContextProposalGoalText({ mission, scope, constraints, acceptance });
1434
- return {
1435
- mission,
1436
- scope,
1437
- constraints,
1438
- acceptance,
1439
- analysis,
1440
- goalText,
1441
- basisPreview,
1442
- source: "session",
1443
- };
1625
+ return finalizeContextProposal(
1626
+ {
1627
+ mission,
1628
+ scope,
1629
+ constraints,
1630
+ acceptance,
1631
+ analysis,
1632
+ goalText,
1633
+ basisPreview,
1634
+ source: "session",
1635
+ },
1636
+ projectName,
1637
+ );
1638
+ }
1639
+
1640
+ function hasStructuredContextProposalSignal(text: string): boolean {
1641
+ const cleaned = stripCodeBlocks(text).replace(/\r/g, "").trim();
1642
+ if (!cleaned) return false;
1643
+ return /(^|\n)\s*(mission|goal|objective|summary|scope|plan|steps|implementation|constraints?|guardrails|non-goals|acceptance|acceptance criteria|deliverables|verification|critique|concerns?|warnings?|notes?|risks?|hazards?|task[\s_-]*type|evaluation[\s_-]*profile)\s*(?:[::]\s*|$)/imu.test(
1644
+ cleaned,
1645
+ );
1646
+ }
1647
+
1648
+ function parseStrictStructuredSessionProposal(text: string, projectName: string): ContextProposal | undefined {
1649
+ const cleaned = stripCodeBlocks(text).replace(/\r/g, "").trim();
1650
+ if (!cleaned) return undefined;
1651
+ const lines = cleaned
1652
+ .split("\n")
1653
+ .map((line) => line.trim())
1654
+ .filter((line) => line.length > 0);
1655
+ if (lines.length === 0) return undefined;
1656
+
1657
+ let section: ContextProposalSection | undefined;
1658
+ const sectionsPresent = new Set<ContextProposalSection>();
1659
+ const missionCandidates: string[] = [];
1660
+
1661
+ for (const rawLine of lines) {
1662
+ const inlineSection = matchInlineProposalSection(rawLine);
1663
+ if (inlineSection) {
1664
+ section = inlineSection.section;
1665
+ sectionsPresent.add(section);
1666
+ if (section === "mission") missionCandidates.push(inlineSection.content);
1667
+ continue;
1668
+ }
1669
+ const headerSection = detectProposalSection(rawLine);
1670
+ if (headerSection) {
1671
+ section = headerSection;
1672
+ sectionsPresent.add(section);
1673
+ continue;
1674
+ }
1675
+ const normalized = bulletText(rawLine) ?? normalizeProposalLine(rawLine);
1676
+ if (normalized && section === "mission") missionCandidates.push(normalized);
1677
+ }
1678
+
1679
+ const requiredSections: ContextProposalSection[] = ["mission", "scope", "constraints", "acceptance"];
1680
+ if (requiredSections.some((candidate) => !sectionsPresent.has(candidate))) return undefined;
1681
+
1682
+ const distinctMissionAnchors = Array.from(
1683
+ new Set(
1684
+ missionCandidates
1685
+ .map((candidate) => normalizeMissionAnchorText(assessMissionAnchor(candidate, projectName).derived))
1686
+ .filter((candidate): candidate is string => Boolean(candidate)),
1687
+ ),
1688
+ );
1689
+ if (distinctMissionAnchors.length !== 1) return undefined;
1690
+
1691
+ const proposal = parseContextProposal(cleaned, projectName);
1692
+ if (!proposal) return undefined;
1693
+ if (
1694
+ normalizeMissionAnchorText(proposal.mission) !== distinctMissionAnchors[0] &&
1695
+ !isGenericPlanningMissionAnchor(distinctMissionAnchors[0])
1696
+ ) {
1697
+ return undefined;
1698
+ }
1699
+ if (proposal.scope.length === 0 || proposal.constraints.length === 0 || proposal.acceptance.length === 0) return undefined;
1700
+ return { ...proposal, source: "session" };
1701
+ }
1702
+
1703
+ function extractContextProposalFromStructuredSession(
1704
+ recentEntries: RecentDiscussionEntry[],
1705
+ projectName: string,
1706
+ ): ContextProposal | undefined {
1707
+ const structuredTexts = recentEntries
1708
+ .slice()
1709
+ .reverse()
1710
+ .map((entry) => entry.text.trim())
1711
+ .filter((text) => hasStructuredContextProposalSignal(text));
1712
+ if (structuredTexts.length === 0) return undefined;
1713
+ return parseStrictStructuredSessionProposal(structuredTexts.join("\n\n"), projectName);
1444
1714
  }
1445
1715
 
1446
1716
  async function extractContextProposalFromSession(
1447
1717
  ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
1448
1718
  projectName: string,
1449
- explicitGoal?: string,
1450
1719
  ): Promise<ContextProposal | undefined> {
1451
1720
  const recentEntries = collectRecentDiscussionEntries(ctx);
1452
- return await analyzeContextProposalWithAgent(ctx, projectName, recentEntries, explicitGoal);
1721
+ return (await analyzeContextProposalWithAgent(ctx, projectName, recentEntries)) ??
1722
+ extractContextProposalFromStructuredSession(recentEntries, projectName);
1453
1723
  }
1454
1724
 
1455
- async function buildGoalAnchoredContextProposal(
1725
+ async function deriveCookContextProposal(
1456
1726
  ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
1457
- goal: string,
1458
1727
  projectName: string,
1459
- ): Promise<ContextProposal> {
1460
- const explicit = parseContextProposal(goal, projectName);
1461
- const sessionProposal = await extractContextProposalFromSession(ctx, projectName, goal);
1462
- const missionSource = explicit?.mission ?? goal;
1463
- const assessment = assessMissionAnchor(missionSource, projectName);
1464
- const mission = assessment.derived;
1465
- const explicitScope = explicit?.scope ?? [];
1466
- const sessionScope = (sessionProposal?.scope ?? []).filter((item) => isSessionScopeItemMissionRelevant(item, mission));
1467
- const scope = uniqueProposalItems([...explicitScope, ...sessionScope]);
1468
- const constraints = uniqueProposalItems([...(explicit?.constraints ?? []), ...(sessionProposal?.constraints ?? [])]);
1469
- const acceptance = uniqueProposalItems([...(explicit?.acceptance ?? []), ...(sessionProposal?.acceptance ?? [])]);
1470
- const analysis = mergeContextProposalAnalysis(
1471
- [explicit?.analysis, sessionProposal?.analysis],
1472
- [goal, mission, ...(sessionProposal?.analysis.possibleNoise ?? []), ...scope, ...constraints, ...acceptance],
1473
- );
1474
- const goalText = buildContextProposalGoalText({ mission, scope, constraints, acceptance });
1475
- return {
1476
- mission,
1477
- scope,
1478
- constraints,
1479
- acceptance,
1480
- analysis,
1481
- goalText,
1482
- basisPreview: sessionProposal?.basisPreview ?? explicit?.basisPreview ?? goal,
1483
- source: sessionProposal?.source ?? explicit?.source ?? "session",
1484
- };
1728
+ ): Promise<ContextProposal | undefined> {
1729
+ return await extractContextProposalFromSession(ctx, projectName);
1485
1730
  }
1486
1731
 
1487
1732
  async function confirmContextProposal(
@@ -1754,32 +1999,103 @@ function buildEvaluationRoleReminderText(snapshot: CompletionStateSnapshot, role
1754
1999
  return buildEvaluationRoleContextLines(snapshot, role).join(" ");
1755
2000
  }
1756
2001
 
1757
- async function confirmExistingWorkflowGoal(
2002
+ async function assessActiveWorkflowProposalRouting(
2003
+ ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
2004
+ snapshot: CompletionStateSnapshot,
2005
+ ): Promise<ActiveWorkflowProposalAssessment> {
2006
+ const currentMission = currentMissionAnchor(snapshot);
2007
+ const projectName = path.basename(snapshot.files.root);
2008
+ const proposal = await deriveCookContextProposal(ctx, projectName);
2009
+ if (!proposal) {
2010
+ const assessment: ActiveWorkflowProposalAssessment = {
2011
+ action: "unclear",
2012
+ currentMissionAnchor: currentMission,
2013
+ reason: "missing_proposal",
2014
+ };
2015
+ maybeWriteActiveWorkflowRoutingSnapshot(assessment);
2016
+ return assessment;
2017
+ }
2018
+ if (missionAnchorsLikelyEquivalent(currentMission, proposal.mission)) {
2019
+ const assessment: ActiveWorkflowProposalAssessment = {
2020
+ action: "continue",
2021
+ currentMissionAnchor: currentMission,
2022
+ proposal,
2023
+ reason: "matching_mission",
2024
+ };
2025
+ maybeWriteActiveWorkflowRoutingSnapshot(assessment);
2026
+ return assessment;
2027
+ }
2028
+ if (shouldTreatBareActiveWorkflowProposalAsClearRefocus(proposal)) {
2029
+ const assessment: ActiveWorkflowProposalAssessment = {
2030
+ action: "refocus",
2031
+ currentMissionAnchor: currentMission,
2032
+ proposal,
2033
+ reason: "clear_refocus",
2034
+ };
2035
+ maybeWriteActiveWorkflowRoutingSnapshot(assessment);
2036
+ return assessment;
2037
+ }
2038
+ const assessment: ActiveWorkflowProposalAssessment = {
2039
+ action: "unclear",
2040
+ currentMissionAnchor: currentMission,
2041
+ proposal,
2042
+ reason: "ambiguous_discussion",
2043
+ };
2044
+ maybeWriteActiveWorkflowRoutingSnapshot(assessment);
2045
+ return assessment;
2046
+ }
2047
+
2048
+ async function resumeActiveWorkflowFromCanonicalState(
2049
+ pi: any,
2050
+ ctx: { cwd: string; hasUI: boolean; ui: any },
2051
+ snapshot: CompletionStateSnapshot,
2052
+ ): Promise<void> {
2053
+ const mission = currentMissionAnchor(snapshot);
2054
+ pi.setSessionName(`completion: ${mission.slice(0, 60)}`);
2055
+ const resumePrompt = completionResumePrompt(
2056
+ currentTaskType(snapshot) ?? "(missing)",
2057
+ currentEvaluationProfile(snapshot) ?? "(missing)",
2058
+ );
2059
+ const rootKey = completionRootKey(snapshot, getCtxCwd(ctx));
2060
+ const fingerprint = completionContinuationFingerprint(snapshot) ?? JSON.stringify({
2061
+ kind: "resume",
2062
+ mission_anchor: mission,
2063
+ current_phase: asString(snapshot.state?.current_phase) ?? null,
2064
+ next_mandatory_role: asString(snapshot.state?.next_mandatory_role) ?? null,
2065
+ });
2066
+ const resumeKind = shouldTestAutoContinueOnSessionStart() && completionTestAutoContinuePromptPath() ? "auto-resume" : "resume";
2067
+ await queueCompletionDriverPrompt(pi, ctx, rootKey, fingerprint, resumePrompt, resumeKind);
2068
+ }
2069
+
2070
+ async function confirmExistingWorkflowProposal(
1758
2071
  ctx: { hasUI: boolean; ui: any },
1759
2072
  snapshot: CompletionStateSnapshot,
1760
- goal: string,
2073
+ proposal: ContextProposal,
2074
+ options: ExistingWorkflowChooserOptions = {},
1761
2075
  ): Promise<ExistingWorkflowDecision | undefined> {
1762
2076
  const currentMission = currentMissionAnchor(snapshot);
1763
- const assessment = assessMissionAnchor(goal, path.basename(snapshot.files.root));
1764
- const normalizedCurrent = normalizeMissionAnchorText(currentMission);
1765
- const normalizedGoal = normalizeMissionAnchorText(goal);
1766
- const normalizedProposed = normalizeMissionAnchorText(assessment.derived);
1767
- if (!normalizedGoal || normalizedGoal === normalizedCurrent || normalizedProposed === normalizedCurrent) {
2077
+ const comparison = options.comparison ?? "semantic";
2078
+ const missionsMatch =
2079
+ comparison === "strict"
2080
+ ? missionAnchorsStrictlyEquivalent(currentMission, proposal.mission)
2081
+ : missionAnchorsLikelyEquivalent(currentMission, proposal.mission);
2082
+ if (missionsMatch) {
1768
2083
  return { action: "continue", currentMissionAnchor: currentMission };
1769
2084
  }
1770
2085
  const title = [
1771
2086
  "Existing completion workflow found",
1772
2087
  "",
1773
- "A workflow is already in progress. Choose how /cook should proceed:",
2088
+ options.intro ?? "A workflow is already in progress. Choose how /cook should proceed:",
1774
2089
  "",
1775
2090
  "Current mission",
1776
2091
  currentMission,
1777
2092
  "",
1778
- "New proposed mission",
1779
- assessment.derived,
2093
+ options.proposedMissionLabel ?? "New proposed mission",
2094
+ proposal.mission,
1780
2095
  ].join("\n");
1781
2096
  const continueChoice = "Continue current workflow\n\nKeep the current mission and treat the new goal as extra direction only.";
1782
2097
  const refocusChoice =
2098
+ options.refocusChoiceLabel ??
1783
2099
  "Abandon current workflow and start this new one\n\nReview the proposed replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state.";
1784
2100
  const cancelChoice = `Cancel\n\nKeep the current workflow unchanged. ${COOK_MAIN_CHAT_RERUN_GUIDANCE}`;
1785
2101
  maybeWriteTestSnapshot(
@@ -1791,7 +2107,7 @@ async function confirmExistingWorkflowGoal(
1791
2107
  return { action: "continue", currentMissionAnchor: currentMission };
1792
2108
  }
1793
2109
  if (actionOverride === "refocus") {
1794
- return { action: "refocus", currentMissionAnchor: currentMission, missionAnchor: assessment.derived };
2110
+ return { action: "refocus", currentMissionAnchor: currentMission, missionAnchor: proposal.mission };
1795
2111
  }
1796
2112
  if (actionOverride === "cancel") return undefined;
1797
2113
  if (!getCtxHasUI(ctx)) {
@@ -1804,7 +2120,7 @@ async function confirmExistingWorkflowGoal(
1804
2120
  const choice = await ui.select(title, [continueChoice, refocusChoice, cancelChoice]);
1805
2121
  if (!choice || choice === cancelChoice) return undefined;
1806
2122
  if (choice === refocusChoice) {
1807
- return { action: "refocus", currentMissionAnchor: currentMission, missionAnchor: assessment.derived };
2123
+ return { action: "refocus", currentMissionAnchor: currentMission, missionAnchor: proposal.mission };
1808
2124
  }
1809
2125
  return { action: "continue", currentMissionAnchor: currentMission };
1810
2126
  }
@@ -3752,13 +4068,16 @@ export default function completionExtension(pi: ExtensionAPI) {
3752
4068
  });
3753
4069
 
3754
4070
  pi.registerCommand("cook", {
3755
- description: "Start or continue the completion workflow for a repo",
4071
+ description: "Discussion-driven /cook workflow: start, continue, refocus, or start the next round",
3756
4072
  handler: async (args, ctx) => {
3757
- const explicitGoal = args.trim();
3758
- let goal = explicitGoal;
4073
+ const inlineArgs = args.trim();
4074
+ if (inlineArgs) {
4075
+ emitCommandText(ctx, COOK_INLINE_ARG_REJECTION_MESSAGE, "info");
4076
+ return;
4077
+ }
4078
+ let goal: string | undefined;
3759
4079
  const cwd = getCtxCwd(ctx);
3760
4080
  let snapshot = await loadCompletionSnapshot(cwd);
3761
- const hadSnapshot = Boolean(snapshot);
3762
4081
  const workflowDone = isWorkflowDone(snapshot);
3763
4082
  let kickoffIntent: "auto" | "continue" | "refocus" = "auto";
3764
4083
  let kickoffMissionAnchor = snapshot ? currentMissionAnchor(snapshot) : undefined;
@@ -3767,40 +4086,21 @@ export default function completionExtension(pi: ExtensionAPI) {
3767
4086
  if (!snapshot) {
3768
4087
  const root = findRepoRoot(cwd) ?? cwd;
3769
4088
  const projectName = path.basename(root);
3770
- if (!goal) {
3771
- const proposal = await extractContextProposalFromSession(ctx, projectName);
3772
- if (!proposal) {
3773
- emitCommandText(
3774
- ctx,
3775
- "Usage: /cook <goal> (discussion-only startup needs proposal analyst output; otherwise pass an explicit goal)",
3776
- "error",
3777
- );
3778
- return;
3779
- }
3780
- const decision = await confirmContextProposal(ctx, proposal, {
3781
- title: "Start a completion workflow from the recent discussion?",
3782
- });
3783
- if (!decision) {
3784
- emitCommandText(ctx, buildCookCancellationMessage("Cancelled recent-discussion workflow proposal"), "info");
3785
- return;
3786
- }
3787
- goal = decision.goalText;
3788
- kickoffMissionAnchor = decision.missionAnchor;
3789
- kickoffAnalysis = decision.analysis;
3790
- } else {
3791
- const proposal = await buildGoalAnchoredContextProposal(ctx, goal, projectName);
3792
- const decision = await confirmContextProposal(ctx, proposal, {
3793
- title: "Start a completion workflow from this goal?",
3794
- nonInteractiveBehavior: "accept",
3795
- });
3796
- if (!decision) {
3797
- emitCommandText(ctx, buildCookCancellationMessage("Cancelled workflow startup proposal"), "info");
3798
- return;
3799
- }
3800
- goal = decision.goalText;
3801
- kickoffMissionAnchor = decision.missionAnchor;
3802
- kickoffAnalysis = decision.analysis;
4089
+ const proposal = await deriveCookContextProposal(ctx, projectName);
4090
+ if (!proposal) {
4091
+ emitCommandText(ctx, buildCookStructuredDiscussionFailureMessage(), "info");
4092
+ return;
4093
+ }
4094
+ const decision = await confirmContextProposal(ctx, proposal, {
4095
+ title: "Start a completion workflow from the recent discussion?",
4096
+ });
4097
+ if (!decision) {
4098
+ emitCommandText(ctx, buildCookCancellationMessage("Cancelled recent-discussion workflow proposal"), "info");
4099
+ return;
3803
4100
  }
4101
+ goal = decision.goalText;
4102
+ kickoffMissionAnchor = decision.missionAnchor;
4103
+ kickoffAnalysis = decision.analysis;
3804
4104
  const startupRouting = finalizeContextProposalAnalysis(kickoffAnalysis, [goal ?? kickoffMissionAnchor ?? projectName]);
3805
4105
  const created = await scaffoldCompletionFiles(root, kickoffMissionAnchor ?? projectName, {
3806
4106
  analysis: startupRouting,
@@ -3824,13 +4124,9 @@ export default function completionExtension(pi: ExtensionAPI) {
3824
4124
  if (!goal) {
3825
4125
  if (workflowDone) {
3826
4126
  const projectName = path.basename(snapshot.files.root);
3827
- const proposal = await extractContextProposalFromSession(ctx, projectName);
4127
+ const proposal = await deriveCookContextProposal(ctx, projectName);
3828
4128
  if (!proposal) {
3829
- emitCommandText(
3830
- ctx,
3831
- "The previous completion workflow is already done. Provide /cook <goal>, or rerun /cook when the proposal analyst can summarize the next round from discussion.",
3832
- "info",
3833
- );
4129
+ emitCommandText(ctx, buildCookStructuredDiscussionFailureMessage("The previous completion workflow is already done."), "info");
3834
4130
  return;
3835
4131
  }
3836
4132
  const decision = await confirmContextProposal(ctx, proposal, {
@@ -3847,78 +4143,41 @@ export default function completionExtension(pi: ExtensionAPI) {
3847
4143
  snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
3848
4144
  emitCommandText(ctx, `Started a new completion workflow round from recent discussion: ${decision.missionAnchor}`, "info");
3849
4145
  } else {
3850
- const mission = currentMissionAnchor(snapshot);
3851
- pi.setSessionName(`completion: ${mission.slice(0, 60)}`);
3852
- const resumePrompt = completionResumePrompt(
3853
- currentTaskType(snapshot) ?? "(missing)",
3854
- currentEvaluationProfile(snapshot) ?? "(missing)",
3855
- );
3856
- const rootKey = completionRootKey(snapshot, getCtxCwd(ctx));
3857
- const fingerprint = completionContinuationFingerprint(snapshot) ?? JSON.stringify({
3858
- kind: "resume",
3859
- mission_anchor: currentMissionAnchor(snapshot),
3860
- current_phase: asString(snapshot.state?.current_phase) ?? null,
3861
- next_mandatory_role: asString(snapshot.state?.next_mandatory_role) ?? null,
3862
- });
3863
- const resumeKind =
3864
- shouldTestAutoContinueOnSessionStart() && completionTestAutoContinuePromptPath() ? "auto-resume" : "resume";
3865
- await queueCompletionDriverPrompt(pi, ctx, rootKey, fingerprint, resumePrompt, resumeKind);
3866
- return;
3867
- }
3868
- }
3869
- kickoffMissionAnchor = kickoffMissionAnchor ?? currentMissionAnchor(snapshot);
3870
- if (hadSnapshot && explicitGoal) {
3871
- if (workflowDone) {
3872
- const projectName = path.basename(snapshot.files.root);
3873
- const proposal = await buildGoalAnchoredContextProposal(ctx, goal, projectName);
3874
- const decision = await confirmContextProposal(ctx, proposal, {
3875
- title: "Start the next workflow round from this goal?",
3876
- nonInteractiveBehavior: "accept",
3877
- });
3878
- if (!decision) {
3879
- emitCommandText(ctx, buildCookCancellationMessage("Cancelled next workflow round proposal"), "info");
4146
+ const assessment = await assessActiveWorkflowProposalRouting(ctx, snapshot);
4147
+ if (assessment.action !== "refocus" || !assessment.proposal) {
4148
+ await resumeActiveWorkflowFromCanonicalState(pi, ctx, snapshot);
3880
4149
  return;
3881
4150
  }
3882
- goal = decision.goalText;
3883
- kickoffIntent = "refocus";
3884
- kickoffMissionAnchor = decision.missionAnchor;
3885
- await refocusCompletionMission(snapshot, decision.missionAnchor, decision.goalText, decision.analysis);
3886
- snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
3887
- emitCommandText(ctx, `Started a new completion workflow round from explicit goal: ${decision.missionAnchor}`, "info");
3888
- } else {
3889
- const decision = await confirmExistingWorkflowGoal(ctx, snapshot, goal);
4151
+ const decision = await confirmExistingWorkflowProposal(ctx, snapshot, assessment.proposal, {
4152
+ intro: "Recent non-command discussion suggests a different workflow. Choose how /cook should proceed:",
4153
+ proposedMissionLabel: "Proposed mission from recent discussion",
4154
+ refocusChoiceLabel:
4155
+ "Start new workflow from recent discussion\n\nReview the proposed replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state.",
4156
+ });
3890
4157
  if (!decision) {
3891
4158
  emitCommandText(ctx, buildCookCancellationMessage("Cancelled existing workflow confirmation"), "info");
3892
4159
  return;
3893
4160
  }
3894
- kickoffIntent = decision.action;
3895
- kickoffMissionAnchor = decision.currentMissionAnchor;
3896
- if (decision.action === "refocus") {
3897
- const projectName = path.basename(snapshot.files.root);
3898
- const proposal = await buildGoalAnchoredContextProposal(ctx, goal, projectName);
3899
- const proposalDecision = await confirmContextProposal(ctx, proposal, {
3900
- title: "Start the replacement workflow from this goal?",
3901
- nonInteractiveBehavior: "accept",
3902
- });
3903
- if (!proposalDecision) {
3904
- emitCommandText(ctx, buildCookCancellationMessage("Cancelled replacement workflow proposal"), "info");
3905
- return;
3906
- }
3907
- goal = proposalDecision.goalText;
3908
- await refocusCompletionMission(
3909
- snapshot,
3910
- proposalDecision.missionAnchor,
3911
- proposalDecision.goalText,
3912
- proposalDecision.analysis,
3913
- );
3914
- snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
3915
- kickoffMissionAnchor = proposalDecision.missionAnchor;
3916
- emitCommandText(ctx, `Refocused completion mission to: ${proposalDecision.missionAnchor}`, "info");
3917
- } else if (normalizeMissionAnchorText(goal) !== normalizeMissionAnchorText(decision.currentMissionAnchor)) {
3918
- emitCommandText(ctx, `Continuing existing workflow without changing mission anchor: ${decision.currentMissionAnchor}`, "info");
4161
+ if (decision.action === "continue") {
4162
+ await resumeActiveWorkflowFromCanonicalState(pi, ctx, snapshot);
4163
+ return;
3919
4164
  }
4165
+ const proposalDecision = await confirmContextProposal(ctx, assessment.proposal, {
4166
+ title: "Start the replacement workflow from recent discussion?",
4167
+ });
4168
+ if (!proposalDecision) {
4169
+ emitCommandText(ctx, buildCookCancellationMessage("Cancelled replacement workflow proposal"), "info");
4170
+ return;
4171
+ }
4172
+ goal = proposalDecision.goalText;
4173
+ kickoffIntent = "refocus";
4174
+ kickoffMissionAnchor = proposalDecision.missionAnchor;
4175
+ await refocusCompletionMission(snapshot, proposalDecision.missionAnchor, proposalDecision.goalText, proposalDecision.analysis);
4176
+ snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
4177
+ emitCommandText(ctx, `Refocused completion mission from recent discussion to: ${proposalDecision.missionAnchor}`, "info");
3920
4178
  }
3921
4179
  }
4180
+ kickoffMissionAnchor = kickoffMissionAnchor ?? currentMissionAnchor(snapshot);
3922
4181
  pi.setSessionName(`completion: ${kickoffMissionAnchor.slice(0, 60)}`);
3923
4182
  const kickoffPrompt = completionKickoff(
3924
4183
  goal,