@linimin/pi-letscook 0.1.27 → 0.1.29
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 +14 -0
- package/README.md +114 -118
- package/extensions/completion/index.ts +588 -89
- package/package.json +1 -1
- package/scripts/context-proposal-test.sh +180 -16
|
@@ -5,8 +5,8 @@ import * as os from "node:os";
|
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { StringEnum } from "@mariozechner/pi-ai";
|
|
7
7
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
8
|
-
import { parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
9
|
-
import { Text } from "@mariozechner/pi-tui";
|
|
8
|
+
import { DynamicBorder, parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { Container, matchesKey, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
|
|
10
10
|
import { Type } from "typebox";
|
|
11
11
|
|
|
12
12
|
const PROTOCOL_ID = "completion";
|
|
@@ -115,7 +115,12 @@ type ContextProposal = {
|
|
|
115
115
|
acceptance: string[];
|
|
116
116
|
goalText: string;
|
|
117
117
|
basisPreview: string;
|
|
118
|
-
source: "session";
|
|
118
|
+
source: "session" | "analyst";
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
type RecentDiscussionEntry = {
|
|
122
|
+
role: "user" | "assistant" | "custom" | "summary";
|
|
123
|
+
text: string;
|
|
119
124
|
};
|
|
120
125
|
|
|
121
126
|
type ContextProposalDecision = {
|
|
@@ -123,12 +128,77 @@ type ContextProposalDecision = {
|
|
|
123
128
|
goalText: string;
|
|
124
129
|
};
|
|
125
130
|
|
|
131
|
+
type ContextProposalConfirmAction = "start" | "edit" | "cancel";
|
|
132
|
+
|
|
133
|
+
type ContextProposalConfirmationActionItem = {
|
|
134
|
+
id: ContextProposalConfirmAction;
|
|
135
|
+
label: string;
|
|
136
|
+
description: string;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
type ContextProposalConfirmationLayout = {
|
|
140
|
+
title: string;
|
|
141
|
+
intro: string;
|
|
142
|
+
proposalHeading: string;
|
|
143
|
+
proposalBody: string;
|
|
144
|
+
actionsHeading: string;
|
|
145
|
+
actions: ContextProposalConfirmationActionItem[];
|
|
146
|
+
footer: string;
|
|
147
|
+
};
|
|
148
|
+
|
|
126
149
|
type ContextProposalConfirmOptions = {
|
|
127
150
|
title: string;
|
|
128
151
|
nonInteractiveBehavior?: "accept" | "cancel";
|
|
129
152
|
editorPrompt?: string;
|
|
130
153
|
};
|
|
131
154
|
|
|
155
|
+
class StartupAnalystOverlay extends Container {
|
|
156
|
+
private readonly border: DynamicBorder;
|
|
157
|
+
private readonly title: Text;
|
|
158
|
+
private readonly body: Text;
|
|
159
|
+
private readonly footer: Text;
|
|
160
|
+
private lines: string[] = [];
|
|
161
|
+
onAbort?: () => void;
|
|
162
|
+
|
|
163
|
+
constructor(private readonly theme: any) {
|
|
164
|
+
super();
|
|
165
|
+
this.border = new DynamicBorder((s: string) => this.theme.fg("accent", s));
|
|
166
|
+
this.title = new Text("", 1, 0);
|
|
167
|
+
this.body = new Text("", 1, 1);
|
|
168
|
+
this.footer = new Text("", 1, 0);
|
|
169
|
+
this.addChild(this.border);
|
|
170
|
+
this.addChild(this.title);
|
|
171
|
+
this.addChild(this.body);
|
|
172
|
+
this.addChild(this.footer);
|
|
173
|
+
this.updateDisplay();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
setLines(lines: string[]): void {
|
|
177
|
+
this.lines = [...lines];
|
|
178
|
+
this.updateDisplay();
|
|
179
|
+
this.invalidate();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private updateDisplay(): void {
|
|
183
|
+
this.title.setText(this.theme.fg("accent", this.theme.bold("/cook proposal analyst")));
|
|
184
|
+
this.body.setText(this.theme.fg("dim", this.lines.join("\n")));
|
|
185
|
+
this.footer.setText(this.theme.fg("muted", "Esc cancel • This analysis runs before /cook writes canonical workflow state"));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
override handleInput(data: string): void {
|
|
189
|
+
if (data === "\u001b") {
|
|
190
|
+
this.onAbort?.();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
// Container does not implement handleInput; ignore all other keys.
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
override invalidate(): void {
|
|
197
|
+
super.invalidate();
|
|
198
|
+
this.updateDisplay();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
132
202
|
const liveRoleActivityByRoot = new Map<string, LiveRoleActivity>();
|
|
133
203
|
const LIVE_ROLE_WAITING_MS = 15_000;
|
|
134
204
|
const LIVE_ROLE_STALLED_MS = 45_000;
|
|
@@ -420,6 +490,23 @@ function completionTestContextProposalEditText(): string | undefined {
|
|
|
420
490
|
return asString(process.env.PI_COMPLETION_CONTEXT_PROPOSAL_EDIT_TEXT);
|
|
421
491
|
}
|
|
422
492
|
|
|
493
|
+
function completionTestContextProposalUiActionOverride(): ContextProposalConfirmAction | undefined {
|
|
494
|
+
const raw = process.env.PI_COMPLETION_TEST_CONTEXT_PROPOSAL_UI_ACTION?.trim().toLowerCase();
|
|
495
|
+
return raw === "start" || raw === "edit" || raw === "cancel" ? raw : undefined;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function completionTestContextProposalUiSnapshotPath(): string | undefined {
|
|
499
|
+
return asString(process.env.PI_COMPLETION_TEST_CONTEXT_PROPOSAL_UI_PATH);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function shouldDisableContextProposalAnalyst(): boolean {
|
|
503
|
+
return process.env.PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST === "1";
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function completionTestContextProposalAnalystOutput(): string | undefined {
|
|
507
|
+
return asString(process.env.PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT);
|
|
508
|
+
}
|
|
509
|
+
|
|
423
510
|
function isWorkflowDone(snapshot: CompletionStateSnapshot | undefined): boolean {
|
|
424
511
|
return asString(snapshot?.state?.continuation_policy) === "done";
|
|
425
512
|
}
|
|
@@ -508,6 +595,335 @@ function uniqueProposalItems(items: string[]): string[] {
|
|
|
508
595
|
return result;
|
|
509
596
|
}
|
|
510
597
|
|
|
598
|
+
const MISSION_SCOPE_FILTER_STOPWORDS = new Set([
|
|
599
|
+
"a",
|
|
600
|
+
"an",
|
|
601
|
+
"and",
|
|
602
|
+
"are",
|
|
603
|
+
"as",
|
|
604
|
+
"at",
|
|
605
|
+
"be",
|
|
606
|
+
"by",
|
|
607
|
+
"for",
|
|
608
|
+
"from",
|
|
609
|
+
"goal",
|
|
610
|
+
"goals",
|
|
611
|
+
"in",
|
|
612
|
+
"into",
|
|
613
|
+
"is",
|
|
614
|
+
"it",
|
|
615
|
+
"its",
|
|
616
|
+
"mission",
|
|
617
|
+
"of",
|
|
618
|
+
"on",
|
|
619
|
+
"or",
|
|
620
|
+
"scope",
|
|
621
|
+
"that",
|
|
622
|
+
"the",
|
|
623
|
+
"their",
|
|
624
|
+
"this",
|
|
625
|
+
"to",
|
|
626
|
+
"using",
|
|
627
|
+
"with",
|
|
628
|
+
"workflow",
|
|
629
|
+
]);
|
|
630
|
+
|
|
631
|
+
function missionScopeFilterTokens(text: string): string[] {
|
|
632
|
+
const normalized = normalizeProposalLine(text).toLowerCase();
|
|
633
|
+
const tokens = normalized.match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
634
|
+
return tokens.filter((token) => {
|
|
635
|
+
if (/^[\p{Script=Han}]+$/u.test(token)) return token.length >= 2;
|
|
636
|
+
if (token.length < 2) return false;
|
|
637
|
+
return !MISSION_SCOPE_FILTER_STOPWORDS.has(token);
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function isSessionScopeItemMissionRelevant(item: string, mission: string): boolean {
|
|
642
|
+
const normalizedItem = normalizeProposalLine(item).toLowerCase();
|
|
643
|
+
const normalizedMission = normalizeMissionAnchorText(mission).toLowerCase();
|
|
644
|
+
if (!normalizedItem || !normalizedMission) return true;
|
|
645
|
+
if (normalizedItem.includes(normalizedMission) || normalizedMission.includes(normalizedItem)) return true;
|
|
646
|
+
const itemTokens = [...new Set(missionScopeFilterTokens(normalizedItem))];
|
|
647
|
+
const missionTokens = new Set(missionScopeFilterTokens(normalizedMission));
|
|
648
|
+
if (itemTokens.length === 0 || missionTokens.size === 0) return true;
|
|
649
|
+
const overlap = itemTokens.filter((token) => missionTokens.has(token));
|
|
650
|
+
if (overlap.length >= 2) return true;
|
|
651
|
+
return overlap.some((token) => token.length >= 6 || /[\p{Script=Han}]/u.test(token));
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const CONTEXT_PROPOSAL_ANALYST_SYSTEM_PROMPT = [
|
|
655
|
+
"You analyze recent /cook startup discussion and return a strict JSON object.",
|
|
656
|
+
"Do not emit markdown, code fences, or commentary.",
|
|
657
|
+
"Return exactly one JSON object with keys: mission, scope, constraints, acceptance, confidence, possible_noise.",
|
|
658
|
+
"mission must be a concise implementation mission anchor sentence.",
|
|
659
|
+
"scope must contain only work items that directly support the mission.",
|
|
660
|
+
"constraints must contain guardrails or non-goals explicitly stated or strongly implied by the discussion.",
|
|
661
|
+
"acceptance must contain verifiable outcomes explicitly stated or strongly implied by the discussion.",
|
|
662
|
+
"possible_noise should list discussion points that look stale, weakly related, or unsafe to promote into scope.",
|
|
663
|
+
"When an explicit goal is provided, keep the mission anchored to that goal instead of replacing it with a broader or different mission.",
|
|
664
|
+
"When discussion is insufficient, prefer empty arrays and a low confidence value over invention.",
|
|
665
|
+
].join(" ");
|
|
666
|
+
|
|
667
|
+
function collectRecentDiscussionEntries(ctx: { sessionManager: any }, limit = 8): RecentDiscussionEntry[] {
|
|
668
|
+
let branch: any[] = [];
|
|
669
|
+
try {
|
|
670
|
+
branch = ctx.sessionManager?.getBranch?.() ?? [];
|
|
671
|
+
} catch (error) {
|
|
672
|
+
if (isStaleContextError(error)) return [];
|
|
673
|
+
throw error;
|
|
674
|
+
}
|
|
675
|
+
const entries: RecentDiscussionEntry[] = [];
|
|
676
|
+
for (let index = branch.length - 1; index >= 0; index -= 1) {
|
|
677
|
+
const entry = branch[index];
|
|
678
|
+
if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message)) continue;
|
|
679
|
+
const message = entry.message as JsonRecord;
|
|
680
|
+
let text = "";
|
|
681
|
+
let role: RecentDiscussionEntry["role"] | undefined;
|
|
682
|
+
const messageRole = asString(message.role);
|
|
683
|
+
if (messageRole === "user" || messageRole === "assistant" || messageRole === "custom") {
|
|
684
|
+
text = extractTextFromMessageContent(message.content);
|
|
685
|
+
role = messageRole;
|
|
686
|
+
} else if (messageRole === "branchSummary" || messageRole === "compactionSummary") {
|
|
687
|
+
text = asString(message.summary) ?? "";
|
|
688
|
+
role = "summary";
|
|
689
|
+
}
|
|
690
|
+
if (!text || !role) continue;
|
|
691
|
+
const trimmed = text.trim();
|
|
692
|
+
if (!trimmed || /^\/(?:cook|complete)\b/i.test(trimmed)) continue;
|
|
693
|
+
entries.push({ role, text: trimmed });
|
|
694
|
+
if (entries.length >= limit) break;
|
|
695
|
+
}
|
|
696
|
+
return entries;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function serializeRecentDiscussionEntries(entries: RecentDiscussionEntry[]): string {
|
|
700
|
+
return entries
|
|
701
|
+
.slice()
|
|
702
|
+
.reverse()
|
|
703
|
+
.map((entry, index) => `[${index + 1}] ${entry.role.toUpperCase()}\n${entry.text}`)
|
|
704
|
+
.join("\n\n");
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function extractJsonObjectFromText(text: string): string | undefined {
|
|
708
|
+
const trimmed = text.trim();
|
|
709
|
+
if (!trimmed) return undefined;
|
|
710
|
+
const unfenced = trimmed.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
|
|
711
|
+
if (unfenced.startsWith("{") && unfenced.endsWith("}")) return unfenced;
|
|
712
|
+
const start = unfenced.indexOf("{");
|
|
713
|
+
const end = unfenced.lastIndexOf("}");
|
|
714
|
+
if (start < 0 || end <= start) return undefined;
|
|
715
|
+
return unfenced.slice(start, end + 1);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function parseContextProposalAnalystOutput(
|
|
719
|
+
raw: string,
|
|
720
|
+
projectName: string,
|
|
721
|
+
explicitGoal?: string,
|
|
722
|
+
): ContextProposal | undefined {
|
|
723
|
+
const jsonText = extractJsonObjectFromText(raw);
|
|
724
|
+
if (!jsonText) return undefined;
|
|
725
|
+
let parsed: unknown;
|
|
726
|
+
try {
|
|
727
|
+
parsed = JSON.parse(jsonText);
|
|
728
|
+
} catch {
|
|
729
|
+
return undefined;
|
|
730
|
+
}
|
|
731
|
+
if (!isRecord(parsed)) return undefined;
|
|
732
|
+
const explicit = explicitGoal ? parseContextProposal(explicitGoal, projectName) : undefined;
|
|
733
|
+
const missionSource = explicit?.mission ?? explicitGoal ?? asString(parsed.mission);
|
|
734
|
+
if (!missionSource) return undefined;
|
|
735
|
+
const assessment = assessMissionAnchor(missionSource, projectName);
|
|
736
|
+
const normalizedMission = normalizeMissionAnchorText(missionSource);
|
|
737
|
+
if (!normalizedMission || isWeakMissionAnchor(normalizedMission)) return undefined;
|
|
738
|
+
const mission = assessment.derived;
|
|
739
|
+
const scope = uniqueProposalItems(asStringArray(parsed.scope));
|
|
740
|
+
const constraints = uniqueProposalItems(asStringArray(parsed.constraints));
|
|
741
|
+
const acceptance = uniqueProposalItems(asStringArray(parsed.acceptance));
|
|
742
|
+
const goalText = buildContextProposalGoalText({ mission, scope, constraints, acceptance });
|
|
743
|
+
return {
|
|
744
|
+
mission,
|
|
745
|
+
scope,
|
|
746
|
+
constraints,
|
|
747
|
+
acceptance,
|
|
748
|
+
goalText,
|
|
749
|
+
basisPreview: raw.replace(/\s+/g, " ").trim(),
|
|
750
|
+
source: "analyst",
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function contextProposalAnalystModelArg(model: unknown): string | undefined {
|
|
755
|
+
if (!isRecord(model)) return undefined;
|
|
756
|
+
const provider = asString(model.provider);
|
|
757
|
+
const id = asString(model.id);
|
|
758
|
+
return provider && id ? `${provider}/${id}` : undefined;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function buildContextProposalAnalystPrompt(projectName: string, recentEntries: RecentDiscussionEntry[], explicitGoal?: string): string {
|
|
762
|
+
const discussion = serializeRecentDiscussionEntries(recentEntries);
|
|
763
|
+
return [
|
|
764
|
+
`Project: ${projectName}`,
|
|
765
|
+
explicitGoal
|
|
766
|
+
? `Explicit goal (keep this mission anchor):\n${explicitGoal}`
|
|
767
|
+
: "Explicit goal: none provided; infer the current mission from the discussion.",
|
|
768
|
+
"",
|
|
769
|
+
"Recent discussion:",
|
|
770
|
+
discussion,
|
|
771
|
+
].join("\n");
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function contextProposalAnalystProgressLines(activity: LiveRoleActivity): string[] {
|
|
775
|
+
return [
|
|
776
|
+
...buildInlineRunningLines({
|
|
777
|
+
role: activity.role,
|
|
778
|
+
startedAt: activity.startedAt,
|
|
779
|
+
updatedAt: activity.updatedAt,
|
|
780
|
+
currentAction: activity.currentAction,
|
|
781
|
+
toolActivity: activity.toolActivity,
|
|
782
|
+
toolRecentActivity: activity.toolRecentActivity,
|
|
783
|
+
recentActivity: activity.recentActivity,
|
|
784
|
+
assistantSummary: activity.assistantSummary,
|
|
785
|
+
progress: activity.progress,
|
|
786
|
+
rationale: activity.rationale,
|
|
787
|
+
nextStep: activity.nextStep,
|
|
788
|
+
verifying: activity.verifying,
|
|
789
|
+
stateDeltas: activity.stateDeltas,
|
|
790
|
+
}),
|
|
791
|
+
"",
|
|
792
|
+
"This step only prepares a proposal for confirmation.",
|
|
793
|
+
];
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
async function runContextProposalAnalystSubprocess(
|
|
797
|
+
ctx: { cwd: string; hasUI: boolean; ui: any; model?: any },
|
|
798
|
+
projectName: string,
|
|
799
|
+
recentEntries: RecentDiscussionEntry[],
|
|
800
|
+
explicitGoal?: string,
|
|
801
|
+
): Promise<string | undefined> {
|
|
802
|
+
const modelArg = contextProposalAnalystModelArg(ctx.model);
|
|
803
|
+
if (!modelArg) return undefined;
|
|
804
|
+
const cwd = getCtxCwd(ctx);
|
|
805
|
+
const runCwd = findCompletionRoot(cwd) ?? findRepoRoot(cwd) ?? cwd;
|
|
806
|
+
const rootKey = completionRootKey(undefined, cwd);
|
|
807
|
+
const prompt = buildContextProposalAnalystPrompt(projectName, recentEntries, explicitGoal);
|
|
808
|
+
const systemPromptTemp = await writeTempFile("pi-cook-proposal-analyst-", CONTEXT_PROPOSAL_ANALYST_SYSTEM_PROMPT);
|
|
809
|
+
const analystRole = "cook-proposal-analyst";
|
|
810
|
+
const args: string[] = ["--mode", "json", "-p", "--no-session", "--append-system-prompt", systemPromptTemp.filePath, "--model", modelArg, prompt];
|
|
811
|
+
const invocation = getPiInvocation(args);
|
|
812
|
+
const liveActivity = createLiveRoleActivity(analystRole);
|
|
813
|
+
liveActivity.progress = "Analyzing recent discussion";
|
|
814
|
+
liveActivity.currentAction = "Reading recent discussion and preparing a startup proposal";
|
|
815
|
+
liveActivity.assistantSummary = liveActivity.progress;
|
|
816
|
+
liveActivity.recentActivity = pushRecentActivity(liveActivity.recentActivity, `assistant: ${liveActivity.progress}`);
|
|
817
|
+
const messages: RoleMessage[] = [];
|
|
818
|
+
let stderr = "";
|
|
819
|
+
let overlay: StartupAnalystOverlay | undefined;
|
|
820
|
+
let finishOverlay: ((value: string | undefined) => void) | undefined;
|
|
821
|
+
let overlaySettled = false;
|
|
822
|
+
const settleOverlay = (value: string | undefined) => {
|
|
823
|
+
if (overlaySettled) return;
|
|
824
|
+
overlaySettled = true;
|
|
825
|
+
finishOverlay?.(value);
|
|
826
|
+
};
|
|
827
|
+
const updateActivity = (fresh = false) => {
|
|
828
|
+
if (fresh) liveActivity.updatedAt = nowMs();
|
|
829
|
+
liveRoleActivityByRoot.set(rootKey, cloneLiveRoleActivity(liveActivity, { status: "running" }));
|
|
830
|
+
void refreshStatus(ctx);
|
|
831
|
+
overlay?.setLines(contextProposalAnalystProgressLines(liveActivity));
|
|
832
|
+
};
|
|
833
|
+
const heartbeat = setInterval(() => updateActivity(false), LIVE_ROLE_HEARTBEAT_MS);
|
|
834
|
+
const run = async (): Promise<string | undefined> => {
|
|
835
|
+
try {
|
|
836
|
+
updateActivity(true);
|
|
837
|
+
const output = await new Promise<string | undefined>((resolve) => {
|
|
838
|
+
const proc = spawn(invocation.command, invocation.args, {
|
|
839
|
+
cwd: runCwd,
|
|
840
|
+
env: process.env,
|
|
841
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
842
|
+
shell: false,
|
|
843
|
+
});
|
|
844
|
+
let buffer = "";
|
|
845
|
+
const processLine = (line: string) => {
|
|
846
|
+
if (!line.trim()) return;
|
|
847
|
+
try {
|
|
848
|
+
const event = JSON.parse(line) as JsonRecord;
|
|
849
|
+
if (applyLiveRoleEvent(liveActivity, event, messages)) updateActivity(true);
|
|
850
|
+
} catch {
|
|
851
|
+
// ignore malformed lines
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
proc.stdout.on("data", (chunk) => {
|
|
855
|
+
buffer += chunk.toString();
|
|
856
|
+
const lines = buffer.split("\n");
|
|
857
|
+
buffer = lines.pop() ?? "";
|
|
858
|
+
for (const line of lines) processLine(line);
|
|
859
|
+
});
|
|
860
|
+
proc.stderr.on("data", (chunk) => {
|
|
861
|
+
stderr += chunk.toString();
|
|
862
|
+
});
|
|
863
|
+
proc.on("close", (code) => {
|
|
864
|
+
if (buffer.trim()) processLine(buffer);
|
|
865
|
+
resolve(code === 0 ? liveActivity.lastAssistantText?.trim() || undefined : undefined);
|
|
866
|
+
});
|
|
867
|
+
proc.on("error", () => resolve(undefined));
|
|
868
|
+
if (overlay) {
|
|
869
|
+
overlay.onAbort = () => {
|
|
870
|
+
proc.kill("SIGTERM");
|
|
871
|
+
resolve(undefined);
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
liveRoleActivityByRoot.set(rootKey, cloneLiveRoleActivity(liveActivity, { status: output ? "ok" : "error" }));
|
|
876
|
+
await refreshStatus(ctx);
|
|
877
|
+
return output;
|
|
878
|
+
} finally {
|
|
879
|
+
clearInterval(heartbeat);
|
|
880
|
+
setTimeout(() => {
|
|
881
|
+
const current = liveRoleActivityByRoot.get(rootKey);
|
|
882
|
+
if (current && current.role === analystRole && current.status !== "running") {
|
|
883
|
+
liveRoleActivityByRoot.delete(rootKey);
|
|
884
|
+
void refreshStatus(ctx);
|
|
885
|
+
}
|
|
886
|
+
}, 10_000);
|
|
887
|
+
await fsp.rm(systemPromptTemp.dir, { recursive: true, force: true });
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
if (getCtxHasUI(ctx)) {
|
|
891
|
+
const ui = getCtxUi(ctx);
|
|
892
|
+
if (ui) {
|
|
893
|
+
return await ui.custom<string | undefined>((_tui, theme, _kb, done) => {
|
|
894
|
+
finishOverlay = done;
|
|
895
|
+
overlay = new StartupAnalystOverlay(theme);
|
|
896
|
+
overlay.setLines(contextProposalAnalystProgressLines(liveActivity));
|
|
897
|
+
run().then(settleOverlay).catch(() => settleOverlay(undefined));
|
|
898
|
+
return overlay;
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return await run();
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
async function analyzeContextProposalWithAgent(
|
|
906
|
+
ctx: { cwd: string; hasUI: boolean; ui: any; model?: any; modelRegistry?: any },
|
|
907
|
+
projectName: string,
|
|
908
|
+
recentEntries: RecentDiscussionEntry[],
|
|
909
|
+
explicitGoal?: string,
|
|
910
|
+
): Promise<ContextProposal | undefined> {
|
|
911
|
+
if (shouldDisableContextProposalAnalyst()) return undefined;
|
|
912
|
+
const testOutput = completionTestContextProposalAnalystOutput();
|
|
913
|
+
if (testOutput) {
|
|
914
|
+
return parseContextProposalAnalystOutput(testOutput, projectName, explicitGoal);
|
|
915
|
+
}
|
|
916
|
+
if (recentEntries.length === 0) return undefined;
|
|
917
|
+
try {
|
|
918
|
+
const raw = await runContextProposalAnalystSubprocess(ctx, projectName, recentEntries, explicitGoal);
|
|
919
|
+
if (!raw) return undefined;
|
|
920
|
+
return parseContextProposalAnalystOutput(raw, projectName, explicitGoal);
|
|
921
|
+
} catch (error) {
|
|
922
|
+
console.warn("[completion] context proposal analyst failed", error);
|
|
923
|
+
return undefined;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
511
927
|
function buildContextProposalGoalText(proposal: {
|
|
512
928
|
mission: string;
|
|
513
929
|
scope: string[];
|
|
@@ -547,27 +963,141 @@ function buildContextProposalDisplayText(proposal: ContextProposal): string {
|
|
|
547
963
|
return lines.join("\n");
|
|
548
964
|
}
|
|
549
965
|
|
|
550
|
-
function
|
|
966
|
+
function buildContextProposalConfirmationActions(): ContextProposalConfirmationActionItem[] {
|
|
551
967
|
return [
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
968
|
+
{
|
|
969
|
+
id: "start",
|
|
970
|
+
label: "Start",
|
|
971
|
+
description: "Accept this proposal and let /cook write or refocus canonical workflow state.",
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
id: "edit",
|
|
975
|
+
label: "Edit",
|
|
976
|
+
description: "Open the existing proposal editor before starting the workflow.",
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
id: "cancel",
|
|
980
|
+
label: "Cancel",
|
|
981
|
+
description: "Exit without changing canonical workflow state.",
|
|
982
|
+
},
|
|
983
|
+
];
|
|
559
984
|
}
|
|
560
985
|
|
|
561
|
-
function
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
986
|
+
function buildContextProposalConfirmationLayout(
|
|
987
|
+
title: string,
|
|
988
|
+
proposal: ContextProposal,
|
|
989
|
+
): ContextProposalConfirmationLayout {
|
|
990
|
+
return {
|
|
991
|
+
title,
|
|
992
|
+
intro: "Review the proposed mission, scope, constraints, and acceptance details before /cook writes canonical workflow state.",
|
|
993
|
+
proposalHeading: "Proposed workflow",
|
|
994
|
+
proposalBody: buildContextProposalDisplayText(proposal),
|
|
995
|
+
actionsHeading: "Actions",
|
|
996
|
+
actions: buildContextProposalConfirmationActions(),
|
|
997
|
+
footer: "↑↓ navigate • enter select • esc cancel",
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function maybeWriteContextProposalConfirmationSnapshot(layout: ContextProposalConfirmationLayout): void {
|
|
1002
|
+
const snapshotPath = completionTestContextProposalUiSnapshotPath();
|
|
1003
|
+
if (!snapshotPath) return;
|
|
1004
|
+
try {
|
|
1005
|
+
fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });
|
|
1006
|
+
fs.writeFileSync(snapshotPath, `${JSON.stringify(layout, null, 2)}\n`, "utf8");
|
|
1007
|
+
} catch {
|
|
1008
|
+
// ignore malformed or unwritable test snapshot paths
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function buildContextProposalConfirmationSelectItems(layout: ContextProposalConfirmationLayout): SelectItem[] {
|
|
1013
|
+
return layout.actions.map((action) => ({
|
|
1014
|
+
value: action.id,
|
|
1015
|
+
label: action.label,
|
|
1016
|
+
description: action.description,
|
|
1017
|
+
}));
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
async function promptContextProposalConfirmationAction(
|
|
1021
|
+
ui: any,
|
|
1022
|
+
layout: ContextProposalConfirmationLayout,
|
|
1023
|
+
): Promise<ContextProposalConfirmAction | undefined> {
|
|
1024
|
+
const items = buildContextProposalConfirmationSelectItems(layout);
|
|
1025
|
+
return await ui.custom<ContextProposalConfirmAction | undefined>((tui: any, theme: any, _kb: any, done: any) => {
|
|
1026
|
+
const container = new Container();
|
|
1027
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
1028
|
+
container.addChild(new Text(theme.fg("accent", theme.bold(layout.title)), 1, 0));
|
|
1029
|
+
container.addChild(new Text(theme.fg("dim", layout.intro), 1, 0));
|
|
1030
|
+
container.addChild(new Text("", 0, 0));
|
|
1031
|
+
container.addChild(new Text(theme.fg("accent", theme.bold(layout.proposalHeading)), 1, 0));
|
|
1032
|
+
container.addChild(new Text(layout.proposalBody, 1, 0));
|
|
1033
|
+
container.addChild(new Text("", 0, 0));
|
|
1034
|
+
container.addChild(new Text(theme.fg("accent", theme.bold(layout.actionsHeading)), 1, 0));
|
|
1035
|
+
const selectList = new SelectList(items, items.length, {
|
|
1036
|
+
selectedPrefix: (text) => theme.fg("accent", text),
|
|
1037
|
+
selectedText: (text) => theme.fg("accent", text),
|
|
1038
|
+
description: (text) => theme.fg("muted", text),
|
|
1039
|
+
scrollInfo: (text) => theme.fg("dim", text),
|
|
1040
|
+
noMatch: (text) => theme.fg("warning", text),
|
|
1041
|
+
});
|
|
1042
|
+
selectList.onSelect = (item) => done(item.value as ContextProposalConfirmAction);
|
|
1043
|
+
selectList.onCancel = () => done(undefined);
|
|
1044
|
+
container.addChild(selectList);
|
|
1045
|
+
container.addChild(new Text(theme.fg("dim", layout.footer), 1, 0));
|
|
1046
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
1047
|
+
|
|
1048
|
+
return {
|
|
1049
|
+
render: (width: number) => container.render(width),
|
|
1050
|
+
invalidate: () => container.invalidate(),
|
|
1051
|
+
handleInput: (data: string) => {
|
|
1052
|
+
if (matchesKey(data, "escape")) {
|
|
1053
|
+
done(undefined);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
selectList.handleInput(data);
|
|
1057
|
+
tui.requestRender();
|
|
1058
|
+
},
|
|
1059
|
+
};
|
|
1060
|
+
});
|
|
567
1061
|
}
|
|
568
1062
|
|
|
569
|
-
function
|
|
570
|
-
|
|
1063
|
+
async function resolveEditedContextProposalDecision(
|
|
1064
|
+
ctx: { hasUI: boolean; ui: any },
|
|
1065
|
+
projectName: string,
|
|
1066
|
+
editedText: string,
|
|
1067
|
+
confirmMissionWhenNeeded: boolean,
|
|
1068
|
+
): Promise<ContextProposalDecision | undefined> {
|
|
1069
|
+
if (!editedText.trim()) return undefined;
|
|
1070
|
+
const editedProposal = parseContextProposal(editedText, projectName);
|
|
1071
|
+
if (editedProposal) return { missionAnchor: editedProposal.mission, goalText: editedProposal.goalText };
|
|
1072
|
+
const assessment = assessMissionAnchor(editedText, projectName);
|
|
1073
|
+
if (!confirmMissionWhenNeeded) {
|
|
1074
|
+
return { missionAnchor: assessment.derived, goalText: editedText.trim() };
|
|
1075
|
+
}
|
|
1076
|
+
const missionAnchor = await confirmMissionAnchor(ctx, assessment);
|
|
1077
|
+
if (!missionAnchor) return undefined;
|
|
1078
|
+
return { missionAnchor, goalText: editedText.trim() };
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
async function resolveContextProposalConfirmationAction(
|
|
1082
|
+
ctx: { hasUI: boolean; ui: any },
|
|
1083
|
+
proposal: ContextProposal,
|
|
1084
|
+
projectName: string,
|
|
1085
|
+
options: ContextProposalConfirmOptions,
|
|
1086
|
+
action: ContextProposalConfirmAction,
|
|
1087
|
+
editedTextOverride?: string,
|
|
1088
|
+
): Promise<ContextProposalDecision | undefined> {
|
|
1089
|
+
if (action === "cancel") return undefined;
|
|
1090
|
+
if (action === "start") {
|
|
1091
|
+
return { missionAnchor: proposal.mission, goalText: proposal.goalText };
|
|
1092
|
+
}
|
|
1093
|
+
const editedText =
|
|
1094
|
+
editedTextOverride ??
|
|
1095
|
+
(await getCtxUi(ctx)?.editor(
|
|
1096
|
+
options.editorPrompt ?? `${options.title}\n\nEdit the proposed mission, scope, constraints, and acceptance details below.`,
|
|
1097
|
+
buildContextProposalEditorText(proposal),
|
|
1098
|
+
));
|
|
1099
|
+
if (!editedText?.trim()) return undefined;
|
|
1100
|
+
return await resolveEditedContextProposalDecision(ctx, projectName, editedText, editedTextOverride === undefined);
|
|
571
1101
|
}
|
|
572
1102
|
|
|
573
1103
|
function buildContextProposalEditorText(proposal: ContextProposal): string {
|
|
@@ -680,53 +1210,28 @@ function parseContextProposal(text: string, projectName: string): ContextProposa
|
|
|
680
1210
|
};
|
|
681
1211
|
}
|
|
682
1212
|
|
|
683
|
-
function extractContextProposalFromSession(
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
}
|
|
691
|
-
const candidates: string[] = [];
|
|
692
|
-
for (let index = branch.length - 1; index >= 0; index -= 1) {
|
|
693
|
-
const entry = branch[index];
|
|
694
|
-
if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message)) continue;
|
|
695
|
-
const message = entry.message as JsonRecord;
|
|
696
|
-
let text = "";
|
|
697
|
-
const role = asString(message.role);
|
|
698
|
-
if (role === "user" || role === "assistant" || role === "custom") {
|
|
699
|
-
text = extractTextFromMessageContent(message.content);
|
|
700
|
-
} else if (role === "branchSummary" || role === "compactionSummary") {
|
|
701
|
-
text = asString(message.summary) ?? "";
|
|
702
|
-
}
|
|
703
|
-
if (!text) continue;
|
|
704
|
-
const trimmed = text.trim();
|
|
705
|
-
if (!trimmed || /^\/(?:cook|complete)\b/i.test(trimmed)) continue;
|
|
706
|
-
candidates.push(trimmed);
|
|
707
|
-
}
|
|
708
|
-
for (const candidate of candidates) {
|
|
709
|
-
const parsed = parseContextProposal(candidate, projectName);
|
|
710
|
-
if (parsed) return parsed;
|
|
711
|
-
}
|
|
712
|
-
if (candidates.length > 1) {
|
|
713
|
-
const combined = candidates.slice(0, 4).reverse().join("\n\n");
|
|
714
|
-
return parseContextProposal(combined, projectName);
|
|
715
|
-
}
|
|
716
|
-
return undefined;
|
|
1213
|
+
async function extractContextProposalFromSession(
|
|
1214
|
+
ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
|
|
1215
|
+
projectName: string,
|
|
1216
|
+
explicitGoal?: string,
|
|
1217
|
+
): Promise<ContextProposal | undefined> {
|
|
1218
|
+
const recentEntries = collectRecentDiscussionEntries(ctx);
|
|
1219
|
+
return await analyzeContextProposalWithAgent(ctx, projectName, recentEntries, explicitGoal);
|
|
717
1220
|
}
|
|
718
1221
|
|
|
719
|
-
function buildGoalAnchoredContextProposal(
|
|
720
|
-
ctx: { sessionManager: any },
|
|
1222
|
+
async function buildGoalAnchoredContextProposal(
|
|
1223
|
+
ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
|
|
721
1224
|
goal: string,
|
|
722
1225
|
projectName: string,
|
|
723
|
-
): ContextProposal {
|
|
1226
|
+
): Promise<ContextProposal> {
|
|
724
1227
|
const explicit = parseContextProposal(goal, projectName);
|
|
725
|
-
const sessionProposal = extractContextProposalFromSession(ctx, projectName);
|
|
1228
|
+
const sessionProposal = await extractContextProposalFromSession(ctx, projectName, goal);
|
|
726
1229
|
const missionSource = explicit?.mission ?? goal;
|
|
727
1230
|
const assessment = assessMissionAnchor(missionSource, projectName);
|
|
728
1231
|
const mission = assessment.derived;
|
|
729
|
-
const
|
|
1232
|
+
const explicitScope = explicit?.scope ?? [];
|
|
1233
|
+
const sessionScope = (sessionProposal?.scope ?? []).filter((item) => isSessionScopeItemMissionRelevant(item, mission));
|
|
1234
|
+
const scope = uniqueProposalItems([...explicitScope, ...sessionScope]);
|
|
730
1235
|
const constraints = uniqueProposalItems([...(explicit?.constraints ?? []), ...(sessionProposal?.constraints ?? [])]);
|
|
731
1236
|
const acceptance = uniqueProposalItems([...(explicit?.acceptance ?? []), ...(sessionProposal?.acceptance ?? [])]);
|
|
732
1237
|
const goalText = buildContextProposalGoalText({ mission, scope, constraints, acceptance });
|
|
@@ -737,7 +1242,7 @@ function buildGoalAnchoredContextProposal(
|
|
|
737
1242
|
acceptance,
|
|
738
1243
|
goalText,
|
|
739
1244
|
basisPreview: sessionProposal?.basisPreview ?? explicit?.basisPreview ?? goal,
|
|
740
|
-
source: "session",
|
|
1245
|
+
source: sessionProposal?.source ?? "session",
|
|
741
1246
|
};
|
|
742
1247
|
}
|
|
743
1248
|
|
|
@@ -755,10 +1260,20 @@ async function confirmContextProposal(
|
|
|
755
1260
|
if (actionOverride === "edit") {
|
|
756
1261
|
const editedText = completionTestContextProposalEditText();
|
|
757
1262
|
if (!editedText) return undefined;
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1263
|
+
return await resolveEditedContextProposalDecision(ctx, projectName, editedText, false);
|
|
1264
|
+
}
|
|
1265
|
+
const layout = buildContextProposalConfirmationLayout(options.title, proposal);
|
|
1266
|
+
maybeWriteContextProposalConfirmationSnapshot(layout);
|
|
1267
|
+
const uiActionOverride = completionTestContextProposalUiActionOverride();
|
|
1268
|
+
if (uiActionOverride) {
|
|
1269
|
+
return await resolveContextProposalConfirmationAction(
|
|
1270
|
+
ctx,
|
|
1271
|
+
proposal,
|
|
1272
|
+
projectName,
|
|
1273
|
+
options,
|
|
1274
|
+
uiActionOverride,
|
|
1275
|
+
uiActionOverride === "edit" ? completionTestContextProposalEditText() : undefined,
|
|
1276
|
+
);
|
|
762
1277
|
}
|
|
763
1278
|
if (!getCtxHasUI(ctx)) {
|
|
764
1279
|
return options.nonInteractiveBehavior === "accept"
|
|
@@ -771,25 +1286,9 @@ async function confirmContextProposal(
|
|
|
771
1286
|
? { missionAnchor: proposal.mission, goalText: proposal.goalText }
|
|
772
1287
|
: undefined;
|
|
773
1288
|
}
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const choice = await ui.select(options.title, [useChoice, editChoice, cancelChoice]);
|
|
778
|
-
if (!choice || choice === cancelChoice) return undefined;
|
|
779
|
-
if (choice === editChoice) {
|
|
780
|
-
const editedText = await ui.editor(
|
|
781
|
-
options.editorPrompt ?? `${options.title}\n\nEdit the proposed mission, scope, constraints, and acceptance details below.`,
|
|
782
|
-
buildContextProposalEditorText(proposal),
|
|
783
|
-
);
|
|
784
|
-
if (!editedText?.trim()) return undefined;
|
|
785
|
-
const editedProposal = parseContextProposal(editedText, projectName);
|
|
786
|
-
if (editedProposal) return { missionAnchor: editedProposal.mission, goalText: editedProposal.goalText };
|
|
787
|
-
const assessment = assessMissionAnchor(editedText, projectName);
|
|
788
|
-
const missionAnchor = await confirmMissionAnchor(ctx, assessment);
|
|
789
|
-
if (!missionAnchor) return undefined;
|
|
790
|
-
return { missionAnchor, goalText: editedText.trim() };
|
|
791
|
-
}
|
|
792
|
-
return { missionAnchor: proposal.mission, goalText: proposal.goalText };
|
|
1289
|
+
const choice = await promptContextProposalConfirmationAction(ui, layout);
|
|
1290
|
+
if (!choice) return undefined;
|
|
1291
|
+
return await resolveContextProposalConfirmationAction(ctx, proposal, projectName, options, choice);
|
|
793
1292
|
}
|
|
794
1293
|
|
|
795
1294
|
function currentMissionAnchor(snapshot: CompletionStateSnapshot): string {
|
|
@@ -2422,11 +2921,11 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
2422
2921
|
const root = findRepoRoot(cwd) ?? cwd;
|
|
2423
2922
|
const projectName = path.basename(root);
|
|
2424
2923
|
if (!goal) {
|
|
2425
|
-
const proposal = extractContextProposalFromSession(ctx, projectName);
|
|
2924
|
+
const proposal = await extractContextProposalFromSession(ctx, projectName);
|
|
2426
2925
|
if (!proposal) {
|
|
2427
2926
|
emitCommandText(
|
|
2428
2927
|
ctx,
|
|
2429
|
-
"Usage: /cook <goal> (
|
|
2928
|
+
"Usage: /cook <goal> (discussion-only startup needs proposal analyst output; otherwise pass an explicit goal)",
|
|
2430
2929
|
"error",
|
|
2431
2930
|
);
|
|
2432
2931
|
return;
|
|
@@ -2443,7 +2942,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
2443
2942
|
goal = decision.goalText;
|
|
2444
2943
|
kickoffMissionAnchor = decision.missionAnchor;
|
|
2445
2944
|
} else {
|
|
2446
|
-
const proposal = buildGoalAnchoredContextProposal(ctx, goal, projectName);
|
|
2945
|
+
const proposal = await buildGoalAnchoredContextProposal(ctx, goal, projectName);
|
|
2447
2946
|
const decision = await confirmContextProposal(ctx, proposal, projectName, {
|
|
2448
2947
|
title: "Start a completion workflow from this goal?",
|
|
2449
2948
|
nonInteractiveBehavior: "accept",
|
|
@@ -2472,11 +2971,11 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
2472
2971
|
if (!goal) {
|
|
2473
2972
|
if (workflowDone) {
|
|
2474
2973
|
const projectName = path.basename(snapshot.files.root);
|
|
2475
|
-
const proposal = extractContextProposalFromSession(ctx, projectName);
|
|
2974
|
+
const proposal = await extractContextProposalFromSession(ctx, projectName);
|
|
2476
2975
|
if (!proposal) {
|
|
2477
2976
|
emitCommandText(
|
|
2478
2977
|
ctx,
|
|
2479
|
-
"The previous completion workflow is already done.
|
|
2978
|
+
"The previous completion workflow is already done. Provide /cook <goal>, or rerun /cook when the proposal analyst can summarize the next round from discussion.",
|
|
2480
2979
|
"info",
|
|
2481
2980
|
);
|
|
2482
2981
|
return;
|
|
@@ -2512,7 +3011,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
2512
3011
|
if (hadSnapshot && explicitGoal) {
|
|
2513
3012
|
if (workflowDone) {
|
|
2514
3013
|
const projectName = path.basename(snapshot.files.root);
|
|
2515
|
-
const proposal = buildGoalAnchoredContextProposal(ctx, goal, projectName);
|
|
3014
|
+
const proposal = await buildGoalAnchoredContextProposal(ctx, goal, projectName);
|
|
2516
3015
|
const decision = await confirmContextProposal(ctx, proposal, projectName, {
|
|
2517
3016
|
title: "Start the next workflow round from this goal?",
|
|
2518
3017
|
nonInteractiveBehavior: "accept",
|
|
@@ -2539,7 +3038,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
2539
3038
|
kickoffMissionAnchor = decision.currentMissionAnchor;
|
|
2540
3039
|
if (decision.action === "refocus") {
|
|
2541
3040
|
const projectName = path.basename(snapshot.files.root);
|
|
2542
|
-
const proposal = buildGoalAnchoredContextProposal(ctx, goal, projectName);
|
|
3041
|
+
const proposal = await buildGoalAnchoredContextProposal(ctx, goal, projectName);
|
|
2543
3042
|
const proposalDecision = await confirmContextProposal(ctx, proposal, projectName, {
|
|
2544
3043
|
title: "Start the replacement workflow from this goal?",
|
|
2545
3044
|
nonInteractiveBehavior: "accept",
|