@linimin/pi-letscook 0.1.35 → 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.
- package/CHANGELOG.md +10 -0
- package/README.md +42 -87
- package/extensions/completion/index.ts +395 -73
- package/package.json +1 -1
- package/scripts/context-proposal-test.sh +153 -13
- package/scripts/refocus-test.sh +197 -10
- package/scripts/release-check.sh +35 -1
- package/scripts/smoke-test.sh +15 -1
|
@@ -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 =
|
|
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 ?? []), ...
|
|
1469
|
-
const acceptance = uniqueProposalItems([...(explicit?.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
|
|
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
|
-
|
|
2053
|
+
proposal: ContextProposal,
|
|
2054
|
+
options: ExistingWorkflowChooserOptions = {},
|
|
1761
2055
|
): Promise<ExistingWorkflowDecision | undefined> {
|
|
1762
2056
|
const currentMission = currentMissionAnchor(snapshot);
|
|
1763
|
-
const
|
|
1764
|
-
const
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
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
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
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
|
|
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
|
|
3890
|
-
if (!
|
|
3891
|
-
emitCommandText(ctx,
|
|
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
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
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
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
proposalDecision.
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
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.
|
|
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,
|