@linimin/pi-letscook 0.1.37 → 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 +18 -2
- package/README.md +9 -15
- package/extensions/completion/index.ts +166 -229
- 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 +324 -133
- package/scripts/refocus-test.sh +76 -31
- package/scripts/release-check.sh +39 -11
- package/scripts/smoke-test.sh +73 -1
|
@@ -467,7 +467,7 @@ type ActiveWorkflowProposalAssessment = {
|
|
|
467
467
|
action: "continue" | "refocus" | "unclear";
|
|
468
468
|
currentMissionAnchor: string;
|
|
469
469
|
proposal?: ContextProposal;
|
|
470
|
-
reason: "matching_mission" | "clear_refocus" | "
|
|
470
|
+
reason: "matching_mission" | "clear_refocus" | "missing_proposal" | "ambiguous_discussion";
|
|
471
471
|
};
|
|
472
472
|
|
|
473
473
|
type ExistingWorkflowChooserOptions = {
|
|
@@ -539,8 +539,10 @@ function maybeWriteTestSnapshot(targetPath: string | undefined, content: string)
|
|
|
539
539
|
}
|
|
540
540
|
|
|
541
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.";
|
|
542
544
|
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
|
|
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.";
|
|
544
546
|
|
|
545
547
|
function buildCookCancellationMessage(prefix: string): string {
|
|
546
548
|
return `${prefix}. ${COOK_MAIN_CHAT_RERUN_GUIDANCE}`;
|
|
@@ -753,6 +755,88 @@ function matchContextProposalRoutingHint(
|
|
|
753
755
|
return label === "tasktype" ? { field: "taskType", value } : { field: "evaluationProfile", value };
|
|
754
756
|
}
|
|
755
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
|
+
|
|
756
840
|
const MISSION_SCOPE_FILTER_STOPWORDS = new Set([
|
|
757
841
|
"a",
|
|
758
842
|
"an",
|
|
@@ -887,19 +971,14 @@ function shouldTreatBareActiveWorkflowProposalAsClearRefocus(proposal: ContextPr
|
|
|
887
971
|
);
|
|
888
972
|
}
|
|
889
973
|
|
|
890
|
-
function maybeWriteActiveWorkflowRoutingSnapshot(
|
|
891
|
-
assessment: ActiveWorkflowProposalAssessment,
|
|
892
|
-
options: { mode: "bare" | "explicit"; explicitGoal?: string },
|
|
893
|
-
): void {
|
|
974
|
+
function maybeWriteActiveWorkflowRoutingSnapshot(assessment: ActiveWorkflowProposalAssessment): void {
|
|
894
975
|
const snapshotPath = completionTestActiveWorkflowRoutingSnapshotPath();
|
|
895
976
|
if (!snapshotPath) return;
|
|
896
977
|
maybeWriteTestSnapshot(
|
|
897
978
|
snapshotPath,
|
|
898
979
|
`${JSON.stringify(
|
|
899
980
|
{
|
|
900
|
-
mode:
|
|
901
|
-
explicitGoalProvided: Boolean(options.explicitGoal),
|
|
902
|
-
explicitGoal: options.explicitGoal ?? null,
|
|
981
|
+
mode: "bare",
|
|
903
982
|
action: assessment.action,
|
|
904
983
|
reason: assessment.reason,
|
|
905
984
|
currentMissionAnchor: assessment.currentMissionAnchor,
|
|
@@ -928,7 +1007,6 @@ const CONTEXT_PROPOSAL_ANALYST_SYSTEM_PROMPT = [
|
|
|
928
1007
|
"risks must contain concrete failure modes or regressions that the later workflow should keep in view.",
|
|
929
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.",
|
|
930
1009
|
"possible_noise should list discussion points that look stale, weakly related, or unsafe to promote into scope.",
|
|
931
|
-
"When an explicit goal is provided, keep the mission anchored to that goal instead of replacing it with a broader or different mission.",
|
|
932
1010
|
"When discussion is insufficient, prefer empty arrays and a low confidence value over invention.",
|
|
933
1011
|
].join(" ");
|
|
934
1012
|
|
|
@@ -983,11 +1061,7 @@ function extractJsonObjectFromText(text: string): string | undefined {
|
|
|
983
1061
|
return unfenced.slice(start, end + 1);
|
|
984
1062
|
}
|
|
985
1063
|
|
|
986
|
-
function parseContextProposalAnalystOutput(
|
|
987
|
-
raw: string,
|
|
988
|
-
projectName: string,
|
|
989
|
-
explicitGoal?: string,
|
|
990
|
-
): ContextProposal | undefined {
|
|
1064
|
+
function parseContextProposalAnalystOutput(raw: string, projectName: string): ContextProposal | undefined {
|
|
991
1065
|
const jsonText = extractJsonObjectFromText(raw);
|
|
992
1066
|
if (!jsonText) return undefined;
|
|
993
1067
|
let parsed: unknown;
|
|
@@ -997,8 +1071,7 @@ function parseContextProposalAnalystOutput(
|
|
|
997
1071
|
return undefined;
|
|
998
1072
|
}
|
|
999
1073
|
if (!isRecord(parsed)) return undefined;
|
|
1000
|
-
const
|
|
1001
|
-
const missionSource = explicit?.mission ?? explicitGoal ?? asString(parsed.mission);
|
|
1074
|
+
const missionSource = asString(parsed.mission);
|
|
1002
1075
|
if (!missionSource) return undefined;
|
|
1003
1076
|
const assessment = assessMissionAnchor(missionSource, projectName);
|
|
1004
1077
|
const normalizedMission = normalizeMissionAnchorText(missionSource);
|
|
@@ -1007,31 +1080,28 @@ function parseContextProposalAnalystOutput(
|
|
|
1007
1080
|
const scope = uniqueProposalItems(asStringArray(parsed.scope));
|
|
1008
1081
|
const constraints = uniqueProposalItems(asStringArray(parsed.constraints));
|
|
1009
1082
|
const acceptance = uniqueProposalItems(asStringArray(parsed.acceptance));
|
|
1010
|
-
const analysis =
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
possibleNoise: asStringArray(parsed.possible_noise ?? parsed.possibleNoise),
|
|
1019
|
-
hintTexts: [raw, mission, ...scope, ...constraints, ...acceptance],
|
|
1020
|
-
}),
|
|
1021
|
-
],
|
|
1022
|
-
[explicitGoal ?? "", raw, mission, ...scope, ...constraints, ...acceptance],
|
|
1023
|
-
);
|
|
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
|
+
});
|
|
1024
1091
|
const goalText = buildContextProposalGoalText({ mission, scope, constraints, acceptance });
|
|
1025
|
-
return
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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
|
+
);
|
|
1035
1105
|
}
|
|
1036
1106
|
|
|
1037
1107
|
function contextProposalAnalystModelArg(model: unknown): string | undefined {
|
|
@@ -1041,17 +1111,9 @@ function contextProposalAnalystModelArg(model: unknown): string | undefined {
|
|
|
1041
1111
|
return provider && id ? `${provider}/${id}` : undefined;
|
|
1042
1112
|
}
|
|
1043
1113
|
|
|
1044
|
-
function buildContextProposalAnalystPrompt(projectName: string, recentEntries: RecentDiscussionEntry[]
|
|
1114
|
+
function buildContextProposalAnalystPrompt(projectName: string, recentEntries: RecentDiscussionEntry[]): string {
|
|
1045
1115
|
const discussion = serializeRecentDiscussionEntries(recentEntries);
|
|
1046
|
-
return [
|
|
1047
|
-
`Project: ${projectName}`,
|
|
1048
|
-
explicitGoal
|
|
1049
|
-
? `Explicit goal (keep this mission anchor):\n${explicitGoal}`
|
|
1050
|
-
: "Explicit goal: none provided; infer the current mission from the discussion.",
|
|
1051
|
-
"",
|
|
1052
|
-
"Recent discussion:",
|
|
1053
|
-
discussion,
|
|
1054
|
-
].join("\n");
|
|
1116
|
+
return [`Project: ${projectName}`, "Infer the current mission from the discussion.", "", "Recent discussion:", discussion].join("\n");
|
|
1055
1117
|
}
|
|
1056
1118
|
|
|
1057
1119
|
function contextProposalAnalystProgressLines(activity: LiveRoleActivity): string[] {
|
|
@@ -1080,14 +1142,13 @@ async function runContextProposalAnalystSubprocess(
|
|
|
1080
1142
|
ctx: { cwd: string; hasUI: boolean; ui: any; model?: any },
|
|
1081
1143
|
projectName: string,
|
|
1082
1144
|
recentEntries: RecentDiscussionEntry[],
|
|
1083
|
-
explicitGoal?: string,
|
|
1084
1145
|
): Promise<string | undefined> {
|
|
1085
1146
|
const modelArg = contextProposalAnalystModelArg(ctx.model);
|
|
1086
1147
|
if (!modelArg) return undefined;
|
|
1087
1148
|
const cwd = getCtxCwd(ctx);
|
|
1088
1149
|
const runCwd = findCompletionRoot(cwd) ?? findRepoRoot(cwd) ?? cwd;
|
|
1089
1150
|
const rootKey = completionRootKey(undefined, cwd);
|
|
1090
|
-
const prompt = buildContextProposalAnalystPrompt(projectName, recentEntries
|
|
1151
|
+
const prompt = buildContextProposalAnalystPrompt(projectName, recentEntries);
|
|
1091
1152
|
const systemPromptTemp = await writeTempFile("pi-cook-proposal-analyst-", CONTEXT_PROPOSAL_ANALYST_SYSTEM_PROMPT);
|
|
1092
1153
|
const analystRole = "cook-proposal-analyst";
|
|
1093
1154
|
const args: string[] = ["--mode", "json", "-p", "--no-session", "--append-system-prompt", systemPromptTemp.filePath, "--model", modelArg, prompt];
|
|
@@ -1189,18 +1250,17 @@ async function analyzeContextProposalWithAgent(
|
|
|
1189
1250
|
ctx: { cwd: string; hasUI: boolean; ui: any; model?: any; modelRegistry?: any },
|
|
1190
1251
|
projectName: string,
|
|
1191
1252
|
recentEntries: RecentDiscussionEntry[],
|
|
1192
|
-
explicitGoal?: string,
|
|
1193
1253
|
): Promise<ContextProposal | undefined> {
|
|
1194
1254
|
if (shouldDisableContextProposalAnalyst()) return undefined;
|
|
1195
1255
|
const testOutput = completionTestContextProposalAnalystOutput();
|
|
1196
1256
|
if (testOutput) {
|
|
1197
|
-
return parseContextProposalAnalystOutput(testOutput, projectName
|
|
1257
|
+
return parseContextProposalAnalystOutput(testOutput, projectName);
|
|
1198
1258
|
}
|
|
1199
1259
|
if (recentEntries.length === 0) return undefined;
|
|
1200
1260
|
try {
|
|
1201
|
-
const raw = await runContextProposalAnalystSubprocess(ctx, projectName, recentEntries
|
|
1261
|
+
const raw = await runContextProposalAnalystSubprocess(ctx, projectName, recentEntries);
|
|
1202
1262
|
if (!raw) return undefined;
|
|
1203
|
-
return parseContextProposalAnalystOutput(raw, projectName
|
|
1263
|
+
return parseContextProposalAnalystOutput(raw, projectName);
|
|
1204
1264
|
} catch (error) {
|
|
1205
1265
|
console.warn("[completion] context proposal analyst failed", error);
|
|
1206
1266
|
return undefined;
|
|
@@ -1562,16 +1622,19 @@ function parseContextProposal(text: string, projectName: string): ContextProposa
|
|
|
1562
1622
|
hintTexts: [cleaned, mission, ...scope, ...constraints, ...acceptance, ...critique, ...risks],
|
|
1563
1623
|
});
|
|
1564
1624
|
const goalText = buildContextProposalGoalText({ mission, scope, constraints, acceptance });
|
|
1565
|
-
return
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
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
|
+
);
|
|
1575
1638
|
}
|
|
1576
1639
|
|
|
1577
1640
|
function hasStructuredContextProposalSignal(text: string): boolean {
|
|
@@ -1627,7 +1690,12 @@ function parseStrictStructuredSessionProposal(text: string, projectName: string)
|
|
|
1627
1690
|
|
|
1628
1691
|
const proposal = parseContextProposal(cleaned, projectName);
|
|
1629
1692
|
if (!proposal) return undefined;
|
|
1630
|
-
if (
|
|
1693
|
+
if (
|
|
1694
|
+
normalizeMissionAnchorText(proposal.mission) !== distinctMissionAnchors[0] &&
|
|
1695
|
+
!isGenericPlanningMissionAnchor(distinctMissionAnchors[0])
|
|
1696
|
+
) {
|
|
1697
|
+
return undefined;
|
|
1698
|
+
}
|
|
1631
1699
|
if (proposal.scope.length === 0 || proposal.constraints.length === 0 || proposal.acceptance.length === 0) return undefined;
|
|
1632
1700
|
return { ...proposal, source: "session" };
|
|
1633
1701
|
}
|
|
@@ -1648,58 +1716,17 @@ function extractContextProposalFromStructuredSession(
|
|
|
1648
1716
|
async function extractContextProposalFromSession(
|
|
1649
1717
|
ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
|
|
1650
1718
|
projectName: string,
|
|
1651
|
-
explicitGoal?: string,
|
|
1652
1719
|
): Promise<ContextProposal | undefined> {
|
|
1653
1720
|
const recentEntries = collectRecentDiscussionEntries(ctx);
|
|
1654
|
-
return (await analyzeContextProposalWithAgent(ctx, projectName, recentEntries
|
|
1721
|
+
return (await analyzeContextProposalWithAgent(ctx, projectName, recentEntries)) ??
|
|
1655
1722
|
extractContextProposalFromStructuredSession(recentEntries, projectName);
|
|
1656
1723
|
}
|
|
1657
1724
|
|
|
1658
|
-
async function buildGoalAnchoredContextProposal(
|
|
1659
|
-
ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
|
|
1660
|
-
goal: string,
|
|
1661
|
-
projectName: string,
|
|
1662
|
-
): Promise<ContextProposal> {
|
|
1663
|
-
const explicit = parseContextProposal(goal, projectName);
|
|
1664
|
-
const sessionProposal = await extractContextProposalFromSession(ctx, projectName, goal);
|
|
1665
|
-
const missionSource = explicit?.mission ?? goal;
|
|
1666
|
-
const assessment = assessMissionAnchor(missionSource, projectName);
|
|
1667
|
-
const mission = assessment.derived;
|
|
1668
|
-
const mergeSessionBody = sessionProposal?.source === "analyst";
|
|
1669
|
-
const explicitScope = explicit?.scope ?? [];
|
|
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 ?? [] : [];
|
|
1675
|
-
const scope = uniqueProposalItems([...explicitScope, ...sessionScope]);
|
|
1676
|
-
const constraints = uniqueProposalItems([...(explicit?.constraints ?? []), ...sessionConstraints]);
|
|
1677
|
-
const acceptance = uniqueProposalItems([...(explicit?.acceptance ?? []), ...sessionAcceptance]);
|
|
1678
|
-
const analysis = mergeContextProposalAnalysis(
|
|
1679
|
-
[explicit?.analysis, sessionProposal?.analysis],
|
|
1680
|
-
[goal, mission, ...(sessionProposal?.analysis.possibleNoise ?? []), ...scope, ...constraints, ...acceptance],
|
|
1681
|
-
);
|
|
1682
|
-
const goalText = buildContextProposalGoalText({ mission, scope, constraints, acceptance });
|
|
1683
|
-
return {
|
|
1684
|
-
mission,
|
|
1685
|
-
scope,
|
|
1686
|
-
constraints,
|
|
1687
|
-
acceptance,
|
|
1688
|
-
analysis,
|
|
1689
|
-
goalText,
|
|
1690
|
-
basisPreview: sessionProposal?.basisPreview ?? explicit?.basisPreview ?? goal,
|
|
1691
|
-
source: sessionProposal?.source ?? explicit?.source ?? "session",
|
|
1692
|
-
};
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
1725
|
async function deriveCookContextProposal(
|
|
1696
1726
|
ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
|
|
1697
1727
|
projectName: string,
|
|
1698
|
-
explicitGoal?: string,
|
|
1699
1728
|
): Promise<ContextProposal | undefined> {
|
|
1700
|
-
return
|
|
1701
|
-
? await buildGoalAnchoredContextProposal(ctx, explicitGoal, projectName)
|
|
1702
|
-
: await extractContextProposalFromSession(ctx, projectName);
|
|
1729
|
+
return await extractContextProposalFromSession(ctx, projectName);
|
|
1703
1730
|
}
|
|
1704
1731
|
|
|
1705
1732
|
async function confirmContextProposal(
|
|
@@ -1975,44 +2002,37 @@ function buildEvaluationRoleReminderText(snapshot: CompletionStateSnapshot, role
|
|
|
1975
2002
|
async function assessActiveWorkflowProposalRouting(
|
|
1976
2003
|
ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
|
|
1977
2004
|
snapshot: CompletionStateSnapshot,
|
|
1978
|
-
explicitGoal?: string,
|
|
1979
2005
|
): Promise<ActiveWorkflowProposalAssessment> {
|
|
1980
2006
|
const currentMission = currentMissionAnchor(snapshot);
|
|
1981
2007
|
const projectName = path.basename(snapshot.files.root);
|
|
1982
|
-
const proposal =
|
|
1983
|
-
? await deriveCookContextProposal(ctx, projectName, explicitGoal)
|
|
1984
|
-
: await deriveCookContextProposal(ctx, projectName);
|
|
1985
|
-
const mode = explicitGoal ? "explicit" : "bare";
|
|
2008
|
+
const proposal = await deriveCookContextProposal(ctx, projectName);
|
|
1986
2009
|
if (!proposal) {
|
|
1987
2010
|
const assessment: ActiveWorkflowProposalAssessment = {
|
|
1988
2011
|
action: "unclear",
|
|
1989
2012
|
currentMissionAnchor: currentMission,
|
|
1990
2013
|
reason: "missing_proposal",
|
|
1991
2014
|
};
|
|
1992
|
-
maybeWriteActiveWorkflowRoutingSnapshot(assessment
|
|
2015
|
+
maybeWriteActiveWorkflowRoutingSnapshot(assessment);
|
|
1993
2016
|
return assessment;
|
|
1994
2017
|
}
|
|
1995
|
-
|
|
1996
|
-
? missionAnchorsStrictlyEquivalent(currentMission, proposal.mission)
|
|
1997
|
-
: missionAnchorsLikelyEquivalent(currentMission, proposal.mission);
|
|
1998
|
-
if (missionsMatch) {
|
|
2018
|
+
if (missionAnchorsLikelyEquivalent(currentMission, proposal.mission)) {
|
|
1999
2019
|
const assessment: ActiveWorkflowProposalAssessment = {
|
|
2000
2020
|
action: "continue",
|
|
2001
2021
|
currentMissionAnchor: currentMission,
|
|
2002
2022
|
proposal,
|
|
2003
2023
|
reason: "matching_mission",
|
|
2004
2024
|
};
|
|
2005
|
-
maybeWriteActiveWorkflowRoutingSnapshot(assessment
|
|
2025
|
+
maybeWriteActiveWorkflowRoutingSnapshot(assessment);
|
|
2006
2026
|
return assessment;
|
|
2007
2027
|
}
|
|
2008
|
-
if (
|
|
2028
|
+
if (shouldTreatBareActiveWorkflowProposalAsClearRefocus(proposal)) {
|
|
2009
2029
|
const assessment: ActiveWorkflowProposalAssessment = {
|
|
2010
2030
|
action: "refocus",
|
|
2011
2031
|
currentMissionAnchor: currentMission,
|
|
2012
2032
|
proposal,
|
|
2013
|
-
reason:
|
|
2033
|
+
reason: "clear_refocus",
|
|
2014
2034
|
};
|
|
2015
|
-
maybeWriteActiveWorkflowRoutingSnapshot(assessment
|
|
2035
|
+
maybeWriteActiveWorkflowRoutingSnapshot(assessment);
|
|
2016
2036
|
return assessment;
|
|
2017
2037
|
}
|
|
2018
2038
|
const assessment: ActiveWorkflowProposalAssessment = {
|
|
@@ -2021,7 +2041,7 @@ async function assessActiveWorkflowProposalRouting(
|
|
|
2021
2041
|
proposal,
|
|
2022
2042
|
reason: "ambiguous_discussion",
|
|
2023
2043
|
};
|
|
2024
|
-
maybeWriteActiveWorkflowRoutingSnapshot(assessment
|
|
2044
|
+
maybeWriteActiveWorkflowRoutingSnapshot(assessment);
|
|
2025
2045
|
return assessment;
|
|
2026
2046
|
}
|
|
2027
2047
|
|
|
@@ -4050,11 +4070,14 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
4050
4070
|
pi.registerCommand("cook", {
|
|
4051
4071
|
description: "Discussion-driven /cook workflow: start, continue, refocus, or start the next round",
|
|
4052
4072
|
handler: async (args, ctx) => {
|
|
4053
|
-
const
|
|
4054
|
-
|
|
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;
|
|
4055
4079
|
const cwd = getCtxCwd(ctx);
|
|
4056
4080
|
let snapshot = await loadCompletionSnapshot(cwd);
|
|
4057
|
-
const hadSnapshot = Boolean(snapshot);
|
|
4058
4081
|
const workflowDone = isWorkflowDone(snapshot);
|
|
4059
4082
|
let kickoffIntent: "auto" | "continue" | "refocus" = "auto";
|
|
4060
4083
|
let kickoffMissionAnchor = snapshot ? currentMissionAnchor(snapshot) : undefined;
|
|
@@ -4063,40 +4086,21 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
4063
4086
|
if (!snapshot) {
|
|
4064
4087
|
const root = findRepoRoot(cwd) ?? cwd;
|
|
4065
4088
|
const projectName = path.basename(root);
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
return;
|
|
4078
|
-
}
|
|
4079
|
-
goal = decision.goalText;
|
|
4080
|
-
kickoffMissionAnchor = decision.missionAnchor;
|
|
4081
|
-
kickoffAnalysis = decision.analysis;
|
|
4082
|
-
} else {
|
|
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
|
-
}
|
|
4088
|
-
const decision = await confirmContextProposal(ctx, proposal, {
|
|
4089
|
-
title: "Start a completion workflow from this goal?",
|
|
4090
|
-
nonInteractiveBehavior: "accept",
|
|
4091
|
-
});
|
|
4092
|
-
if (!decision) {
|
|
4093
|
-
emitCommandText(ctx, buildCookCancellationMessage("Cancelled workflow startup proposal"), "info");
|
|
4094
|
-
return;
|
|
4095
|
-
}
|
|
4096
|
-
goal = decision.goalText;
|
|
4097
|
-
kickoffMissionAnchor = decision.missionAnchor;
|
|
4098
|
-
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;
|
|
4099
4100
|
}
|
|
4101
|
+
goal = decision.goalText;
|
|
4102
|
+
kickoffMissionAnchor = decision.missionAnchor;
|
|
4103
|
+
kickoffAnalysis = decision.analysis;
|
|
4100
4104
|
const startupRouting = finalizeContextProposalAnalysis(kickoffAnalysis, [goal ?? kickoffMissionAnchor ?? projectName]);
|
|
4101
4105
|
const created = await scaffoldCompletionFiles(root, kickoffMissionAnchor ?? projectName, {
|
|
4102
4106
|
analysis: startupRouting,
|
|
@@ -4174,73 +4178,6 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
4174
4178
|
}
|
|
4175
4179
|
}
|
|
4176
4180
|
kickoffMissionAnchor = kickoffMissionAnchor ?? currentMissionAnchor(snapshot);
|
|
4177
|
-
if (hadSnapshot && explicitGoal) {
|
|
4178
|
-
if (workflowDone) {
|
|
4179
|
-
const projectName = path.basename(snapshot.files.root);
|
|
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
|
-
}
|
|
4185
|
-
const decision = await confirmContextProposal(ctx, proposal, {
|
|
4186
|
-
title: "Start the next workflow round from this goal?",
|
|
4187
|
-
nonInteractiveBehavior: "accept",
|
|
4188
|
-
});
|
|
4189
|
-
if (!decision) {
|
|
4190
|
-
emitCommandText(ctx, buildCookCancellationMessage("Cancelled next workflow round proposal"), "info");
|
|
4191
|
-
return;
|
|
4192
|
-
}
|
|
4193
|
-
goal = decision.goalText;
|
|
4194
|
-
kickoffIntent = "refocus";
|
|
4195
|
-
kickoffMissionAnchor = decision.missionAnchor;
|
|
4196
|
-
await refocusCompletionMission(snapshot, decision.missionAnchor, decision.goalText, decision.analysis);
|
|
4197
|
-
snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
|
|
4198
|
-
emitCommandText(ctx, `Started a new completion workflow round from explicit goal: ${decision.missionAnchor}`, "info");
|
|
4199
|
-
} else {
|
|
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");
|
|
4203
|
-
return;
|
|
4204
|
-
}
|
|
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");
|
|
4215
|
-
return;
|
|
4216
|
-
}
|
|
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
|
-
}
|
|
4241
|
-
}
|
|
4242
|
-
}
|
|
4243
|
-
}
|
|
4244
4181
|
pi.setSessionName(`completion: ${kickoffMissionAnchor.slice(0, 60)}`);
|
|
4245
4182
|
const kickoffPrompt = completionKickoff(
|
|
4246
4183
|
goal,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linimin/pi-letscook",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.39",
|
|
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,
|
|
@@ -5,6 +5,45 @@ PKG_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
5
5
|
TMPDIR="$(mktemp -d)"
|
|
6
6
|
trap 'rm -rf "$TMPDIR"' EXIT
|
|
7
7
|
|
|
8
|
+
write_session() {
|
|
9
|
+
local session_path="$1"
|
|
10
|
+
local cwd="$2"
|
|
11
|
+
local text="$3"
|
|
12
|
+
python3 - "$session_path" "$cwd" "$text" <<'PY'
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
session_path = Path(sys.argv[1])
|
|
18
|
+
cwd = sys.argv[2]
|
|
19
|
+
text = sys.argv[3]
|
|
20
|
+
session_path.parent.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
entries = [
|
|
22
|
+
{
|
|
23
|
+
"type": "session",
|
|
24
|
+
"version": 3,
|
|
25
|
+
"id": "11111111-1111-4111-8111-111111111111",
|
|
26
|
+
"timestamp": "2026-01-01T00:00:00.000Z",
|
|
27
|
+
"cwd": cwd,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"type": "message",
|
|
31
|
+
"id": "a1b2c3d4",
|
|
32
|
+
"parentId": None,
|
|
33
|
+
"timestamp": "2026-01-01T00:00:01.000Z",
|
|
34
|
+
"message": {
|
|
35
|
+
"role": "user",
|
|
36
|
+
"content": text,
|
|
37
|
+
"timestamp": 1767225601000,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
]
|
|
41
|
+
with session_path.open('w', encoding='utf-8') as fh:
|
|
42
|
+
for entry in entries:
|
|
43
|
+
fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
44
|
+
PY
|
|
45
|
+
}
|
|
46
|
+
|
|
8
47
|
cd "$PKG_ROOT"
|
|
9
48
|
|
|
10
49
|
node <<'NODE'
|
|
@@ -46,12 +85,17 @@ NODE
|
|
|
46
85
|
|
|
47
86
|
ROOT="$TMPDIR/repo"
|
|
48
87
|
PROMPT="$TMPDIR/resume-prompt.txt"
|
|
88
|
+
BOOTSTRAP_SESSION="$TMPDIR/session-active-slice-bootstrap.jsonl"
|
|
89
|
+
BOOTSTRAP_DISCUSSION=$'Mission: Exercise active-slice contract parity.\nScope:\n- Bootstrap canonical completion files for the active-slice contract fixture.\nConstraints:\n- Use supported bare /cook startup only.\nAcceptance:\n- Materialize canonical files before the fixture rewrites them.'
|
|
49
90
|
mkdir -p "$ROOT"
|
|
50
91
|
cd "$ROOT"
|
|
51
92
|
git init -q
|
|
93
|
+
write_session "$BOOTSTRAP_SESSION" "$ROOT" "$BOOTSTRAP_DISCUSSION"
|
|
52
94
|
|
|
95
|
+
PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
|
|
96
|
+
PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
|
|
53
97
|
PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
|
|
54
|
-
pi -e "$PKG_ROOT" -p "/cook
|
|
98
|
+
pi --session "$BOOTSTRAP_SESSION" -e "$PKG_ROOT" -p "/cook" \
|
|
55
99
|
>"$TMPDIR/pi-active-slice-bootstrap.out" 2>"$TMPDIR/pi-active-slice-bootstrap.err"
|
|
56
100
|
|
|
57
101
|
python3 - <<'PY'
|