@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.
- package/CHANGELOG.md +22 -2
- package/README.md +27 -27
- package/extensions/completion/index.ts +465 -206
- package/package.json +1 -1
- package/scripts/active-slice-contract-test.sh +45 -1
- package/scripts/canonical-evidence-artifact-test.sh +45 -1
- package/scripts/context-proposal-test.sh +462 -131
- package/scripts/refocus-test.sh +260 -28
- package/scripts/release-check.sh +63 -1
- package/scripts/smoke-test.sh +88 -2
|
@@ -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
|
|
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 =
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
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
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
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[]
|
|
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
|
|
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
|
|
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
|
|
1261
|
+
const raw = await runContextProposalAnalystSubprocess(ctx, projectName, recentEntries);
|
|
1071
1262
|
if (!raw) return undefined;
|
|
1072
|
-
return parseContextProposalAnalystOutput(raw, projectName
|
|
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
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
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
|
|
1721
|
+
return (await analyzeContextProposalWithAgent(ctx, projectName, recentEntries)) ??
|
|
1722
|
+
extractContextProposalFromStructuredSession(recentEntries, projectName);
|
|
1453
1723
|
}
|
|
1454
1724
|
|
|
1455
|
-
async function
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2073
|
+
proposal: ContextProposal,
|
|
2074
|
+
options: ExistingWorkflowChooserOptions = {},
|
|
1761
2075
|
): Promise<ExistingWorkflowDecision | undefined> {
|
|
1762
2076
|
const currentMission = currentMissionAnchor(snapshot);
|
|
1763
|
-
const
|
|
1764
|
-
const
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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: "
|
|
4071
|
+
description: "Discussion-driven /cook workflow: start, continue, refocus, or start the next round",
|
|
3756
4072
|
handler: async (args, ctx) => {
|
|
3757
|
-
const
|
|
3758
|
-
|
|
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
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
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
|
|
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
|
|
3851
|
-
|
|
3852
|
-
|
|
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
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
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
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
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,
|