@linimin/pi-letscook 0.1.36 → 0.1.37

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" | "explicit_goal" | "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,17 @@ 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_STRUCTURED_DISCUSSION_FAILURE_DETAIL =
543
+ "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 or, as a temporary compatibility shim, pass /cook <text>.";
524
544
 
525
545
  function buildCookCancellationMessage(prefix: string): string {
526
546
  return `${prefix}. ${COOK_MAIN_CHAT_RERUN_GUIDANCE}`;
527
547
  }
528
548
 
549
+ function buildCookStructuredDiscussionFailureMessage(prefix?: string): string {
550
+ return prefix ? `${prefix} ${COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL}` : COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL;
551
+ }
552
+
529
553
  function shouldDisableContextProposalAnalyst(): boolean {
530
554
  return process.env.PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST === "1";
531
555
  }
@@ -785,6 +809,113 @@ function isSessionScopeItemMissionRelevant(item: string, mission: string): boole
785
809
  return overlap.some((token) => token.length >= 6 || /[\p{Script=Han}]/u.test(token));
786
810
  }
787
811
 
812
+ function missionAnchorSemanticTokens(text: string): string[] {
813
+ return [...new Set(missionScopeFilterTokens(normalizeMissionAnchorText(text).toLowerCase()))];
814
+ }
815
+
816
+ function missionAnchorOrderedTokenOverlapRatio(leftTokens: string[], rightTokens: string[]): number {
817
+ if (leftTokens.length === 0 || rightTokens.length === 0) return 0;
818
+ const dp = new Array(rightTokens.length + 1).fill(0);
819
+ for (const leftToken of leftTokens) {
820
+ let previous = 0;
821
+ for (let index = 0; index < rightTokens.length; index += 1) {
822
+ const nextPrevious = dp[index + 1];
823
+ if (leftToken === rightTokens[index]) {
824
+ dp[index + 1] = previous + 1;
825
+ } else {
826
+ dp[index + 1] = Math.max(dp[index + 1], dp[index]);
827
+ }
828
+ previous = nextPrevious;
829
+ }
830
+ }
831
+ return dp[rightTokens.length] / Math.max(leftTokens.length, rightTokens.length);
832
+ }
833
+
834
+ function missionAnchorBigramOverlapRatio(leftTokens: string[], rightTokens: string[]): number {
835
+ if (leftTokens.length < 2 || rightTokens.length < 2) return 0;
836
+ const leftBigrams = new Set(leftTokens.slice(0, -1).map((token, index) => `${token} ${leftTokens[index + 1]}`));
837
+ const rightBigrams = new Set(rightTokens.slice(0, -1).map((token, index) => `${token} ${rightTokens[index + 1]}`));
838
+ if (leftBigrams.size === 0 || rightBigrams.size === 0) return 0;
839
+ let overlap = 0;
840
+ for (const bigram of leftBigrams) {
841
+ if (rightBigrams.has(bigram)) overlap += 1;
842
+ }
843
+ return overlap / Math.max(leftBigrams.size, rightBigrams.size);
844
+ }
845
+
846
+ function missionAnchorsStrictlyEquivalent(left: string, right: string): boolean {
847
+ return normalizeMissionAnchorText(left).toLowerCase() === normalizeMissionAnchorText(right).toLowerCase();
848
+ }
849
+
850
+ const MISSION_NEGATION_CUE_REGEX = /(?:^|[^\p{L}\p{N}_])(?:no|not|without|never|cannot|don['’]?t)(?=$|[^\p{L}\p{N}_])/u;
851
+
852
+ function missionAnchorHasNegationCue(text: string): boolean {
853
+ return MISSION_NEGATION_CUE_REGEX.test(text);
854
+ }
855
+
856
+ function missionAnchorsLikelyEquivalent(left: string, right: string): boolean {
857
+ const normalizedLeft = normalizeMissionAnchorText(left).toLowerCase();
858
+ const normalizedRight = normalizeMissionAnchorText(right).toLowerCase();
859
+ if (!normalizedLeft || !normalizedRight) return false;
860
+ const leftHasNegationCue = missionAnchorHasNegationCue(normalizedLeft);
861
+ const rightHasNegationCue = missionAnchorHasNegationCue(normalizedRight);
862
+ if (leftHasNegationCue !== rightHasNegationCue) return false;
863
+ if (normalizedLeft === normalizedRight) return true;
864
+ if (!leftHasNegationCue && (normalizedLeft.includes(normalizedRight) || normalizedRight.includes(normalizedLeft))) return true;
865
+ const leftTokens = missionAnchorSemanticTokens(normalizedLeft);
866
+ const rightTokens = missionAnchorSemanticTokens(normalizedRight);
867
+ if (leftTokens.length === 0 || rightTokens.length === 0) return false;
868
+ const rightSet = new Set(rightTokens);
869
+ const overlap = leftTokens.filter((token) => rightSet.has(token));
870
+ if (overlap.length < 3) return false;
871
+ const maxLen = Math.max(leftTokens.length, rightTokens.length);
872
+ if (overlap.length / maxLen < 0.75) return false;
873
+ if (missionAnchorOrderedTokenOverlapRatio(leftTokens, rightTokens) < 0.75) return false;
874
+ if (Math.min(leftTokens.length, rightTokens.length) < 4) return true;
875
+ return missionAnchorBigramOverlapRatio(leftTokens, rightTokens) >= 0.5;
876
+ }
877
+
878
+ function shouldTreatBareActiveWorkflowProposalAsClearRefocus(proposal: ContextProposal): boolean {
879
+ if (proposal.source === "session") {
880
+ return proposal.scope.length > 0 && proposal.constraints.length > 0 && proposal.acceptance.length > 0;
881
+ }
882
+ return (
883
+ proposal.scope.length > 0 &&
884
+ proposal.constraints.length > 0 &&
885
+ proposal.acceptance.length > 0 &&
886
+ proposal.analysis.possibleNoise.length === 0
887
+ );
888
+ }
889
+
890
+ function maybeWriteActiveWorkflowRoutingSnapshot(
891
+ assessment: ActiveWorkflowProposalAssessment,
892
+ options: { mode: "bare" | "explicit"; explicitGoal?: string },
893
+ ): void {
894
+ const snapshotPath = completionTestActiveWorkflowRoutingSnapshotPath();
895
+ if (!snapshotPath) return;
896
+ maybeWriteTestSnapshot(
897
+ snapshotPath,
898
+ `${JSON.stringify(
899
+ {
900
+ mode: options.mode,
901
+ explicitGoalProvided: Boolean(options.explicitGoal),
902
+ explicitGoal: options.explicitGoal ?? null,
903
+ action: assessment.action,
904
+ reason: assessment.reason,
905
+ currentMissionAnchor: assessment.currentMissionAnchor,
906
+ proposedMissionAnchor: assessment.proposal?.mission ?? null,
907
+ proposalSource: assessment.proposal?.source ?? null,
908
+ possibleNoise: assessment.proposal?.analysis.possibleNoise ?? [],
909
+ scope: assessment.proposal?.scope ?? [],
910
+ constraints: assessment.proposal?.constraints ?? [],
911
+ acceptance: assessment.proposal?.acceptance ?? [],
912
+ },
913
+ null,
914
+ 2,
915
+ )}\n`,
916
+ );
917
+ }
918
+
788
919
  const CONTEXT_PROPOSAL_ANALYST_SYSTEM_PROMPT = [
789
920
  "You analyze recent /cook startup discussion and return a strict JSON object.",
790
921
  "Do not emit markdown, code fences, or commentary.",
@@ -1443,13 +1574,85 @@ function parseContextProposal(text: string, projectName: string): ContextProposa
1443
1574
  };
1444
1575
  }
1445
1576
 
1577
+ function hasStructuredContextProposalSignal(text: string): boolean {
1578
+ const cleaned = stripCodeBlocks(text).replace(/\r/g, "").trim();
1579
+ if (!cleaned) return false;
1580
+ 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(
1581
+ cleaned,
1582
+ );
1583
+ }
1584
+
1585
+ function parseStrictStructuredSessionProposal(text: string, projectName: string): ContextProposal | undefined {
1586
+ const cleaned = stripCodeBlocks(text).replace(/\r/g, "").trim();
1587
+ if (!cleaned) return undefined;
1588
+ const lines = cleaned
1589
+ .split("\n")
1590
+ .map((line) => line.trim())
1591
+ .filter((line) => line.length > 0);
1592
+ if (lines.length === 0) return undefined;
1593
+
1594
+ let section: ContextProposalSection | undefined;
1595
+ const sectionsPresent = new Set<ContextProposalSection>();
1596
+ const missionCandidates: string[] = [];
1597
+
1598
+ for (const rawLine of lines) {
1599
+ const inlineSection = matchInlineProposalSection(rawLine);
1600
+ if (inlineSection) {
1601
+ section = inlineSection.section;
1602
+ sectionsPresent.add(section);
1603
+ if (section === "mission") missionCandidates.push(inlineSection.content);
1604
+ continue;
1605
+ }
1606
+ const headerSection = detectProposalSection(rawLine);
1607
+ if (headerSection) {
1608
+ section = headerSection;
1609
+ sectionsPresent.add(section);
1610
+ continue;
1611
+ }
1612
+ const normalized = bulletText(rawLine) ?? normalizeProposalLine(rawLine);
1613
+ if (normalized && section === "mission") missionCandidates.push(normalized);
1614
+ }
1615
+
1616
+ const requiredSections: ContextProposalSection[] = ["mission", "scope", "constraints", "acceptance"];
1617
+ if (requiredSections.some((candidate) => !sectionsPresent.has(candidate))) return undefined;
1618
+
1619
+ const distinctMissionAnchors = Array.from(
1620
+ new Set(
1621
+ missionCandidates
1622
+ .map((candidate) => normalizeMissionAnchorText(assessMissionAnchor(candidate, projectName).derived))
1623
+ .filter((candidate): candidate is string => Boolean(candidate)),
1624
+ ),
1625
+ );
1626
+ if (distinctMissionAnchors.length !== 1) return undefined;
1627
+
1628
+ const proposal = parseContextProposal(cleaned, projectName);
1629
+ if (!proposal) return undefined;
1630
+ if (normalizeMissionAnchorText(proposal.mission) !== distinctMissionAnchors[0]) return undefined;
1631
+ if (proposal.scope.length === 0 || proposal.constraints.length === 0 || proposal.acceptance.length === 0) return undefined;
1632
+ return { ...proposal, source: "session" };
1633
+ }
1634
+
1635
+ function extractContextProposalFromStructuredSession(
1636
+ recentEntries: RecentDiscussionEntry[],
1637
+ projectName: string,
1638
+ ): ContextProposal | undefined {
1639
+ const structuredTexts = recentEntries
1640
+ .slice()
1641
+ .reverse()
1642
+ .map((entry) => entry.text.trim())
1643
+ .filter((text) => hasStructuredContextProposalSignal(text));
1644
+ if (structuredTexts.length === 0) return undefined;
1645
+ return parseStrictStructuredSessionProposal(structuredTexts.join("\n\n"), projectName);
1646
+ }
1647
+
1446
1648
  async function extractContextProposalFromSession(
1447
1649
  ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
1448
1650
  projectName: string,
1449
1651
  explicitGoal?: string,
1450
1652
  ): Promise<ContextProposal | undefined> {
1451
1653
  const recentEntries = collectRecentDiscussionEntries(ctx);
1452
- return await analyzeContextProposalWithAgent(ctx, projectName, recentEntries, explicitGoal);
1654
+ return (await analyzeContextProposalWithAgent(ctx, projectName, recentEntries, explicitGoal)) ??
1655
+ extractContextProposalFromStructuredSession(recentEntries, projectName);
1453
1656
  }
1454
1657
 
1455
1658
  async function buildGoalAnchoredContextProposal(
@@ -1462,11 +1665,16 @@ async function buildGoalAnchoredContextProposal(
1462
1665
  const missionSource = explicit?.mission ?? goal;
1463
1666
  const assessment = assessMissionAnchor(missionSource, projectName);
1464
1667
  const mission = assessment.derived;
1668
+ const mergeSessionBody = sessionProposal?.source === "analyst";
1465
1669
  const explicitScope = explicit?.scope ?? [];
1466
- const sessionScope = (sessionProposal?.scope ?? []).filter((item) => isSessionScopeItemMissionRelevant(item, mission));
1670
+ const sessionScope = mergeSessionBody
1671
+ ? (sessionProposal?.scope ?? []).filter((item) => isSessionScopeItemMissionRelevant(item, mission))
1672
+ : [];
1673
+ const sessionConstraints = mergeSessionBody ? sessionProposal?.constraints ?? [] : [];
1674
+ const sessionAcceptance = mergeSessionBody ? sessionProposal?.acceptance ?? [] : [];
1467
1675
  const scope = uniqueProposalItems([...explicitScope, ...sessionScope]);
1468
- const constraints = uniqueProposalItems([...(explicit?.constraints ?? []), ...(sessionProposal?.constraints ?? [])]);
1469
- const acceptance = uniqueProposalItems([...(explicit?.acceptance ?? []), ...(sessionProposal?.acceptance ?? [])]);
1676
+ const constraints = uniqueProposalItems([...(explicit?.constraints ?? []), ...sessionConstraints]);
1677
+ const acceptance = uniqueProposalItems([...(explicit?.acceptance ?? []), ...sessionAcceptance]);
1470
1678
  const analysis = mergeContextProposalAnalysis(
1471
1679
  [explicit?.analysis, sessionProposal?.analysis],
1472
1680
  [goal, mission, ...(sessionProposal?.analysis.possibleNoise ?? []), ...scope, ...constraints, ...acceptance],
@@ -1484,6 +1692,16 @@ async function buildGoalAnchoredContextProposal(
1484
1692
  };
1485
1693
  }
1486
1694
 
1695
+ async function deriveCookContextProposal(
1696
+ ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
1697
+ projectName: string,
1698
+ explicitGoal?: string,
1699
+ ): Promise<ContextProposal | undefined> {
1700
+ return explicitGoal
1701
+ ? await buildGoalAnchoredContextProposal(ctx, explicitGoal, projectName)
1702
+ : await extractContextProposalFromSession(ctx, projectName);
1703
+ }
1704
+
1487
1705
  async function confirmContextProposal(
1488
1706
  ctx: { hasUI: boolean; ui: any },
1489
1707
  proposal: ContextProposal,
@@ -1754,32 +1972,110 @@ function buildEvaluationRoleReminderText(snapshot: CompletionStateSnapshot, role
1754
1972
  return buildEvaluationRoleContextLines(snapshot, role).join(" ");
1755
1973
  }
1756
1974
 
1757
- async function confirmExistingWorkflowGoal(
1975
+ async function assessActiveWorkflowProposalRouting(
1976
+ ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
1977
+ snapshot: CompletionStateSnapshot,
1978
+ explicitGoal?: string,
1979
+ ): Promise<ActiveWorkflowProposalAssessment> {
1980
+ const currentMission = currentMissionAnchor(snapshot);
1981
+ const projectName = path.basename(snapshot.files.root);
1982
+ const proposal = explicitGoal
1983
+ ? await deriveCookContextProposal(ctx, projectName, explicitGoal)
1984
+ : await deriveCookContextProposal(ctx, projectName);
1985
+ const mode = explicitGoal ? "explicit" : "bare";
1986
+ if (!proposal) {
1987
+ const assessment: ActiveWorkflowProposalAssessment = {
1988
+ action: "unclear",
1989
+ currentMissionAnchor: currentMission,
1990
+ reason: "missing_proposal",
1991
+ };
1992
+ maybeWriteActiveWorkflowRoutingSnapshot(assessment, { mode, explicitGoal });
1993
+ return assessment;
1994
+ }
1995
+ const missionsMatch = explicitGoal
1996
+ ? missionAnchorsStrictlyEquivalent(currentMission, proposal.mission)
1997
+ : missionAnchorsLikelyEquivalent(currentMission, proposal.mission);
1998
+ if (missionsMatch) {
1999
+ const assessment: ActiveWorkflowProposalAssessment = {
2000
+ action: "continue",
2001
+ currentMissionAnchor: currentMission,
2002
+ proposal,
2003
+ reason: "matching_mission",
2004
+ };
2005
+ maybeWriteActiveWorkflowRoutingSnapshot(assessment, { mode, explicitGoal });
2006
+ return assessment;
2007
+ }
2008
+ if (explicitGoal || shouldTreatBareActiveWorkflowProposalAsClearRefocus(proposal)) {
2009
+ const assessment: ActiveWorkflowProposalAssessment = {
2010
+ action: "refocus",
2011
+ currentMissionAnchor: currentMission,
2012
+ proposal,
2013
+ reason: explicitGoal ? "explicit_goal" : "clear_refocus",
2014
+ };
2015
+ maybeWriteActiveWorkflowRoutingSnapshot(assessment, { mode, explicitGoal });
2016
+ return assessment;
2017
+ }
2018
+ const assessment: ActiveWorkflowProposalAssessment = {
2019
+ action: "unclear",
2020
+ currentMissionAnchor: currentMission,
2021
+ proposal,
2022
+ reason: "ambiguous_discussion",
2023
+ };
2024
+ maybeWriteActiveWorkflowRoutingSnapshot(assessment, { mode, explicitGoal });
2025
+ return assessment;
2026
+ }
2027
+
2028
+ async function resumeActiveWorkflowFromCanonicalState(
2029
+ pi: any,
2030
+ ctx: { cwd: string; hasUI: boolean; ui: any },
2031
+ snapshot: CompletionStateSnapshot,
2032
+ ): Promise<void> {
2033
+ const mission = currentMissionAnchor(snapshot);
2034
+ pi.setSessionName(`completion: ${mission.slice(0, 60)}`);
2035
+ const resumePrompt = completionResumePrompt(
2036
+ currentTaskType(snapshot) ?? "(missing)",
2037
+ currentEvaluationProfile(snapshot) ?? "(missing)",
2038
+ );
2039
+ const rootKey = completionRootKey(snapshot, getCtxCwd(ctx));
2040
+ const fingerprint = completionContinuationFingerprint(snapshot) ?? JSON.stringify({
2041
+ kind: "resume",
2042
+ mission_anchor: mission,
2043
+ current_phase: asString(snapshot.state?.current_phase) ?? null,
2044
+ next_mandatory_role: asString(snapshot.state?.next_mandatory_role) ?? null,
2045
+ });
2046
+ const resumeKind = shouldTestAutoContinueOnSessionStart() && completionTestAutoContinuePromptPath() ? "auto-resume" : "resume";
2047
+ await queueCompletionDriverPrompt(pi, ctx, rootKey, fingerprint, resumePrompt, resumeKind);
2048
+ }
2049
+
2050
+ async function confirmExistingWorkflowProposal(
1758
2051
  ctx: { hasUI: boolean; ui: any },
1759
2052
  snapshot: CompletionStateSnapshot,
1760
- goal: string,
2053
+ proposal: ContextProposal,
2054
+ options: ExistingWorkflowChooserOptions = {},
1761
2055
  ): Promise<ExistingWorkflowDecision | undefined> {
1762
2056
  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) {
2057
+ const comparison = options.comparison ?? "semantic";
2058
+ const missionsMatch =
2059
+ comparison === "strict"
2060
+ ? missionAnchorsStrictlyEquivalent(currentMission, proposal.mission)
2061
+ : missionAnchorsLikelyEquivalent(currentMission, proposal.mission);
2062
+ if (missionsMatch) {
1768
2063
  return { action: "continue", currentMissionAnchor: currentMission };
1769
2064
  }
1770
2065
  const title = [
1771
2066
  "Existing completion workflow found",
1772
2067
  "",
1773
- "A workflow is already in progress. Choose how /cook should proceed:",
2068
+ options.intro ?? "A workflow is already in progress. Choose how /cook should proceed:",
1774
2069
  "",
1775
2070
  "Current mission",
1776
2071
  currentMission,
1777
2072
  "",
1778
- "New proposed mission",
1779
- assessment.derived,
2073
+ options.proposedMissionLabel ?? "New proposed mission",
2074
+ proposal.mission,
1780
2075
  ].join("\n");
1781
2076
  const continueChoice = "Continue current workflow\n\nKeep the current mission and treat the new goal as extra direction only.";
1782
2077
  const refocusChoice =
2078
+ options.refocusChoiceLabel ??
1783
2079
  "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
2080
  const cancelChoice = `Cancel\n\nKeep the current workflow unchanged. ${COOK_MAIN_CHAT_RERUN_GUIDANCE}`;
1785
2081
  maybeWriteTestSnapshot(
@@ -1791,7 +2087,7 @@ async function confirmExistingWorkflowGoal(
1791
2087
  return { action: "continue", currentMissionAnchor: currentMission };
1792
2088
  }
1793
2089
  if (actionOverride === "refocus") {
1794
- return { action: "refocus", currentMissionAnchor: currentMission, missionAnchor: assessment.derived };
2090
+ return { action: "refocus", currentMissionAnchor: currentMission, missionAnchor: proposal.mission };
1795
2091
  }
1796
2092
  if (actionOverride === "cancel") return undefined;
1797
2093
  if (!getCtxHasUI(ctx)) {
@@ -1804,7 +2100,7 @@ async function confirmExistingWorkflowGoal(
1804
2100
  const choice = await ui.select(title, [continueChoice, refocusChoice, cancelChoice]);
1805
2101
  if (!choice || choice === cancelChoice) return undefined;
1806
2102
  if (choice === refocusChoice) {
1807
- return { action: "refocus", currentMissionAnchor: currentMission, missionAnchor: assessment.derived };
2103
+ return { action: "refocus", currentMissionAnchor: currentMission, missionAnchor: proposal.mission };
1808
2104
  }
1809
2105
  return { action: "continue", currentMissionAnchor: currentMission };
1810
2106
  }
@@ -3752,7 +4048,7 @@ export default function completionExtension(pi: ExtensionAPI) {
3752
4048
  });
3753
4049
 
3754
4050
  pi.registerCommand("cook", {
3755
- description: "Start or continue the completion workflow for a repo",
4051
+ description: "Discussion-driven /cook workflow: start, continue, refocus, or start the next round",
3756
4052
  handler: async (args, ctx) => {
3757
4053
  const explicitGoal = args.trim();
3758
4054
  let goal = explicitGoal;
@@ -3768,13 +4064,9 @@ export default function completionExtension(pi: ExtensionAPI) {
3768
4064
  const root = findRepoRoot(cwd) ?? cwd;
3769
4065
  const projectName = path.basename(root);
3770
4066
  if (!goal) {
3771
- const proposal = await extractContextProposalFromSession(ctx, projectName);
4067
+ const proposal = await deriveCookContextProposal(ctx, projectName);
3772
4068
  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
- );
4069
+ emitCommandText(ctx, buildCookStructuredDiscussionFailureMessage(), "info");
3778
4070
  return;
3779
4071
  }
3780
4072
  const decision = await confirmContextProposal(ctx, proposal, {
@@ -3788,7 +4080,11 @@ export default function completionExtension(pi: ExtensionAPI) {
3788
4080
  kickoffMissionAnchor = decision.missionAnchor;
3789
4081
  kickoffAnalysis = decision.analysis;
3790
4082
  } else {
3791
- const proposal = await buildGoalAnchoredContextProposal(ctx, goal, projectName);
4083
+ const proposal = await deriveCookContextProposal(ctx, projectName, goal);
4084
+ if (!proposal) {
4085
+ emitCommandText(ctx, "Failed to derive a workflow startup proposal from this goal.", "error");
4086
+ return;
4087
+ }
3792
4088
  const decision = await confirmContextProposal(ctx, proposal, {
3793
4089
  title: "Start a completion workflow from this goal?",
3794
4090
  nonInteractiveBehavior: "accept",
@@ -3824,13 +4120,9 @@ export default function completionExtension(pi: ExtensionAPI) {
3824
4120
  if (!goal) {
3825
4121
  if (workflowDone) {
3826
4122
  const projectName = path.basename(snapshot.files.root);
3827
- const proposal = await extractContextProposalFromSession(ctx, projectName);
4123
+ const proposal = await deriveCookContextProposal(ctx, projectName);
3828
4124
  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
- );
4125
+ emitCommandText(ctx, buildCookStructuredDiscussionFailureMessage("The previous completion workflow is already done."), "info");
3834
4126
  return;
3835
4127
  }
3836
4128
  const decision = await confirmContextProposal(ctx, proposal, {
@@ -3847,30 +4139,49 @@ export default function completionExtension(pi: ExtensionAPI) {
3847
4139
  snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
3848
4140
  emitCommandText(ctx, `Started a new completion workflow round from recent discussion: ${decision.missionAnchor}`, "info");
3849
4141
  } 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,
4142
+ const assessment = await assessActiveWorkflowProposalRouting(ctx, snapshot);
4143
+ if (assessment.action !== "refocus" || !assessment.proposal) {
4144
+ await resumeActiveWorkflowFromCanonicalState(pi, ctx, snapshot);
4145
+ return;
4146
+ }
4147
+ const decision = await confirmExistingWorkflowProposal(ctx, snapshot, assessment.proposal, {
4148
+ intro: "Recent non-command discussion suggests a different workflow. Choose how /cook should proceed:",
4149
+ proposedMissionLabel: "Proposed mission from recent discussion",
4150
+ refocusChoiceLabel:
4151
+ "Start new workflow from recent discussion\n\nReview the proposed replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state.",
3862
4152
  });
3863
- const resumeKind =
3864
- shouldTestAutoContinueOnSessionStart() && completionTestAutoContinuePromptPath() ? "auto-resume" : "resume";
3865
- await queueCompletionDriverPrompt(pi, ctx, rootKey, fingerprint, resumePrompt, resumeKind);
3866
- return;
4153
+ if (!decision) {
4154
+ emitCommandText(ctx, buildCookCancellationMessage("Cancelled existing workflow confirmation"), "info");
4155
+ return;
4156
+ }
4157
+ if (decision.action === "continue") {
4158
+ await resumeActiveWorkflowFromCanonicalState(pi, ctx, snapshot);
4159
+ return;
4160
+ }
4161
+ const proposalDecision = await confirmContextProposal(ctx, assessment.proposal, {
4162
+ title: "Start the replacement workflow from recent discussion?",
4163
+ });
4164
+ if (!proposalDecision) {
4165
+ emitCommandText(ctx, buildCookCancellationMessage("Cancelled replacement workflow proposal"), "info");
4166
+ return;
4167
+ }
4168
+ goal = proposalDecision.goalText;
4169
+ kickoffIntent = "refocus";
4170
+ kickoffMissionAnchor = proposalDecision.missionAnchor;
4171
+ await refocusCompletionMission(snapshot, proposalDecision.missionAnchor, proposalDecision.goalText, proposalDecision.analysis);
4172
+ snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
4173
+ emitCommandText(ctx, `Refocused completion mission from recent discussion to: ${proposalDecision.missionAnchor}`, "info");
3867
4174
  }
3868
4175
  }
3869
4176
  kickoffMissionAnchor = kickoffMissionAnchor ?? currentMissionAnchor(snapshot);
3870
4177
  if (hadSnapshot && explicitGoal) {
3871
4178
  if (workflowDone) {
3872
4179
  const projectName = path.basename(snapshot.files.root);
3873
- const proposal = await buildGoalAnchoredContextProposal(ctx, goal, projectName);
4180
+ const proposal = await deriveCookContextProposal(ctx, projectName, goal);
4181
+ if (!proposal) {
4182
+ emitCommandText(ctx, "Failed to derive the next workflow round proposal from this goal.", "error");
4183
+ return;
4184
+ }
3874
4185
  const decision = await confirmContextProposal(ctx, proposal, {
3875
4186
  title: "Start the next workflow round from this goal?",
3876
4187
  nonInteractiveBehavior: "accept",
@@ -3886,36 +4197,47 @@ export default function completionExtension(pi: ExtensionAPI) {
3886
4197
  snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
3887
4198
  emitCommandText(ctx, `Started a new completion workflow round from explicit goal: ${decision.missionAnchor}`, "info");
3888
4199
  } else {
3889
- const decision = await confirmExistingWorkflowGoal(ctx, snapshot, goal);
3890
- if (!decision) {
3891
- emitCommandText(ctx, buildCookCancellationMessage("Cancelled existing workflow confirmation"), "info");
4200
+ const assessment = await assessActiveWorkflowProposalRouting(ctx, snapshot, goal);
4201
+ if (!assessment.proposal) {
4202
+ emitCommandText(ctx, "Failed to derive the replacement workflow proposal from this goal.", "error");
3892
4203
  return;
3893
4204
  }
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");
4205
+ if (assessment.action === "continue") {
4206
+ kickoffIntent = "continue";
4207
+ kickoffMissionAnchor = assessment.currentMissionAnchor;
4208
+ if (normalizeMissionAnchorText(goal) !== normalizeMissionAnchorText(assessment.currentMissionAnchor)) {
4209
+ emitCommandText(ctx, `Continuing existing workflow without changing mission anchor: ${assessment.currentMissionAnchor}`, "info");
4210
+ }
4211
+ } else {
4212
+ const decision = await confirmExistingWorkflowProposal(ctx, snapshot, assessment.proposal, { comparison: "strict" });
4213
+ if (!decision) {
4214
+ emitCommandText(ctx, buildCookCancellationMessage("Cancelled existing workflow confirmation"), "info");
3905
4215
  return;
3906
4216
  }
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");
4217
+ kickoffIntent = decision.action;
4218
+ kickoffMissionAnchor = decision.currentMissionAnchor;
4219
+ if (decision.action === "refocus") {
4220
+ const proposalDecision = await confirmContextProposal(ctx, assessment.proposal, {
4221
+ title: "Start the replacement workflow from this goal?",
4222
+ nonInteractiveBehavior: "accept",
4223
+ });
4224
+ if (!proposalDecision) {
4225
+ emitCommandText(ctx, buildCookCancellationMessage("Cancelled replacement workflow proposal"), "info");
4226
+ return;
4227
+ }
4228
+ goal = proposalDecision.goalText;
4229
+ await refocusCompletionMission(
4230
+ snapshot,
4231
+ proposalDecision.missionAnchor,
4232
+ proposalDecision.goalText,
4233
+ proposalDecision.analysis,
4234
+ );
4235
+ snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
4236
+ kickoffMissionAnchor = proposalDecision.missionAnchor;
4237
+ emitCommandText(ctx, `Refocused completion mission to: ${proposalDecision.missionAnchor}`, "info");
4238
+ } else if (normalizeMissionAnchorText(goal) !== normalizeMissionAnchorText(decision.currentMissionAnchor)) {
4239
+ emitCommandText(ctx, `Continuing existing workflow without changing mission anchor: ${decision.currentMissionAnchor}`, "info");
4240
+ }
3919
4241
  }
3920
4242
  }
3921
4243
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linimin/pi-letscook",
3
- "version": "0.1.36",
3
+ "version": "0.1.37",
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,