@linimin/pi-letscook 0.1.30 → 0.1.32
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 +23 -0
- package/README.md +48 -1
- package/agents/completion-auditor.md +17 -0
- package/agents/completion-reviewer.md +17 -0
- package/agents/completion-stop-judge.md +17 -0
- package/extensions/completion/index.ts +912 -205
- package/extensions/completion/role-reporting.js +356 -0
- package/package.json +2 -1
- package/scripts/context-proposal-test.sh +115 -6
- package/scripts/refocus-test.sh +11 -0
- package/scripts/release-check.sh +2 -0
- package/scripts/rubric-contract-test.sh +249 -0
- package/scripts/smoke-test.sh +175 -23
- package/skills/completion-protocol/SKILL.md +39 -0
- package/skills/completion-protocol/references/completion.md +71 -0
|
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import { promises as fsp } from "node:fs";
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
+
import * as roleReporting from "./role-reporting.js";
|
|
6
7
|
import { StringEnum } from "@mariozechner/pi-ai";
|
|
7
8
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
8
9
|
import { DynamicBorder, parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
@@ -30,6 +31,11 @@ const PACKAGE_REFERENCE_PATH = PACKAGE_ROOT
|
|
|
30
31
|
const PACKAGE_AGENTS_DIR = PACKAGE_ROOT ? path.join(PACKAGE_ROOT, "agents") : undefined;
|
|
31
32
|
const SKILL_PATH = PACKAGE_SKILL_PATH ?? path.join(AGENT_HOME, "skills", "completion-protocol", "SKILL.md");
|
|
32
33
|
const REFERENCE_PATH = PACKAGE_REFERENCE_PATH ?? path.join(AGENT_HOME, "skills", "completion-protocol", "references", "completion.md");
|
|
34
|
+
const DEFAULT_TASK_TYPE = "completion-workflow";
|
|
35
|
+
const DEFAULT_EVALUATION_PROFILE = "completion-rubric-v1";
|
|
36
|
+
const RUBRIC_EVALUATION_ROLES = ["completion-reviewer", "completion-auditor", "completion-stop-judge"] as const;
|
|
37
|
+
|
|
38
|
+
type RubricEvaluationRole = (typeof RUBRIC_EVALUATION_ROLES)[number];
|
|
33
39
|
|
|
34
40
|
type CompletionRole = (typeof ROLE_NAMES)[number];
|
|
35
41
|
type JsonRecord = Record<string, unknown>;
|
|
@@ -108,16 +114,27 @@ type CompletionStatusSurface = {
|
|
|
108
114
|
liveDetailsLines?: string[];
|
|
109
115
|
};
|
|
110
116
|
|
|
117
|
+
type ContextProposalAnalysis = {
|
|
118
|
+
taskType?: string;
|
|
119
|
+
evaluationProfile?: string;
|
|
120
|
+
critique: string[];
|
|
121
|
+
risks: string[];
|
|
122
|
+
possibleNoise: string[];
|
|
123
|
+
};
|
|
124
|
+
|
|
111
125
|
type ContextProposal = {
|
|
112
126
|
mission: string;
|
|
113
127
|
scope: string[];
|
|
114
128
|
constraints: string[];
|
|
115
129
|
acceptance: string[];
|
|
130
|
+
analysis: ContextProposalAnalysis;
|
|
116
131
|
goalText: string;
|
|
117
132
|
basisPreview: string;
|
|
118
133
|
source: "session" | "analyst";
|
|
119
134
|
};
|
|
120
135
|
|
|
136
|
+
type ContextProposalSection = "mission" | "scope" | "constraints" | "acceptance" | "critique" | "risks";
|
|
137
|
+
|
|
121
138
|
type RecentDiscussionEntry = {
|
|
122
139
|
role: "user" | "assistant" | "custom" | "summary";
|
|
123
140
|
text: string;
|
|
@@ -126,6 +143,7 @@ type RecentDiscussionEntry = {
|
|
|
126
143
|
type ContextProposalDecision = {
|
|
127
144
|
missionAnchor: string;
|
|
128
145
|
goalText: string;
|
|
146
|
+
analysis: ContextProposalAnalysis;
|
|
129
147
|
};
|
|
130
148
|
|
|
131
149
|
type ContextProposalConfirmAction = "start" | "edit" | "cancel";
|
|
@@ -141,6 +159,10 @@ type ContextProposalConfirmationLayout = {
|
|
|
141
159
|
intro: string;
|
|
142
160
|
proposalHeading: string;
|
|
143
161
|
proposalBody: string;
|
|
162
|
+
critiqueHeading?: string;
|
|
163
|
+
critiqueBody?: string;
|
|
164
|
+
routingHeading?: string;
|
|
165
|
+
routingBody?: string;
|
|
144
166
|
actionsHeading: string;
|
|
145
167
|
actions: ContextProposalConfirmationActionItem[];
|
|
146
168
|
footer: string;
|
|
@@ -203,6 +225,16 @@ const liveRoleActivityByRoot = new Map<string, LiveRoleActivity>();
|
|
|
203
225
|
const LIVE_ROLE_WAITING_MS = 15_000;
|
|
204
226
|
const LIVE_ROLE_STALLED_MS = 45_000;
|
|
205
227
|
const LIVE_ROLE_HEARTBEAT_MS = 5_000;
|
|
228
|
+
const DRIVER_AUTO_CONTINUE_MAX_ATTEMPTS = 2;
|
|
229
|
+
|
|
230
|
+
type DriverContinuationTracker = {
|
|
231
|
+
fingerprint: string;
|
|
232
|
+
attempts: number;
|
|
233
|
+
inFlight: boolean;
|
|
234
|
+
warned: boolean;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const driverContinuationByRoot = new Map<string, DriverContinuationTracker>();
|
|
206
238
|
|
|
207
239
|
function isRecord(value: unknown): value is JsonRecord {
|
|
208
240
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -499,6 +531,36 @@ function completionTestContextProposalUiSnapshotPath(): string | undefined {
|
|
|
499
531
|
return asString(process.env.PI_COMPLETION_TEST_CONTEXT_PROPOSAL_UI_PATH);
|
|
500
532
|
}
|
|
501
533
|
|
|
534
|
+
function completionTestContextProposalSnapshotPath(): string | undefined {
|
|
535
|
+
return asString(process.env.PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function completionTestDriverPromptPath(): string | undefined {
|
|
539
|
+
return asString(process.env.PI_COMPLETION_TEST_DRIVER_PROMPT_PATH);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function completionTestAutoContinuePromptPath(): string | undefined {
|
|
543
|
+
return asString(process.env.PI_COMPLETION_TEST_AUTO_CONTINUE_PROMPT_PATH);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function shouldTestAutoContinueOnSessionStart(): boolean {
|
|
547
|
+
return process.env.PI_COMPLETION_TEST_AUTO_CONTINUE_ON_SESSION_START === "1";
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function completionTestSystemReminderPath(): string | undefined {
|
|
551
|
+
return asString(process.env.PI_COMPLETION_TEST_SYSTEM_REMINDER_PATH);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function maybeWriteTestSnapshot(targetPath: string | undefined, content: string): void {
|
|
555
|
+
if (!targetPath) return;
|
|
556
|
+
try {
|
|
557
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
558
|
+
fs.writeFileSync(targetPath, content, "utf8");
|
|
559
|
+
} catch {
|
|
560
|
+
// ignore malformed or unwritable test snapshot paths
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
502
564
|
function shouldDisableContextProposalAnalyst(): boolean {
|
|
503
565
|
return process.env.PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST === "1";
|
|
504
566
|
}
|
|
@@ -541,7 +603,7 @@ function normalizeProposalLine(line: string): string {
|
|
|
541
603
|
.trim();
|
|
542
604
|
}
|
|
543
605
|
|
|
544
|
-
function detectProposalSection(line: string):
|
|
606
|
+
function detectProposalSection(line: string): ContextProposalSection | undefined {
|
|
545
607
|
const normalized = normalizeProposalLine(line)
|
|
546
608
|
.toLowerCase()
|
|
547
609
|
.replace(/[::]$/, "")
|
|
@@ -551,12 +613,12 @@ function detectProposalSection(line: string): "mission" | "scope" | "constraints
|
|
|
551
613
|
if (["scope", "plan", "steps", "implementation", "範圍", "范围", "實作", "实现", "步驟", "步骤"].includes(normalized)) return "scope";
|
|
552
614
|
if (["constraints", "constraint", "guardrails", "non-goals", "限制", "約束", "约束", "非目標", "非目标"].includes(normalized)) return "constraints";
|
|
553
615
|
if (["acceptance", "acceptance criteria", "deliverables", "verification", "驗收", "验收", "交付", "驗證", "验证"].includes(normalized)) return "acceptance";
|
|
616
|
+
if (["critique", "critic", "concerns", "concern", "warnings", "warning", "notes", "note", "評論", "评论", "提醒"].includes(normalized)) return "critique";
|
|
617
|
+
if (["risk", "risks", "hazards", "hazard", "failure modes", "failure mode", "風險", "风险"].includes(normalized)) return "risks";
|
|
554
618
|
return undefined;
|
|
555
619
|
}
|
|
556
620
|
|
|
557
|
-
function matchInlineProposalSection(
|
|
558
|
-
line: string,
|
|
559
|
-
): { section: "mission" | "scope" | "constraints" | "acceptance"; content: string } | undefined {
|
|
621
|
+
function matchInlineProposalSection(line: string): { section: ContextProposalSection; content: string } | undefined {
|
|
560
622
|
const normalized = normalizeProposalLine(line);
|
|
561
623
|
const match = normalized.match(/^([^::]+)[::]\s*(.+)$/u);
|
|
562
624
|
if (!match) return undefined;
|
|
@@ -595,6 +657,113 @@ function uniqueProposalItems(items: string[]): string[] {
|
|
|
595
657
|
return result;
|
|
596
658
|
}
|
|
597
659
|
|
|
660
|
+
function normalizeContextProposalHint(value: unknown): string | undefined {
|
|
661
|
+
const normalized = asString(value)?.replace(/\s+/g, " ").trim();
|
|
662
|
+
return normalized || undefined;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function normalizeContextProposalTaskTypeHint(value: unknown): string | undefined {
|
|
666
|
+
const normalized = normalizeContextProposalHint(value);
|
|
667
|
+
if (!normalized) return undefined;
|
|
668
|
+
const canonical = normalized.toLowerCase().replace(/[\s/]+/g, "-");
|
|
669
|
+
return canonical === DEFAULT_TASK_TYPE ? DEFAULT_TASK_TYPE : normalized;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function normalizeContextProposalEvaluationProfileHint(value: unknown): string | undefined {
|
|
673
|
+
const normalized = normalizeContextProposalHint(value);
|
|
674
|
+
if (!normalized) return undefined;
|
|
675
|
+
const canonical = normalized.toLowerCase().replace(/[\s/]+/g, "-");
|
|
676
|
+
return canonical === DEFAULT_EVALUATION_PROFILE ? DEFAULT_EVALUATION_PROFILE : normalized;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function inferContextProposalTaskType(texts: string[]): string | undefined {
|
|
680
|
+
const corpus = texts
|
|
681
|
+
.map((text) => normalizeProposalLine(text).toLowerCase())
|
|
682
|
+
.filter(Boolean)
|
|
683
|
+
.join("\n");
|
|
684
|
+
if (!corpus) return undefined;
|
|
685
|
+
return /(completion|\/cook|\/complete|\.agent|slice|reground|reviewer|auditor|stop judge|stop-judge|workflow)/i.test(corpus)
|
|
686
|
+
? DEFAULT_TASK_TYPE
|
|
687
|
+
: undefined;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function inferContextProposalEvaluationProfile(texts: string[], taskType?: string): string | undefined {
|
|
691
|
+
const corpus = texts
|
|
692
|
+
.map((text) => normalizeProposalLine(text).toLowerCase())
|
|
693
|
+
.filter(Boolean)
|
|
694
|
+
.join("\n");
|
|
695
|
+
if (!corpus) return undefined;
|
|
696
|
+
if (
|
|
697
|
+
/(rubric|evaluation[_\s-]*profile|pass\|concern\|fail|contract coverage|correctness risk|verification evidence|docs\/state parity|reviewer|auditor|stop judge|stop-judge)/i.test(
|
|
698
|
+
corpus,
|
|
699
|
+
)
|
|
700
|
+
) {
|
|
701
|
+
return DEFAULT_EVALUATION_PROFILE;
|
|
702
|
+
}
|
|
703
|
+
return taskType === DEFAULT_TASK_TYPE && /(completion|\/cook|\/complete|slice|workflow|review|audit)/i.test(corpus)
|
|
704
|
+
? DEFAULT_EVALUATION_PROFILE
|
|
705
|
+
: undefined;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function buildContextProposalAnalysis(args: {
|
|
709
|
+
taskType?: unknown;
|
|
710
|
+
evaluationProfile?: unknown;
|
|
711
|
+
critique?: string[];
|
|
712
|
+
risks?: string[];
|
|
713
|
+
possibleNoise?: string[];
|
|
714
|
+
hintTexts?: string[];
|
|
715
|
+
}): ContextProposalAnalysis {
|
|
716
|
+
const critique = uniqueProposalItems(args.critique ?? []);
|
|
717
|
+
const risks = uniqueProposalItems(args.risks ?? []);
|
|
718
|
+
const possibleNoise = uniqueProposalItems(args.possibleNoise ?? []);
|
|
719
|
+
const hintTexts = [...(args.hintTexts ?? []), ...critique, ...risks, ...possibleNoise];
|
|
720
|
+
const taskType = normalizeContextProposalTaskTypeHint(args.taskType) ?? inferContextProposalTaskType(hintTexts);
|
|
721
|
+
const evaluationProfile =
|
|
722
|
+
normalizeContextProposalEvaluationProfileHint(args.evaluationProfile) ??
|
|
723
|
+
inferContextProposalEvaluationProfile(hintTexts, taskType);
|
|
724
|
+
return {
|
|
725
|
+
taskType,
|
|
726
|
+
evaluationProfile,
|
|
727
|
+
critique,
|
|
728
|
+
risks,
|
|
729
|
+
possibleNoise,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function mergeContextProposalAnalysis(
|
|
734
|
+
sources: Array<ContextProposalAnalysis | undefined>,
|
|
735
|
+
hintTexts: string[] = [],
|
|
736
|
+
): ContextProposalAnalysis {
|
|
737
|
+
const critique = uniqueProposalItems(sources.flatMap((source) => source?.critique ?? []));
|
|
738
|
+
const risks = uniqueProposalItems(sources.flatMap((source) => source?.risks ?? []));
|
|
739
|
+
const possibleNoise = uniqueProposalItems(sources.flatMap((source) => source?.possibleNoise ?? []));
|
|
740
|
+
const taskType =
|
|
741
|
+
sources.map((source) => source?.taskType).find((value): value is string => Boolean(value)) ??
|
|
742
|
+
inferContextProposalTaskType([...hintTexts, ...critique, ...risks, ...possibleNoise]);
|
|
743
|
+
const evaluationProfile =
|
|
744
|
+
sources.map((source) => source?.evaluationProfile).find((value): value is string => Boolean(value)) ??
|
|
745
|
+
inferContextProposalEvaluationProfile([...hintTexts, ...critique, ...risks, ...possibleNoise], taskType);
|
|
746
|
+
return {
|
|
747
|
+
taskType,
|
|
748
|
+
evaluationProfile,
|
|
749
|
+
critique,
|
|
750
|
+
risks,
|
|
751
|
+
possibleNoise,
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function matchContextProposalRoutingHint(
|
|
756
|
+
line: string,
|
|
757
|
+
): { field: "taskType" | "evaluationProfile"; value: string } | undefined {
|
|
758
|
+
const normalized = normalizeProposalLine(line);
|
|
759
|
+
const match = normalized.match(/^(task[\s_-]*type|evaluation[\s_-]*profile)[::]\s*(.+)$/iu);
|
|
760
|
+
if (!match) return undefined;
|
|
761
|
+
const label = match[1].toLowerCase().replace(/[\s_-]+/g, "");
|
|
762
|
+
const value = match[2].trim();
|
|
763
|
+
if (!value) return undefined;
|
|
764
|
+
return label === "tasktype" ? { field: "taskType", value } : { field: "evaluationProfile", value };
|
|
765
|
+
}
|
|
766
|
+
|
|
598
767
|
const MISSION_SCOPE_FILTER_STOPWORDS = new Set([
|
|
599
768
|
"a",
|
|
600
769
|
"an",
|
|
@@ -654,11 +823,14 @@ function isSessionScopeItemMissionRelevant(item: string, mission: string): boole
|
|
|
654
823
|
const CONTEXT_PROPOSAL_ANALYST_SYSTEM_PROMPT = [
|
|
655
824
|
"You analyze recent /cook startup discussion and return a strict JSON object.",
|
|
656
825
|
"Do not emit markdown, code fences, or commentary.",
|
|
657
|
-
"Return exactly one JSON object with keys: mission, scope, constraints, acceptance, confidence, possible_noise.",
|
|
826
|
+
"Return exactly one JSON object with keys: mission, scope, constraints, acceptance, critique, risks, task_type, evaluation_profile, confidence, possible_noise.",
|
|
658
827
|
"mission must be a concise implementation mission anchor sentence.",
|
|
659
828
|
"scope must contain only work items that directly support the mission.",
|
|
660
829
|
"constraints must contain guardrails or non-goals explicitly stated or strongly implied by the discussion.",
|
|
661
830
|
"acceptance must contain verifiable outcomes explicitly stated or strongly implied by the discussion.",
|
|
831
|
+
"critique must contain operator-facing cautions, concerns, or reminders that should be shown separately from mission and scope later.",
|
|
832
|
+
"risks must contain concrete failure modes or regressions that the later workflow should keep in view.",
|
|
833
|
+
"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.",
|
|
662
834
|
"possible_noise should list discussion points that look stale, weakly related, or unsafe to promote into scope.",
|
|
663
835
|
"When an explicit goal is provided, keep the mission anchored to that goal instead of replacing it with a broader or different mission.",
|
|
664
836
|
"When discussion is insufficient, prefer empty arrays and a low confidence value over invention.",
|
|
@@ -739,12 +911,27 @@ function parseContextProposalAnalystOutput(
|
|
|
739
911
|
const scope = uniqueProposalItems(asStringArray(parsed.scope));
|
|
740
912
|
const constraints = uniqueProposalItems(asStringArray(parsed.constraints));
|
|
741
913
|
const acceptance = uniqueProposalItems(asStringArray(parsed.acceptance));
|
|
914
|
+
const analysis = mergeContextProposalAnalysis(
|
|
915
|
+
[
|
|
916
|
+
explicit?.analysis,
|
|
917
|
+
buildContextProposalAnalysis({
|
|
918
|
+
taskType: parsed.task_type ?? parsed.taskType,
|
|
919
|
+
evaluationProfile: parsed.evaluation_profile ?? parsed.evaluationProfile,
|
|
920
|
+
critique: asStringArray(parsed.critique),
|
|
921
|
+
risks: asStringArray(parsed.risks ?? parsed.risk),
|
|
922
|
+
possibleNoise: asStringArray(parsed.possible_noise ?? parsed.possibleNoise),
|
|
923
|
+
hintTexts: [raw, mission, ...scope, ...constraints, ...acceptance],
|
|
924
|
+
}),
|
|
925
|
+
],
|
|
926
|
+
[explicitGoal ?? "", raw, mission, ...scope, ...constraints, ...acceptance],
|
|
927
|
+
);
|
|
742
928
|
const goalText = buildContextProposalGoalText({ mission, scope, constraints, acceptance });
|
|
743
929
|
return {
|
|
744
930
|
mission,
|
|
745
931
|
scope,
|
|
746
932
|
constraints,
|
|
747
933
|
acceptance,
|
|
934
|
+
analysis,
|
|
748
935
|
goalText,
|
|
749
936
|
basisPreview: raw.replace(/\s+/g, " ").trim(),
|
|
750
937
|
source: "analyst",
|
|
@@ -963,6 +1150,59 @@ function buildContextProposalDisplayText(proposal: ContextProposal): string {
|
|
|
963
1150
|
return lines.join("\n");
|
|
964
1151
|
}
|
|
965
1152
|
|
|
1153
|
+
function finalizeContextProposalAnalysis(analysis: ContextProposalAnalysis | undefined, hintTexts: string[] = []): ContextProposalAnalysis {
|
|
1154
|
+
const merged = mergeContextProposalAnalysis(analysis ? [analysis] : [], hintTexts);
|
|
1155
|
+
return {
|
|
1156
|
+
taskType: merged.taskType ?? DEFAULT_TASK_TYPE,
|
|
1157
|
+
evaluationProfile: merged.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
|
|
1158
|
+
critique: merged.critique,
|
|
1159
|
+
risks: merged.risks,
|
|
1160
|
+
possibleNoise: merged.possibleNoise,
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function buildContextProposalCritiqueText(analysis: ContextProposalAnalysis): string {
|
|
1165
|
+
const lines: string[] = [];
|
|
1166
|
+
if (analysis.critique.length > 0) {
|
|
1167
|
+
lines.push("Critique");
|
|
1168
|
+
for (const item of analysis.critique) lines.push(`- ${item}`);
|
|
1169
|
+
}
|
|
1170
|
+
if (analysis.risks.length > 0) {
|
|
1171
|
+
if (lines.length > 0) lines.push("");
|
|
1172
|
+
lines.push("Risks");
|
|
1173
|
+
for (const item of analysis.risks) lines.push(`- ${item}`);
|
|
1174
|
+
}
|
|
1175
|
+
if (analysis.possibleNoise.length > 0) {
|
|
1176
|
+
if (lines.length > 0) lines.push("");
|
|
1177
|
+
lines.push("Possible noise");
|
|
1178
|
+
for (const item of analysis.possibleNoise) lines.push(`- ${item}`);
|
|
1179
|
+
}
|
|
1180
|
+
if (lines.length === 0) {
|
|
1181
|
+
return "No critique, risk, or possible-noise notes were derived for this startup proposal.";
|
|
1182
|
+
}
|
|
1183
|
+
return lines.join("\n");
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function buildContextProposalRoutingText(analysis: ContextProposalAnalysis): string {
|
|
1187
|
+
return [`- task_type: ${analysis.taskType ?? DEFAULT_TASK_TYPE}`, `- evaluation_profile: ${analysis.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE}`].join(
|
|
1188
|
+
"\n",
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function summarizeContextProposalAnalysisItems(label: string, items: string[]): string | undefined {
|
|
1193
|
+
if (items.length === 0) return undefined;
|
|
1194
|
+
return `${label}=${truncateInline(items.join(" | "), 160)}`;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function buildContextProposalContinuationReason(prefix: string, goalText: string, analysis: ContextProposalAnalysis): string {
|
|
1198
|
+
const critiqueParts = [
|
|
1199
|
+
analysis.critique.length > 0 ? `accepted critique=${truncateInline(analysis.critique.join(" | "), 160)}` : "accepted critique=none",
|
|
1200
|
+
summarizeContextProposalAnalysisItems("risks", analysis.risks),
|
|
1201
|
+
summarizeContextProposalAnalysisItems("possible_noise", analysis.possibleNoise),
|
|
1202
|
+
].filter((part): part is string => Boolean(part));
|
|
1203
|
+
return `${prefix} ${truncateInline(goalText, 220)} | startup routing: task_type=${analysis.taskType ?? DEFAULT_TASK_TYPE}; evaluation_profile=${analysis.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE}; critique outcome=${critiqueParts.join("; ")}`;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
966
1206
|
function buildContextProposalConfirmationActions(): ContextProposalConfirmationActionItem[] {
|
|
967
1207
|
return [
|
|
968
1208
|
{
|
|
@@ -987,11 +1227,16 @@ function buildContextProposalConfirmationLayout(
|
|
|
987
1227
|
title: string,
|
|
988
1228
|
proposal: ContextProposal,
|
|
989
1229
|
): ContextProposalConfirmationLayout {
|
|
1230
|
+
const analysis = finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]);
|
|
990
1231
|
return {
|
|
991
1232
|
title,
|
|
992
|
-
intro: "Review the proposed mission, scope, constraints, and
|
|
1233
|
+
intro: "Review the proposed mission, scope, constraints, acceptance, critique, and routing details before /cook writes canonical workflow state.",
|
|
993
1234
|
proposalHeading: "Proposed workflow",
|
|
994
1235
|
proposalBody: buildContextProposalDisplayText(proposal),
|
|
1236
|
+
critiqueHeading: "Critique and risks",
|
|
1237
|
+
critiqueBody: buildContextProposalCritiqueText(analysis),
|
|
1238
|
+
routingHeading: "Routing recommendations",
|
|
1239
|
+
routingBody: buildContextProposalRoutingText(analysis),
|
|
995
1240
|
actionsHeading: "Actions",
|
|
996
1241
|
actions: buildContextProposalConfirmationActions(),
|
|
997
1242
|
footer: "↑↓ navigate • enter select • esc cancel",
|
|
@@ -1009,6 +1254,17 @@ function maybeWriteContextProposalConfirmationSnapshot(layout: ContextProposalCo
|
|
|
1009
1254
|
}
|
|
1010
1255
|
}
|
|
1011
1256
|
|
|
1257
|
+
function maybeWriteContextProposalSnapshot(proposal: ContextProposal): void {
|
|
1258
|
+
const snapshotPath = completionTestContextProposalSnapshotPath();
|
|
1259
|
+
if (!snapshotPath) return;
|
|
1260
|
+
try {
|
|
1261
|
+
fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });
|
|
1262
|
+
fs.writeFileSync(snapshotPath, `${JSON.stringify(proposal, null, 2)}\n`, "utf8");
|
|
1263
|
+
} catch {
|
|
1264
|
+
// ignore malformed or unwritable test snapshot paths
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1012
1268
|
function buildContextProposalConfirmationSelectItems(layout: ContextProposalConfirmationLayout): SelectItem[] {
|
|
1013
1269
|
return layout.actions.map((action) => ({
|
|
1014
1270
|
value: action.id,
|
|
@@ -1030,6 +1286,16 @@ async function promptContextProposalConfirmationAction(
|
|
|
1030
1286
|
container.addChild(new Text("", 0, 0));
|
|
1031
1287
|
container.addChild(new Text(theme.fg("accent", theme.bold(layout.proposalHeading)), 1, 0));
|
|
1032
1288
|
container.addChild(new Text(layout.proposalBody, 1, 0));
|
|
1289
|
+
if (layout.critiqueHeading && layout.critiqueBody) {
|
|
1290
|
+
container.addChild(new Text("", 0, 0));
|
|
1291
|
+
container.addChild(new Text(theme.fg("accent", theme.bold(layout.critiqueHeading)), 1, 0));
|
|
1292
|
+
container.addChild(new Text(layout.critiqueBody, 1, 0));
|
|
1293
|
+
}
|
|
1294
|
+
if (layout.routingHeading && layout.routingBody) {
|
|
1295
|
+
container.addChild(new Text("", 0, 0));
|
|
1296
|
+
container.addChild(new Text(theme.fg("accent", theme.bold(layout.routingHeading)), 1, 0));
|
|
1297
|
+
container.addChild(new Text(layout.routingBody, 1, 0));
|
|
1298
|
+
}
|
|
1033
1299
|
container.addChild(new Text("", 0, 0));
|
|
1034
1300
|
container.addChild(new Text(theme.fg("accent", theme.bold(layout.actionsHeading)), 1, 0));
|
|
1035
1301
|
const selectList = new SelectList(items, items.length, {
|
|
@@ -1065,17 +1331,28 @@ async function resolveEditedContextProposalDecision(
|
|
|
1065
1331
|
projectName: string,
|
|
1066
1332
|
editedText: string,
|
|
1067
1333
|
confirmMissionWhenNeeded: boolean,
|
|
1334
|
+
fallbackAnalysis?: ContextProposalAnalysis,
|
|
1068
1335
|
): Promise<ContextProposalDecision | undefined> {
|
|
1069
1336
|
if (!editedText.trim()) return undefined;
|
|
1070
1337
|
const editedProposal = parseContextProposal(editedText, projectName);
|
|
1071
|
-
if (editedProposal)
|
|
1338
|
+
if (editedProposal) {
|
|
1339
|
+
return {
|
|
1340
|
+
missionAnchor: editedProposal.mission,
|
|
1341
|
+
goalText: editedProposal.goalText,
|
|
1342
|
+
analysis: finalizeContextProposalAnalysis(
|
|
1343
|
+
mergeContextProposalAnalysis([editedProposal.analysis, fallbackAnalysis], [editedText, editedProposal.mission]),
|
|
1344
|
+
[editedText, editedProposal.mission],
|
|
1345
|
+
),
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1072
1348
|
const assessment = assessMissionAnchor(editedText, projectName);
|
|
1349
|
+
const analysis = finalizeContextProposalAnalysis(fallbackAnalysis, [editedText, assessment.derived]);
|
|
1073
1350
|
if (!confirmMissionWhenNeeded) {
|
|
1074
|
-
return { missionAnchor: assessment.derived, goalText: editedText.trim() };
|
|
1351
|
+
return { missionAnchor: assessment.derived, goalText: editedText.trim(), analysis };
|
|
1075
1352
|
}
|
|
1076
1353
|
const missionAnchor = await confirmMissionAnchor(ctx, assessment);
|
|
1077
1354
|
if (!missionAnchor) return undefined;
|
|
1078
|
-
return { missionAnchor, goalText: editedText.trim() };
|
|
1355
|
+
return { missionAnchor, goalText: editedText.trim(), analysis };
|
|
1079
1356
|
}
|
|
1080
1357
|
|
|
1081
1358
|
async function resolveContextProposalConfirmationAction(
|
|
@@ -1087,8 +1364,9 @@ async function resolveContextProposalConfirmationAction(
|
|
|
1087
1364
|
editedTextOverride?: string,
|
|
1088
1365
|
): Promise<ContextProposalDecision | undefined> {
|
|
1089
1366
|
if (action === "cancel") return undefined;
|
|
1367
|
+
const analysis = finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]);
|
|
1090
1368
|
if (action === "start") {
|
|
1091
|
-
return { missionAnchor: proposal.mission, goalText: proposal.goalText };
|
|
1369
|
+
return { missionAnchor: proposal.mission, goalText: proposal.goalText, analysis };
|
|
1092
1370
|
}
|
|
1093
1371
|
const editedText =
|
|
1094
1372
|
editedTextOverride ??
|
|
@@ -1097,7 +1375,7 @@ async function resolveContextProposalConfirmationAction(
|
|
|
1097
1375
|
buildContextProposalEditorText(proposal),
|
|
1098
1376
|
));
|
|
1099
1377
|
if (!editedText?.trim()) return undefined;
|
|
1100
|
-
return await resolveEditedContextProposalDecision(ctx, projectName, editedText, editedTextOverride === undefined);
|
|
1378
|
+
return await resolveEditedContextProposalDecision(ctx, projectName, editedText, editedTextOverride === undefined, analysis);
|
|
1101
1379
|
}
|
|
1102
1380
|
|
|
1103
1381
|
function buildContextProposalEditorText(proposal: ContextProposal): string {
|
|
@@ -1113,14 +1391,25 @@ function parseContextProposal(text: string, projectName: string): ContextProposa
|
|
|
1113
1391
|
.filter((line) => line.length > 0);
|
|
1114
1392
|
if (lines.length === 0) return undefined;
|
|
1115
1393
|
|
|
1116
|
-
let section:
|
|
1394
|
+
let section: ContextProposalSection | undefined;
|
|
1117
1395
|
let missionLine: string | undefined;
|
|
1396
|
+
let taskTypeHint: string | undefined;
|
|
1397
|
+
let evaluationProfileHint: string | undefined;
|
|
1118
1398
|
const scope: string[] = [];
|
|
1119
1399
|
const constraints: string[] = [];
|
|
1120
1400
|
const acceptance: string[] = [];
|
|
1401
|
+
const critique: string[] = [];
|
|
1402
|
+
const risks: string[] = [];
|
|
1121
1403
|
let structuredSignalCount = 0;
|
|
1122
1404
|
|
|
1123
1405
|
for (const rawLine of lines) {
|
|
1406
|
+
const routingHint = matchContextProposalRoutingHint(rawLine);
|
|
1407
|
+
if (routingHint) {
|
|
1408
|
+
structuredSignalCount += 1;
|
|
1409
|
+
if (routingHint.field === "taskType") taskTypeHint = routingHint.value;
|
|
1410
|
+
else evaluationProfileHint = routingHint.value;
|
|
1411
|
+
continue;
|
|
1412
|
+
}
|
|
1124
1413
|
const inlineSection = matchInlineProposalSection(rawLine);
|
|
1125
1414
|
if (inlineSection) {
|
|
1126
1415
|
section = inlineSection.section;
|
|
@@ -1133,6 +1422,10 @@ function parseContextProposal(text: string, projectName: string): ContextProposa
|
|
|
1133
1422
|
acceptance.push(inlineSection.content);
|
|
1134
1423
|
} else if (inlineSection.section === "scope") {
|
|
1135
1424
|
scope.push(inlineSection.content);
|
|
1425
|
+
} else if (inlineSection.section === "critique") {
|
|
1426
|
+
critique.push(inlineSection.content);
|
|
1427
|
+
} else if (inlineSection.section === "risks") {
|
|
1428
|
+
risks.push(inlineSection.content);
|
|
1136
1429
|
}
|
|
1137
1430
|
continue;
|
|
1138
1431
|
}
|
|
@@ -1161,6 +1454,14 @@ function parseContextProposal(text: string, projectName: string): ContextProposa
|
|
|
1161
1454
|
scope.push(bullet);
|
|
1162
1455
|
continue;
|
|
1163
1456
|
}
|
|
1457
|
+
if (section === "critique") {
|
|
1458
|
+
critique.push(bullet);
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
if (section === "risks") {
|
|
1462
|
+
risks.push(bullet);
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1164
1465
|
if (!missionLine) {
|
|
1165
1466
|
missionLine = bullet;
|
|
1166
1467
|
continue;
|
|
@@ -1176,6 +1477,14 @@ function parseContextProposal(text: string, projectName: string): ContextProposa
|
|
|
1176
1477
|
missionLine = normalized;
|
|
1177
1478
|
continue;
|
|
1178
1479
|
}
|
|
1480
|
+
if (section === "critique") {
|
|
1481
|
+
critique.push(normalized);
|
|
1482
|
+
continue;
|
|
1483
|
+
}
|
|
1484
|
+
if (section === "risks") {
|
|
1485
|
+
risks.push(normalized);
|
|
1486
|
+
continue;
|
|
1487
|
+
}
|
|
1179
1488
|
if (section === "constraints" || looksLikeConstraint(normalized)) {
|
|
1180
1489
|
constraints.push(normalized);
|
|
1181
1490
|
continue;
|
|
@@ -1193,17 +1502,25 @@ function parseContextProposal(text: string, projectName: string): ContextProposa
|
|
|
1193
1502
|
const missionSource = missionLine ?? scope[0] ?? acceptance[0] ?? constraints[0] ?? basisPreview;
|
|
1194
1503
|
const assessment = assessMissionAnchor(missionSource, projectName);
|
|
1195
1504
|
const normalizedMission = normalizeMissionAnchorText(missionSource);
|
|
1196
|
-
const itemCount = scope.length + constraints.length + acceptance.length;
|
|
1505
|
+
const itemCount = scope.length + constraints.length + acceptance.length + critique.length + risks.length;
|
|
1197
1506
|
const hasStrongStructure = structuredSignalCount >= 2 || itemCount >= 2;
|
|
1198
1507
|
if (!normalizedMission || isWeakMissionAnchor(normalizedMission)) return undefined;
|
|
1199
1508
|
if (!hasStrongStructure && basisPreview.length < 140) return undefined;
|
|
1200
1509
|
const mission = assessment.derived;
|
|
1510
|
+
const analysis = buildContextProposalAnalysis({
|
|
1511
|
+
taskType: taskTypeHint,
|
|
1512
|
+
evaluationProfile: evaluationProfileHint,
|
|
1513
|
+
critique,
|
|
1514
|
+
risks,
|
|
1515
|
+
hintTexts: [cleaned, mission, ...scope, ...constraints, ...acceptance, ...critique, ...risks],
|
|
1516
|
+
});
|
|
1201
1517
|
const goalText = buildContextProposalGoalText({ mission, scope, constraints, acceptance });
|
|
1202
1518
|
return {
|
|
1203
1519
|
mission,
|
|
1204
1520
|
scope,
|
|
1205
1521
|
constraints,
|
|
1206
1522
|
acceptance,
|
|
1523
|
+
analysis,
|
|
1207
1524
|
goalText,
|
|
1208
1525
|
basisPreview,
|
|
1209
1526
|
source: "session",
|
|
@@ -1234,15 +1551,20 @@ async function buildGoalAnchoredContextProposal(
|
|
|
1234
1551
|
const scope = uniqueProposalItems([...explicitScope, ...sessionScope]);
|
|
1235
1552
|
const constraints = uniqueProposalItems([...(explicit?.constraints ?? []), ...(sessionProposal?.constraints ?? [])]);
|
|
1236
1553
|
const acceptance = uniqueProposalItems([...(explicit?.acceptance ?? []), ...(sessionProposal?.acceptance ?? [])]);
|
|
1554
|
+
const analysis = mergeContextProposalAnalysis(
|
|
1555
|
+
[explicit?.analysis, sessionProposal?.analysis],
|
|
1556
|
+
[goal, mission, ...(sessionProposal?.analysis.possibleNoise ?? []), ...scope, ...constraints, ...acceptance],
|
|
1557
|
+
);
|
|
1237
1558
|
const goalText = buildContextProposalGoalText({ mission, scope, constraints, acceptance });
|
|
1238
1559
|
return {
|
|
1239
1560
|
mission,
|
|
1240
1561
|
scope,
|
|
1241
1562
|
constraints,
|
|
1242
1563
|
acceptance,
|
|
1564
|
+
analysis,
|
|
1243
1565
|
goalText,
|
|
1244
1566
|
basisPreview: sessionProposal?.basisPreview ?? explicit?.basisPreview ?? goal,
|
|
1245
|
-
source: sessionProposal?.source ?? "session",
|
|
1567
|
+
source: sessionProposal?.source ?? explicit?.source ?? "session",
|
|
1246
1568
|
};
|
|
1247
1569
|
}
|
|
1248
1570
|
|
|
@@ -1252,15 +1574,26 @@ async function confirmContextProposal(
|
|
|
1252
1574
|
projectName: string,
|
|
1253
1575
|
options: ContextProposalConfirmOptions,
|
|
1254
1576
|
): Promise<ContextProposalDecision | undefined> {
|
|
1577
|
+
maybeWriteContextProposalSnapshot(proposal);
|
|
1255
1578
|
const actionOverride = completionTestContextProposalActionOverride();
|
|
1256
1579
|
if (actionOverride === "cancel") return undefined;
|
|
1257
1580
|
if (actionOverride === "accept") {
|
|
1258
|
-
return {
|
|
1581
|
+
return {
|
|
1582
|
+
missionAnchor: proposal.mission,
|
|
1583
|
+
goalText: proposal.goalText,
|
|
1584
|
+
analysis: finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]),
|
|
1585
|
+
};
|
|
1259
1586
|
}
|
|
1260
1587
|
if (actionOverride === "edit") {
|
|
1261
1588
|
const editedText = completionTestContextProposalEditText();
|
|
1262
1589
|
if (!editedText) return undefined;
|
|
1263
|
-
return await resolveEditedContextProposalDecision(
|
|
1590
|
+
return await resolveEditedContextProposalDecision(
|
|
1591
|
+
ctx,
|
|
1592
|
+
projectName,
|
|
1593
|
+
editedText,
|
|
1594
|
+
false,
|
|
1595
|
+
finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]),
|
|
1596
|
+
);
|
|
1264
1597
|
}
|
|
1265
1598
|
const layout = buildContextProposalConfirmationLayout(options.title, proposal);
|
|
1266
1599
|
maybeWriteContextProposalConfirmationSnapshot(layout);
|
|
@@ -1277,13 +1610,21 @@ async function confirmContextProposal(
|
|
|
1277
1610
|
}
|
|
1278
1611
|
if (!getCtxHasUI(ctx)) {
|
|
1279
1612
|
return options.nonInteractiveBehavior === "accept"
|
|
1280
|
-
? {
|
|
1613
|
+
? {
|
|
1614
|
+
missionAnchor: proposal.mission,
|
|
1615
|
+
goalText: proposal.goalText,
|
|
1616
|
+
analysis: finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]),
|
|
1617
|
+
}
|
|
1281
1618
|
: undefined;
|
|
1282
1619
|
}
|
|
1283
1620
|
const ui = getCtxUi(ctx);
|
|
1284
1621
|
if (!ui) {
|
|
1285
1622
|
return options.nonInteractiveBehavior === "accept"
|
|
1286
|
-
? {
|
|
1623
|
+
? {
|
|
1624
|
+
missionAnchor: proposal.mission,
|
|
1625
|
+
goalText: proposal.goalText,
|
|
1626
|
+
analysis: finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]),
|
|
1627
|
+
}
|
|
1287
1628
|
: undefined;
|
|
1288
1629
|
}
|
|
1289
1630
|
const choice = await promptContextProposalConfirmationAction(ui, layout);
|
|
@@ -1300,6 +1641,202 @@ function currentMissionAnchor(snapshot: CompletionStateSnapshot): string {
|
|
|
1300
1641
|
);
|
|
1301
1642
|
}
|
|
1302
1643
|
|
|
1644
|
+
function currentTaskType(snapshot: CompletionStateSnapshot): string | undefined {
|
|
1645
|
+
return (
|
|
1646
|
+
asString(snapshot.active?.task_type) ??
|
|
1647
|
+
asString(snapshot.state?.task_type) ??
|
|
1648
|
+
asString(snapshot.plan?.task_type) ??
|
|
1649
|
+
asString(snapshot.profile?.task_type)
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
function currentEvaluationProfile(snapshot: CompletionStateSnapshot): string | undefined {
|
|
1654
|
+
return (
|
|
1655
|
+
asString(snapshot.active?.evaluation_profile) ??
|
|
1656
|
+
asString(snapshot.state?.evaluation_profile) ??
|
|
1657
|
+
asString(snapshot.plan?.evaluation_profile) ??
|
|
1658
|
+
asString(snapshot.profile?.evaluation_profile)
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
function completionContinuationFingerprint(snapshot: CompletionStateSnapshot): string | undefined {
|
|
1663
|
+
if (asString(snapshot.state?.continuation_policy) !== "continue") return undefined;
|
|
1664
|
+
const nextMandatoryRole = asString(snapshot.state?.next_mandatory_role);
|
|
1665
|
+
if (!nextMandatoryRole) return undefined;
|
|
1666
|
+
return JSON.stringify({
|
|
1667
|
+
mission_anchor: asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? null,
|
|
1668
|
+
task_type: currentTaskType(snapshot) ?? null,
|
|
1669
|
+
evaluation_profile: currentEvaluationProfile(snapshot) ?? null,
|
|
1670
|
+
current_phase: asString(snapshot.state?.current_phase) ?? null,
|
|
1671
|
+
next_mandatory_role: nextMandatoryRole,
|
|
1672
|
+
next_mandatory_action: asString(snapshot.state?.next_mandatory_action) ?? null,
|
|
1673
|
+
active_status: asString(snapshot.active?.status) ?? null,
|
|
1674
|
+
active_slice_id: asString(snapshot.active?.slice_id) ?? asString(snapshot.activeSlice?.slice_id) ?? null,
|
|
1675
|
+
latest_completed_slice: asString(snapshot.state?.latest_completed_slice) ?? null,
|
|
1676
|
+
latest_verified_slice: asString(snapshot.state?.latest_verified_slice) ?? null,
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
function noteQueuedDriverPrompt(rootKey: string, fingerprint: string): void {
|
|
1681
|
+
const tracker = driverContinuationByRoot.get(rootKey);
|
|
1682
|
+
if (tracker && tracker.fingerprint === fingerprint) {
|
|
1683
|
+
tracker.attempts += 1;
|
|
1684
|
+
tracker.inFlight = false;
|
|
1685
|
+
tracker.warned = false;
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
driverContinuationByRoot.set(rootKey, {
|
|
1689
|
+
fingerprint,
|
|
1690
|
+
attempts: 1,
|
|
1691
|
+
inFlight: false,
|
|
1692
|
+
warned: false,
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function markQueuedDriverPromptInFlight(rootKey: string, fingerprint: string): void {
|
|
1697
|
+
const tracker = driverContinuationByRoot.get(rootKey);
|
|
1698
|
+
if (!tracker || tracker.fingerprint !== fingerprint) return;
|
|
1699
|
+
tracker.inFlight = true;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
function clearDriverContinuationTracker(rootKey: string): void {
|
|
1703
|
+
driverContinuationByRoot.delete(rootKey);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
function hasRunningCompletionRole(rootKey: string): boolean {
|
|
1707
|
+
return liveRoleActivityByRoot.get(rootKey)?.status === "running";
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function isWorkflowDriverActive(snapshot: CompletionStateSnapshot | undefined): boolean {
|
|
1711
|
+
return Boolean(snapshot) && asString(snapshot?.state?.continuation_policy) === "continue";
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
function isDriverContinuationStateParked(rootKey: string, fingerprint: string): boolean {
|
|
1715
|
+
const tracker = driverContinuationByRoot.get(rootKey);
|
|
1716
|
+
if (!tracker || tracker.fingerprint !== fingerprint) return false;
|
|
1717
|
+
return tracker.warned;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
function rememberParkedDriverContinuation(rootKey: string, fingerprint: string): void {
|
|
1721
|
+
const tracker = driverContinuationByRoot.get(rootKey);
|
|
1722
|
+
if (!tracker || tracker.fingerprint !== fingerprint) return;
|
|
1723
|
+
tracker.warned = true;
|
|
1724
|
+
tracker.inFlight = false;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
async function queueCompletionDriverPrompt(
|
|
1728
|
+
pi: ExtensionAPI,
|
|
1729
|
+
ctx: { cwd: string; hasUI: boolean; ui: any },
|
|
1730
|
+
rootKey: string,
|
|
1731
|
+
fingerprint: string,
|
|
1732
|
+
prompt: string,
|
|
1733
|
+
kind: "kickoff" | "resume" | "auto-resume",
|
|
1734
|
+
): Promise<boolean> {
|
|
1735
|
+
const snapshotPath = kind === "auto-resume" ? completionTestAutoContinuePromptPath() : completionTestDriverPromptPath();
|
|
1736
|
+
maybeWriteTestSnapshot(snapshotPath, `${prompt}\n`);
|
|
1737
|
+
noteQueuedDriverPrompt(rootKey, fingerprint);
|
|
1738
|
+
if (shouldSkipDriverKickoffForTests()) {
|
|
1739
|
+
emitCommandText(ctx, `Skipped completion workflow ${kind} prompt (test mode)`, "info");
|
|
1740
|
+
return false;
|
|
1741
|
+
}
|
|
1742
|
+
pi.sendUserMessage(prompt);
|
|
1743
|
+
emitCommandText(ctx, `Queued completion workflow ${kind}`, "info");
|
|
1744
|
+
return true;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
async function autoContinueWorkflowIfNeeded(pi: ExtensionAPI, ctx: { cwd: string; hasUI: boolean; ui: any }): Promise<void> {
|
|
1748
|
+
if (roleFromEnv()) return;
|
|
1749
|
+
const snapshot = await loadCompletionSnapshot(getCtxCwd(ctx));
|
|
1750
|
+
const rootKey = completionRootKey(snapshot, getCtxCwd(ctx));
|
|
1751
|
+
if (!snapshot) {
|
|
1752
|
+
clearDriverContinuationTracker(rootKey);
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
const fingerprint = completionContinuationFingerprint(snapshot);
|
|
1756
|
+
if (!fingerprint) {
|
|
1757
|
+
clearDriverContinuationTracker(rootKey);
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
if (!isWorkflowDriverActive(snapshot) || hasRunningCompletionRole(rootKey)) return;
|
|
1761
|
+
const tracker = driverContinuationByRoot.get(rootKey);
|
|
1762
|
+
if (tracker && tracker.fingerprint === fingerprint) {
|
|
1763
|
+
if (tracker.inFlight) {
|
|
1764
|
+
tracker.inFlight = false;
|
|
1765
|
+
if (tracker.attempts >= DRIVER_AUTO_CONTINUE_MAX_ATTEMPTS) {
|
|
1766
|
+
if (!isDriverContinuationStateParked(rootKey, fingerprint)) {
|
|
1767
|
+
rememberParkedDriverContinuation(rootKey, fingerprint);
|
|
1768
|
+
emitCommandText(
|
|
1769
|
+
ctx,
|
|
1770
|
+
`Completion workflow is parked before mandatory role dispatch: ${asString(snapshot.state?.next_mandatory_role) ?? "(unknown)"}. Rerun /cook to continue from canonical state.`,
|
|
1771
|
+
"warning",
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
} else {
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
const resumePrompt = completionResumePrompt(currentTaskType(snapshot) ?? "(missing)", currentEvaluationProfile(snapshot) ?? "(missing)");
|
|
1781
|
+
await queueCompletionDriverPrompt(pi, ctx, rootKey, fingerprint, resumePrompt, "auto-resume");
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
function isRubricEvaluationRole(role: string | undefined): role is RubricEvaluationRole {
|
|
1785
|
+
return RUBRIC_EVALUATION_ROLES.includes(role as RubricEvaluationRole);
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
function activeSliceContext(snapshot: CompletionStateSnapshot) {
|
|
1789
|
+
const active = snapshot.active;
|
|
1790
|
+
const activeSlice = snapshot.activeSlice;
|
|
1791
|
+
return {
|
|
1792
|
+
sliceId: asString(active?.slice_id) ?? asString(activeSlice?.slice_id),
|
|
1793
|
+
status: asString(active?.status) ?? asString(activeSlice?.status),
|
|
1794
|
+
goal: asString(active?.goal) ?? asString(activeSlice?.goal),
|
|
1795
|
+
contractIds:
|
|
1796
|
+
asStringArray(active?.contract_ids).length > 0 ? asStringArray(active?.contract_ids) : asStringArray(activeSlice?.contract_ids),
|
|
1797
|
+
acceptance:
|
|
1798
|
+
asStringArray(active?.acceptance_criteria).length > 0
|
|
1799
|
+
? asStringArray(active?.acceptance_criteria)
|
|
1800
|
+
: asStringArray(activeSlice?.acceptance_criteria),
|
|
1801
|
+
implementationSurfaces: asStringArray(active?.implementation_surfaces),
|
|
1802
|
+
verificationCommands: asStringArray(active?.verification_commands),
|
|
1803
|
+
lockedNotes: asStringArray(active?.locked_notes),
|
|
1804
|
+
mustFixFindings: asStringArray(active?.must_fix_findings),
|
|
1805
|
+
remainingBefore: asStringArray(active?.remaining_contract_ids_before),
|
|
1806
|
+
basisCommit: asString(active?.basis_commit),
|
|
1807
|
+
releaseBlockerCountBefore: asNumber(active?.release_blocker_count_before),
|
|
1808
|
+
highValueGapCountBefore: asNumber(active?.high_value_gap_count_before),
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function buildEvaluationRoleContextLines(snapshot: CompletionStateSnapshot, role: RubricEvaluationRole): string[] {
|
|
1813
|
+
const context = activeSliceContext(snapshot);
|
|
1814
|
+
const lines = [
|
|
1815
|
+
`Canonical evaluation handoff for ${role}:`,
|
|
1816
|
+
`- task_type: ${currentTaskType(snapshot) ?? "(missing)"}`,
|
|
1817
|
+
`- evaluation_profile: ${currentEvaluationProfile(snapshot) ?? "(missing)"}`,
|
|
1818
|
+
`- latest_completed_slice: ${asString(snapshot.state?.latest_completed_slice) ?? "(none)"}`,
|
|
1819
|
+
`- active_slice_id: ${context.sliceId ?? "(none)"}`,
|
|
1820
|
+
`- active_slice_status: ${context.status ?? "(unknown)"}`,
|
|
1821
|
+
`- active_slice_goal: ${context.goal ?? "(unknown)"}`,
|
|
1822
|
+
`- contract_ids: ${context.contractIds.length > 0 ? context.contractIds.join(", ") : "(none)"}`,
|
|
1823
|
+
`- acceptance_criteria: ${context.acceptance.length > 0 ? context.acceptance.join(" | ") : "(none)"}`,
|
|
1824
|
+
`- implementation_surfaces: ${context.implementationSurfaces.length > 0 ? context.implementationSurfaces.join(" | ") : "(none)"}`,
|
|
1825
|
+
`- verification_commands: ${context.verificationCommands.length > 0 ? context.verificationCommands.join(" | ") : "(none)"}`,
|
|
1826
|
+
`- locked_notes: ${context.lockedNotes.length > 0 ? context.lockedNotes.join(" | ") : "(none)"}`,
|
|
1827
|
+
`- must_fix_findings: ${context.mustFixFindings.length > 0 ? context.mustFixFindings.join(" | ") : "(none)"}`,
|
|
1828
|
+
`- basis_commit: ${context.basisCommit ?? "(none)"}`,
|
|
1829
|
+
`- remaining_contract_ids_before: ${context.remainingBefore.length > 0 ? context.remainingBefore.join(", ") : "(none)"}`,
|
|
1830
|
+
`- release_blocker_count_before: ${context.releaseBlockerCountBefore ?? "(unknown)"}`,
|
|
1831
|
+
`- high_value_gap_count_before: ${context.highValueGapCountBefore ?? "(unknown)"}`,
|
|
1832
|
+
];
|
|
1833
|
+
return lines;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
function buildEvaluationRoleReminderText(snapshot: CompletionStateSnapshot, role: RubricEvaluationRole): string {
|
|
1837
|
+
return buildEvaluationRoleContextLines(snapshot, role).join(" ");
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1303
1840
|
async function confirmExistingWorkflowGoal(
|
|
1304
1841
|
ctx: { hasUI: boolean; ui: any },
|
|
1305
1842
|
snapshot: CompletionStateSnapshot,
|
|
@@ -1351,22 +1888,41 @@ async function confirmExistingWorkflowGoal(
|
|
|
1351
1888
|
return { action: "continue", currentMissionAnchor: currentMission };
|
|
1352
1889
|
}
|
|
1353
1890
|
|
|
1354
|
-
async function refocusCompletionMission(
|
|
1891
|
+
async function refocusCompletionMission(
|
|
1892
|
+
snapshot: CompletionStateSnapshot,
|
|
1893
|
+
missionAnchor: string,
|
|
1894
|
+
rawGoal: string,
|
|
1895
|
+
analysis?: ContextProposalAnalysis,
|
|
1896
|
+
): Promise<void> {
|
|
1355
1897
|
const requiredStopJudges = asNumber(snapshot.profile?.required_stop_judges) ?? 3;
|
|
1356
1898
|
const root = snapshot.files.root;
|
|
1899
|
+
const routing = finalizeContextProposalAnalysis(analysis, [rawGoal, missionAnchor]);
|
|
1900
|
+
const docsSurfaces = asStringArray(snapshot.profile?.docs_surfaces);
|
|
1901
|
+
const nextProfile = buildProfileRecord({
|
|
1902
|
+
projectName: asString(snapshot.profile?.project_name) ?? path.basename(root),
|
|
1903
|
+
requiredStopJudges,
|
|
1904
|
+
priorityPolicyId: asString(snapshot.profile?.priority_policy_id) ?? "completion-default",
|
|
1905
|
+
docsSurfaces: docsSurfaces.length > 0 ? docsSurfaces : await detectDocsSurfaces(root),
|
|
1906
|
+
taskType: routing.taskType,
|
|
1907
|
+
evaluationProfile: routing.evaluationProfile,
|
|
1908
|
+
});
|
|
1357
1909
|
const nextState = {
|
|
1358
|
-
...defaultState(missionAnchor
|
|
1910
|
+
...defaultState(missionAnchor, {
|
|
1911
|
+
taskType: routing.taskType,
|
|
1912
|
+
evaluationProfile: routing.evaluationProfile,
|
|
1913
|
+
continuationReason: buildContextProposalContinuationReason("User refocused workflow via /cook:", rawGoal, routing),
|
|
1914
|
+
}),
|
|
1359
1915
|
remaining_stop_judges: requiredStopJudges,
|
|
1360
|
-
continuation_reason: `User refocused workflow via /cook: ${truncateInline(rawGoal, 160)}`,
|
|
1361
1916
|
next_mandatory_action: "Reconcile canonical state from current repo truth for the refocused mission",
|
|
1362
1917
|
};
|
|
1363
1918
|
const nextPlan = {
|
|
1364
|
-
...defaultPlan(missionAnchor),
|
|
1919
|
+
...defaultPlan(missionAnchor, { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile }),
|
|
1365
1920
|
plan_basis: "user_refocus",
|
|
1366
1921
|
};
|
|
1367
|
-
const nextActive = defaultActiveSlice(missionAnchor);
|
|
1922
|
+
const nextActive = defaultActiveSlice(missionAnchor, { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile });
|
|
1368
1923
|
await Promise.all([
|
|
1369
1924
|
fsp.writeFile(path.join(snapshot.files.agentDir, "mission.md"), buildMission(path.basename(root), missionAnchor), "utf8"),
|
|
1925
|
+
writeJsonFile(snapshot.files.profilePath, nextProfile),
|
|
1370
1926
|
writeJsonFile(snapshot.files.statePath, nextState),
|
|
1371
1927
|
writeJsonFile(snapshot.files.planPath, nextPlan),
|
|
1372
1928
|
writeJsonFile(snapshot.files.activePath, nextActive),
|
|
@@ -1398,14 +1954,39 @@ function deriveMissionAnchor(rawGoal: string, projectName: string): string {
|
|
|
1398
1954
|
return mission;
|
|
1399
1955
|
}
|
|
1400
1956
|
|
|
1401
|
-
function
|
|
1957
|
+
function buildProfileRecord(args: {
|
|
1958
|
+
projectName: string;
|
|
1959
|
+
requiredStopJudges: number;
|
|
1960
|
+
priorityPolicyId?: string;
|
|
1961
|
+
docsSurfaces: string[];
|
|
1962
|
+
taskType?: string;
|
|
1963
|
+
evaluationProfile?: string;
|
|
1964
|
+
}): JsonRecord {
|
|
1965
|
+
return {
|
|
1966
|
+
schema_version: 1,
|
|
1967
|
+
protocol_id: PROTOCOL_ID,
|
|
1968
|
+
project_name: args.projectName,
|
|
1969
|
+
required_stop_judges: args.requiredStopJudges,
|
|
1970
|
+
priority_policy_id: args.priorityPolicyId ?? "completion-default",
|
|
1971
|
+
task_type: args.taskType ?? DEFAULT_TASK_TYPE,
|
|
1972
|
+
evaluation_profile: args.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
|
|
1973
|
+
docs_surfaces: args.docsSurfaces,
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
function defaultState(
|
|
1978
|
+
missionAnchor: string,
|
|
1979
|
+
routing?: { taskType?: string; evaluationProfile?: string; continuationReason?: string },
|
|
1980
|
+
): JsonRecord {
|
|
1402
1981
|
return {
|
|
1403
1982
|
schema_version: 1,
|
|
1404
1983
|
mission_anchor: missionAnchor,
|
|
1405
1984
|
current_phase: "reground",
|
|
1406
1985
|
continuation_policy: "continue",
|
|
1407
|
-
continuation_reason: "Fresh completion bootstrap requires canonical re-ground",
|
|
1986
|
+
continuation_reason: routing?.continuationReason ?? "Fresh completion bootstrap requires canonical re-ground",
|
|
1408
1987
|
project_done: false,
|
|
1988
|
+
task_type: routing?.taskType ?? DEFAULT_TASK_TYPE,
|
|
1989
|
+
evaluation_profile: routing?.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
|
|
1409
1990
|
requires_reground: true,
|
|
1410
1991
|
slices_since_last_reground: 0,
|
|
1411
1992
|
remaining_release_blockers: null,
|
|
@@ -1423,20 +2004,30 @@ function defaultState(missionAnchor: string): JsonRecord {
|
|
|
1423
2004
|
};
|
|
1424
2005
|
}
|
|
1425
2006
|
|
|
1426
|
-
function defaultPlan(
|
|
2007
|
+
function defaultPlan(
|
|
2008
|
+
missionAnchor: string,
|
|
2009
|
+
routing?: { taskType?: string; evaluationProfile?: string },
|
|
2010
|
+
): JsonRecord {
|
|
1427
2011
|
return {
|
|
1428
2012
|
schema_version: 1,
|
|
1429
2013
|
mission_anchor: missionAnchor,
|
|
2014
|
+
task_type: routing?.taskType ?? DEFAULT_TASK_TYPE,
|
|
2015
|
+
evaluation_profile: routing?.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
|
|
1430
2016
|
last_reground_at: null,
|
|
1431
2017
|
plan_basis: "bootstrap",
|
|
1432
2018
|
candidate_slices: [],
|
|
1433
2019
|
};
|
|
1434
2020
|
}
|
|
1435
2021
|
|
|
1436
|
-
function defaultActiveSlice(
|
|
2022
|
+
function defaultActiveSlice(
|
|
2023
|
+
missionAnchor: string,
|
|
2024
|
+
routing?: { taskType?: string; evaluationProfile?: string },
|
|
2025
|
+
): JsonRecord {
|
|
1437
2026
|
return {
|
|
1438
2027
|
schema_version: 1,
|
|
1439
2028
|
mission_anchor: missionAnchor,
|
|
2029
|
+
task_type: routing?.taskType ?? DEFAULT_TASK_TYPE,
|
|
2030
|
+
evaluation_profile: routing?.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
|
|
1440
2031
|
status: "idle",
|
|
1441
2032
|
slice_id: null,
|
|
1442
2033
|
goal: null,
|
|
@@ -1447,6 +2038,8 @@ function defaultActiveSlice(missionAnchor: string): JsonRecord {
|
|
|
1447
2038
|
blocked_on: [],
|
|
1448
2039
|
locked_notes: [],
|
|
1449
2040
|
must_fix_findings: [],
|
|
2041
|
+
implementation_surfaces: [],
|
|
2042
|
+
verification_commands: [],
|
|
1450
2043
|
basis_commit: null,
|
|
1451
2044
|
remaining_contract_ids_before: [],
|
|
1452
2045
|
release_blocker_count_before: null,
|
|
@@ -1470,15 +2063,151 @@ function buildVerifyStopScript(verifierCommand?: string): string {
|
|
|
1470
2063
|
}
|
|
1471
2064
|
|
|
1472
2065
|
function buildVerifyControlPlaneScript(): string {
|
|
1473
|
-
return `#!/usr/bin/env bash
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
.agent/
|
|
1478
|
-
.agent/
|
|
1479
|
-
.agent/
|
|
1480
|
-
.agent/
|
|
1481
|
-
.agent/
|
|
2066
|
+
return `#!/usr/bin/env bash
|
|
2067
|
+
set -euo pipefail
|
|
2068
|
+
|
|
2069
|
+
for file in \
|
|
2070
|
+
.agent/README.md \
|
|
2071
|
+
.agent/mission.md \
|
|
2072
|
+
.agent/profile.json \
|
|
2073
|
+
.agent/verify_completion_stop.sh \
|
|
2074
|
+
.agent/verify_completion_control_plane.sh \
|
|
2075
|
+
.agent/state.json \
|
|
2076
|
+
.agent/plan.json \
|
|
2077
|
+
.agent/active-slice.json; do
|
|
2078
|
+
[[ -e "$file" ]] || { echo "missing required file: $file"; exit 1; }
|
|
2079
|
+
done
|
|
2080
|
+
|
|
2081
|
+
node <<'NODE'
|
|
2082
|
+
const fs = require('node:fs');
|
|
2083
|
+
|
|
2084
|
+
const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
2085
|
+
const assert = (condition, message) => {
|
|
2086
|
+
if (!condition) {
|
|
2087
|
+
console.error(message);
|
|
2088
|
+
process.exit(1);
|
|
2089
|
+
}
|
|
2090
|
+
};
|
|
2091
|
+
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
2092
|
+
const isString = (value) => typeof value === 'string';
|
|
2093
|
+
const isNonEmptyString = (value) => isString(value) && value.length > 0;
|
|
2094
|
+
const isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
2095
|
+
const hasOnlyKeys = (object, allowed, label) => {
|
|
2096
|
+
const unknown = Object.keys(object).filter((key) => !allowed.includes(key));
|
|
2097
|
+
assert(unknown.length === 0, label + ': unknown keys: ' + unknown.join(', '));
|
|
2098
|
+
};
|
|
2099
|
+
const requireKeys = (object, required, label) => {
|
|
2100
|
+
for (const key of required) {
|
|
2101
|
+
assert(Object.prototype.hasOwnProperty.call(object, key), label + ': missing required field: ' + key);
|
|
2102
|
+
}
|
|
2103
|
+
};
|
|
2104
|
+
|
|
2105
|
+
for (const file of ['.agent/profile.json', '.agent/state.json', '.agent/plan.json', '.agent/active-slice.json']) {
|
|
2106
|
+
readJson(file);
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
const profile = readJson('.agent/profile.json');
|
|
2110
|
+
const state = readJson('.agent/state.json');
|
|
2111
|
+
const plan = readJson('.agent/plan.json');
|
|
2112
|
+
const active = readJson('.agent/active-slice.json');
|
|
2113
|
+
|
|
2114
|
+
assert(isObject(profile), '.agent/profile.json must be an object');
|
|
2115
|
+
assert(isObject(state), '.agent/state.json must be an object');
|
|
2116
|
+
assert(isObject(plan), '.agent/plan.json must be an object');
|
|
2117
|
+
assert(isObject(active), '.agent/active-slice.json must be an object');
|
|
2118
|
+
|
|
2119
|
+
const requiredProfile = ['schema_version', 'protocol_id', 'project_name', 'required_stop_judges', 'priority_policy_id', 'task_type', 'evaluation_profile', 'docs_surfaces'];
|
|
2120
|
+
requireKeys(profile, requiredProfile, '.agent/profile.json');
|
|
2121
|
+
hasOnlyKeys(profile, requiredProfile, '.agent/profile.json');
|
|
2122
|
+
assert(profile.protocol_id === 'completion', '.agent/profile.json: protocol_id must be completion');
|
|
2123
|
+
assert(Array.isArray(profile.docs_surfaces), '.agent/profile.json: docs_surfaces must be an array');
|
|
2124
|
+
assert(isNonEmptyString(profile.task_type), '.agent/profile.json: task_type must be a non-empty string');
|
|
2125
|
+
assert(isNonEmptyString(profile.evaluation_profile), '.agent/profile.json: evaluation_profile must be a non-empty string');
|
|
2126
|
+
|
|
2127
|
+
const requiredState = [
|
|
2128
|
+
'schema_version','mission_anchor','task_type','evaluation_profile','current_phase','continuation_policy','continuation_reason','project_done',
|
|
2129
|
+
'requires_reground','slices_since_last_reground','remaining_release_blockers','remaining_high_value_gaps',
|
|
2130
|
+
'unsatisfied_contract_ids','release_blocker_ids','next_mandatory_action','next_mandatory_role',
|
|
2131
|
+
'remaining_stop_judges','last_reground_at','last_auditor_verdict','contract_status','latest_completed_slice','latest_verified_slice'
|
|
2132
|
+
];
|
|
2133
|
+
const continuationPolicies = ['continue', 'await_user_input', 'blocked', 'paused', 'done'];
|
|
2134
|
+
const workflowRoles = ['completion-bootstrapper', 'completion-regrounder', 'completion-implementer', 'completion-reviewer', 'completion-auditor', 'completion-stop-judge', null];
|
|
2135
|
+
const workflowPhases = ['reground', 'implement', 'post_commit_review', 'post_commit_audit', 'post_commit_reconcile', 'stop_wave', 'awaiting_user', 'blocked', 'done'];
|
|
2136
|
+
requireKeys(state, requiredState, '.agent/state.json');
|
|
2137
|
+
hasOnlyKeys(state, requiredState, '.agent/state.json');
|
|
2138
|
+
assert(continuationPolicies.includes(state.continuation_policy), '.agent/state.json: invalid continuation_policy');
|
|
2139
|
+
assert(workflowRoles.includes(state.next_mandatory_role), '.agent/state.json: invalid next_mandatory_role');
|
|
2140
|
+
assert(workflowPhases.includes(state.current_phase), '.agent/state.json: invalid current_phase');
|
|
2141
|
+
assert(isNonEmptyString(state.task_type), '.agent/state.json: task_type must be a non-empty string');
|
|
2142
|
+
assert(isNonEmptyString(state.evaluation_profile), '.agent/state.json: evaluation_profile must be a non-empty string');
|
|
2143
|
+
assert(isStringArray(state.unsatisfied_contract_ids), '.agent/state.json: unsatisfied_contract_ids must be an array of strings');
|
|
2144
|
+
assert(isStringArray(state.release_blocker_ids), '.agent/state.json: release_blocker_ids must be an array of strings');
|
|
2145
|
+
|
|
2146
|
+
const requiredPlan = ['schema_version', 'mission_anchor', 'task_type', 'evaluation_profile', 'last_reground_at', 'plan_basis', 'candidate_slices'];
|
|
2147
|
+
const requiredSlice = ['slice_id', 'goal', 'acceptance_criteria', 'contract_ids', 'priority', 'status', 'why_now', 'blocked_on', 'evidence'];
|
|
2148
|
+
const sliceStatuses = ['planned', 'selected', 'in_progress', 'blocked', 'done', 'cancelled'];
|
|
2149
|
+
requireKeys(plan, requiredPlan, '.agent/plan.json');
|
|
2150
|
+
hasOnlyKeys(plan, requiredPlan, '.agent/plan.json');
|
|
2151
|
+
assert(isNonEmptyString(plan.task_type), '.agent/plan.json: task_type must be a non-empty string');
|
|
2152
|
+
assert(isNonEmptyString(plan.evaluation_profile), '.agent/plan.json: evaluation_profile must be a non-empty string');
|
|
2153
|
+
assert(Array.isArray(plan.candidate_slices), '.agent/plan.json: candidate_slices must be an array');
|
|
2154
|
+
for (const [index, slice] of plan.candidate_slices.entries()) {
|
|
2155
|
+
const label = '.agent/plan.json candidate_slices[' + index + ']';
|
|
2156
|
+
assert(isObject(slice), label + ' must be an object');
|
|
2157
|
+
requireKeys(slice, requiredSlice, label);
|
|
2158
|
+
hasOnlyKeys(slice, requiredSlice, label);
|
|
2159
|
+
assert(isString(slice.slice_id) && slice.slice_id.length > 0, label + ': slice_id must be a non-empty string');
|
|
2160
|
+
assert(isString(slice.goal) && slice.goal.length > 0, label + ': goal must be a non-empty string');
|
|
2161
|
+
assert(Array.isArray(slice.acceptance_criteria) && slice.acceptance_criteria.length > 0 && slice.acceptance_criteria.every((item) => typeof item === 'string' && item.length > 0), label + ': acceptance_criteria must be a non-empty array of strings');
|
|
2162
|
+
assert(isStringArray(slice.contract_ids), label + ': contract_ids must be an array of strings');
|
|
2163
|
+
assert(typeof slice.priority === 'number' && Number.isFinite(slice.priority), label + ': priority must be a finite number');
|
|
2164
|
+
assert(sliceStatuses.includes(slice.status), label + ': invalid status');
|
|
2165
|
+
assert(isString(slice.why_now) && slice.why_now.length > 0, label + ': why_now must be a non-empty string');
|
|
2166
|
+
assert(isStringArray(slice.blocked_on), label + ': blocked_on must be an array of strings');
|
|
2167
|
+
assert(isStringArray(slice.evidence), label + ': evidence must be an array of strings');
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
const isNonEmptyStringArray = (value) => Array.isArray(value) && value.length > 0 && value.every((item) => isNonEmptyString(item));
|
|
2171
|
+
const requiredActiveBase = ['schema_version', 'mission_anchor', 'task_type', 'evaluation_profile', 'status', 'slice_id', 'goal', 'contract_ids', 'acceptance_criteria', 'blocked_on', 'locked_notes', 'must_fix_findings', 'implementation_surfaces', 'verification_commands', 'basis_commit', 'remaining_contract_ids_before', 'release_blocker_count_before', 'high_value_gap_count_before'];
|
|
2172
|
+
const allowedActive = [...requiredActiveBase, 'priority', 'why_now'];
|
|
2173
|
+
const activeStatuses = ['idle', 'selected', 'in_progress', 'committed', 'done'];
|
|
2174
|
+
requireKeys(active, requiredActiveBase, '.agent/active-slice.json');
|
|
2175
|
+
hasOnlyKeys(active, allowedActive, '.agent/active-slice.json');
|
|
2176
|
+
assert(activeStatuses.includes(active.status), '.agent/active-slice.json: invalid status');
|
|
2177
|
+
assert(isNonEmptyString(active.task_type), '.agent/active-slice.json: task_type must be a non-empty string');
|
|
2178
|
+
assert(isNonEmptyString(active.evaluation_profile), '.agent/active-slice.json: evaluation_profile must be a non-empty string');
|
|
2179
|
+
assert(isStringArray(active.contract_ids), '.agent/active-slice.json: contract_ids must be an array of strings');
|
|
2180
|
+
assert(Array.isArray(active.acceptance_criteria), '.agent/active-slice.json: acceptance_criteria must be an array');
|
|
2181
|
+
assert(isStringArray(active.blocked_on), '.agent/active-slice.json: blocked_on must be an array of strings');
|
|
2182
|
+
assert(isStringArray(active.locked_notes), '.agent/active-slice.json: locked_notes must be an array of strings');
|
|
2183
|
+
assert(isStringArray(active.must_fix_findings), '.agent/active-slice.json: must_fix_findings must be an array of strings');
|
|
2184
|
+
assert(isStringArray(active.implementation_surfaces), '.agent/active-slice.json: implementation_surfaces must be an array of strings');
|
|
2185
|
+
assert(isStringArray(active.verification_commands), '.agent/active-slice.json: verification_commands must be an array of strings');
|
|
2186
|
+
assert(isStringArray(active.remaining_contract_ids_before), '.agent/active-slice.json: remaining_contract_ids_before must be an array of strings');
|
|
2187
|
+
|
|
2188
|
+
assert(state.task_type === profile.task_type, '.agent/state.json: task_type must match .agent/profile.json');
|
|
2189
|
+
assert(plan.task_type === profile.task_type, '.agent/plan.json: task_type must match .agent/profile.json');
|
|
2190
|
+
assert(active.task_type === profile.task_type, '.agent/active-slice.json: task_type must match .agent/profile.json');
|
|
2191
|
+
assert(state.evaluation_profile === profile.evaluation_profile, '.agent/state.json: evaluation_profile must match .agent/profile.json');
|
|
2192
|
+
assert(plan.evaluation_profile === profile.evaluation_profile, '.agent/plan.json: evaluation_profile must match .agent/profile.json');
|
|
2193
|
+
assert(active.evaluation_profile === profile.evaluation_profile, '.agent/active-slice.json: evaluation_profile must match .agent/profile.json');
|
|
2194
|
+
|
|
2195
|
+
const requiresExactHandoff = ['selected', 'in_progress', 'committed', 'done'].includes(active.status);
|
|
2196
|
+
if (requiresExactHandoff) {
|
|
2197
|
+
assert(isNonEmptyStringArray(active.acceptance_criteria), '.agent/active-slice.json: acceptance_criteria must be a non-empty array of strings when status carries an exact handoff');
|
|
2198
|
+
assert(typeof active.priority === 'number' && Number.isFinite(active.priority), '.agent/active-slice.json: priority must be a finite number when status carries an exact handoff');
|
|
2199
|
+
assert(isString(active.why_now) && active.why_now.length > 0, '.agent/active-slice.json: why_now must be a non-empty string when status carries an exact handoff');
|
|
2200
|
+
assert(isNonEmptyStringArray(active.implementation_surfaces), '.agent/active-slice.json: implementation_surfaces must be a non-empty array of strings when status carries an exact handoff');
|
|
2201
|
+
assert(isNonEmptyStringArray(active.verification_commands), '.agent/active-slice.json: verification_commands must be a non-empty array of strings when status carries an exact handoff');
|
|
2202
|
+
assert(isString(active.basis_commit) && active.basis_commit.length > 0, '.agent/active-slice.json: basis_commit must be a non-empty string when status carries an exact handoff');
|
|
2203
|
+
assert(typeof active.release_blocker_count_before === 'number' && Number.isFinite(active.release_blocker_count_before), '.agent/active-slice.json: release_blocker_count_before must be a finite number when status carries an exact handoff');
|
|
2204
|
+
assert(typeof active.high_value_gap_count_before === 'number' && Number.isFinite(active.high_value_gap_count_before), '.agent/active-slice.json: high_value_gap_count_before must be a finite number when status carries an exact handoff');
|
|
2205
|
+
} else {
|
|
2206
|
+
assert(active.priority === null || active.priority === undefined || (typeof active.priority === 'number' && Number.isFinite(active.priority)), '.agent/active-slice.json: idle priority must be null/undefined or a finite number');
|
|
2207
|
+
assert(active.why_now === null || active.why_now === undefined || typeof active.why_now === 'string', '.agent/active-slice.json: idle why_now must be null/undefined or a string');
|
|
2208
|
+
}
|
|
2209
|
+
NODE
|
|
2210
|
+
`;
|
|
1482
2211
|
}
|
|
1483
2212
|
|
|
1484
2213
|
async function ensureGitignore(root: string): Promise<boolean> {
|
|
@@ -1515,13 +2244,18 @@ type ScaffoldResult = {
|
|
|
1515
2244
|
missionAnchor: string;
|
|
1516
2245
|
};
|
|
1517
2246
|
|
|
1518
|
-
async function scaffoldCompletionFiles(
|
|
2247
|
+
async function scaffoldCompletionFiles(
|
|
2248
|
+
root: string,
|
|
2249
|
+
missionAnchor: string,
|
|
2250
|
+
options?: { analysis?: ContextProposalAnalysis; continuationReason?: string },
|
|
2251
|
+
): Promise<ScaffoldResult> {
|
|
1519
2252
|
const files = resolveFiles(root);
|
|
1520
2253
|
const created: string[] = [];
|
|
1521
2254
|
const updated: string[] = [];
|
|
1522
2255
|
await fsp.mkdir(files.agentDir, { recursive: true });
|
|
1523
2256
|
await fsp.mkdir(path.join(files.agentDir, "tmp"), { recursive: true });
|
|
1524
2257
|
const projectName = path.basename(root);
|
|
2258
|
+
const routing = finalizeContextProposalAnalysis(options?.analysis, [missionAnchor]);
|
|
1525
2259
|
const docsSurfaces = await detectDocsSurfaces(root);
|
|
1526
2260
|
const verifierCommand = await detectVerifierCommand(root);
|
|
1527
2261
|
const trackedFiles: Array<{ path: string; content: string; executable?: boolean }> = [
|
|
@@ -1529,13 +2263,16 @@ async function scaffoldCompletionFiles(root: string, missionAnchor: string): Pro
|
|
|
1529
2263
|
{ path: path.join(files.agentDir, "mission.md"), content: buildMission(projectName, missionAnchor) },
|
|
1530
2264
|
{
|
|
1531
2265
|
path: files.profilePath,
|
|
1532
|
-
content: `${JSON.stringify({
|
|
2266
|
+
content: `${JSON.stringify(buildProfileRecord({ projectName, requiredStopJudges: 3, docsSurfaces, taskType: routing.taskType, evaluationProfile: routing.evaluationProfile }), null, 2)}\n`,
|
|
1533
2267
|
},
|
|
1534
2268
|
{ path: path.join(files.agentDir, "verify_completion_stop.sh"), content: buildVerifyStopScript(verifierCommand), executable: true },
|
|
1535
2269
|
{ path: path.join(files.agentDir, "verify_completion_control_plane.sh"), content: buildVerifyControlPlaneScript(), executable: true },
|
|
1536
|
-
{
|
|
1537
|
-
|
|
1538
|
-
|
|
2270
|
+
{
|
|
2271
|
+
path: files.statePath,
|
|
2272
|
+
content: `${JSON.stringify(defaultState(missionAnchor, { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile, continuationReason: options?.continuationReason }), null, 2)}\n`,
|
|
2273
|
+
},
|
|
2274
|
+
{ path: files.planPath, content: `${JSON.stringify(defaultPlan(missionAnchor, { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile }), null, 2)}\n` },
|
|
2275
|
+
{ path: files.activePath, content: `${JSON.stringify(defaultActiveSlice(missionAnchor, { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile }), null, 2)}\n` },
|
|
1539
2276
|
{ path: files.sliceHistoryPath, content: "" },
|
|
1540
2277
|
{ path: files.stopHistoryPath, content: "" },
|
|
1541
2278
|
];
|
|
@@ -1573,8 +2310,12 @@ function activeSliceMatchesPlan(snapshot: CompletionStateSnapshot): "yes" | "no"
|
|
|
1573
2310
|
}
|
|
1574
2311
|
|
|
1575
2312
|
function handoffSnapshotState(active: JsonRecord | undefined): "present" | "missing_or_unclear" {
|
|
2313
|
+
const exactArrays = [
|
|
2314
|
+
asStringArray(active?.acceptance_criteria),
|
|
2315
|
+
asStringArray(active?.implementation_surfaces),
|
|
2316
|
+
asStringArray(active?.verification_commands),
|
|
2317
|
+
];
|
|
1576
2318
|
const required = [
|
|
1577
|
-
active?.acceptance_criteria,
|
|
1578
2319
|
active?.priority,
|
|
1579
2320
|
active?.why_now,
|
|
1580
2321
|
active?.blocked_on,
|
|
@@ -1585,15 +2326,24 @@ function handoffSnapshotState(active: JsonRecord | undefined): "present" | "miss
|
|
|
1585
2326
|
active?.release_blocker_count_before,
|
|
1586
2327
|
active?.high_value_gap_count_before,
|
|
1587
2328
|
];
|
|
1588
|
-
return required.every((value) => value !== undefined && value !== null)
|
|
2329
|
+
return exactArrays.every((items) => items.length > 0) && required.every((value) => value !== undefined && value !== null)
|
|
2330
|
+
? "present"
|
|
2331
|
+
: "missing_or_unclear";
|
|
1589
2332
|
}
|
|
1590
2333
|
|
|
1591
2334
|
function buildSystemReminder(snapshot: CompletionStateSnapshot, sliceHistory: JsonRecord[], stopHistory: JsonRecord[]): string {
|
|
1592
2335
|
const history = historyCounts(sliceHistory, stopHistory);
|
|
1593
|
-
|
|
2336
|
+
const implementationSurfaces = asStringArray(snapshot.active?.implementation_surfaces);
|
|
2337
|
+
const verificationCommands = asStringArray(snapshot.active?.verification_commands);
|
|
2338
|
+
const activePriority = asNumber(snapshot.active?.priority);
|
|
2339
|
+
const activeWhyNow = asString(snapshot.active?.why_now);
|
|
2340
|
+
const nextRole = asString(snapshot.state?.next_mandatory_role);
|
|
2341
|
+
const lines = [
|
|
1594
2342
|
"Completion workflow detected.",
|
|
1595
2343
|
"Canonical truth lives in .agent/state.json, .agent/plan.json, .agent/active-slice.json, .agent/slice-history.jsonl, and .agent/stop-check-history.jsonl.",
|
|
1596
2344
|
`Mission anchor: ${asString(snapshot.state?.mission_anchor) ?? "(unknown)"}`,
|
|
2345
|
+
`Task type: ${currentTaskType(snapshot) ?? "(missing)"}`,
|
|
2346
|
+
`Evaluation profile: ${currentEvaluationProfile(snapshot) ?? "(missing)"}`,
|
|
1597
2347
|
`Current phase: ${asString(snapshot.state?.current_phase) ?? "unknown"}`,
|
|
1598
2348
|
`Continuation policy: ${asString(snapshot.state?.continuation_policy) ?? "unknown"}`,
|
|
1599
2349
|
`Continuation reason: ${asString(snapshot.state?.continuation_reason) ?? "(unknown)"}`,
|
|
@@ -1607,7 +2357,13 @@ function buildSystemReminder(snapshot: CompletionStateSnapshot, sliceHistory: Js
|
|
|
1607
2357
|
"Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.",
|
|
1608
2358
|
"If canonical state is stale, invalid, ambiguous, or missing, route to completion-regrounder.",
|
|
1609
2359
|
"When recovering from compaction, prefer a deterministic restart from canonical files over conversational inference.",
|
|
1610
|
-
]
|
|
2360
|
+
];
|
|
2361
|
+
if (activePriority !== undefined) lines.push(`Active slice priority: ${activePriority}`);
|
|
2362
|
+
if (activeWhyNow) lines.push(`Active slice why_now: ${activeWhyNow}`);
|
|
2363
|
+
if (implementationSurfaces.length > 0) lines.push(`Active implementation surfaces: ${implementationSurfaces.join(", ")}`);
|
|
2364
|
+
if (verificationCommands.length > 0) lines.push(`Active verification commands: ${verificationCommands.join(" | ")}`);
|
|
2365
|
+
if (isRubricEvaluationRole(nextRole)) lines.push(buildEvaluationRoleReminderText(snapshot, nextRole));
|
|
2366
|
+
return lines.join(" ");
|
|
1611
2367
|
}
|
|
1612
2368
|
|
|
1613
2369
|
function buildPostCompactionDriverInstructions(snapshot: CompletionStateSnapshot, marker: JsonRecord | undefined): string {
|
|
@@ -1616,11 +2372,19 @@ function buildPostCompactionDriverInstructions(snapshot: CompletionStateSnapshot
|
|
|
1616
2372
|
const nextAction = asString(snapshot.state?.next_mandatory_action) ?? "unknown";
|
|
1617
2373
|
const continuation = asString(snapshot.state?.continuation_policy) ?? "unknown";
|
|
1618
2374
|
const activeSliceId = asString(snapshot.active?.slice_id) ?? asString(snapshot.activeSlice?.slice_id) ?? "(none)";
|
|
1619
|
-
|
|
2375
|
+
const taskType = currentTaskType(snapshot) ?? "(missing)";
|
|
2376
|
+
const evaluationProfile = currentEvaluationProfile(snapshot) ?? "(missing)";
|
|
2377
|
+
const implementationSurfaces = asStringArray(snapshot.active?.implementation_surfaces);
|
|
2378
|
+
const verificationCommands = asStringArray(snapshot.active?.verification_commands);
|
|
2379
|
+
const activePriority = asNumber(snapshot.active?.priority);
|
|
2380
|
+
const activeWhyNow = asString(snapshot.active?.why_now);
|
|
2381
|
+
const lines = [
|
|
1620
2382
|
"POST-COMPACTION RECOVERY MODE is active.",
|
|
1621
2383
|
`Compaction marker time: ${markerAt}`,
|
|
1622
2384
|
"Treat the previous conversation as lossy continuity support only.",
|
|
1623
2385
|
"Before taking any substantive action, re-read .agent/state.json, .agent/plan.json, .agent/active-slice.json, .agent/slice-history.jsonl, and .agent/stop-check-history.jsonl from disk.",
|
|
2386
|
+
`Canonical task_type is currently: ${taskType}`,
|
|
2387
|
+
`Canonical evaluation_profile is currently: ${evaluationProfile}`,
|
|
1624
2388
|
`Canonical next mandatory role is currently: ${nextRole}`,
|
|
1625
2389
|
`Canonical next mandatory action is currently: ${nextAction}`,
|
|
1626
2390
|
`Canonical continuation policy is currently: ${continuation}`,
|
|
@@ -1629,7 +2393,13 @@ function buildPostCompactionDriverInstructions(snapshot: CompletionStateSnapshot
|
|
|
1629
2393
|
"If the canonical state is ambiguous, inconsistent, missing, or stale after re-reading it, your first mandatory action is to dispatch completion-regrounder rather than guessing.",
|
|
1630
2394
|
"If continuation_policy == continue and canonical state is coherent, continue dispatching the mandatory role directly without asking the user whether to continue.",
|
|
1631
2395
|
"If you are about to implement after compaction, confirm the active slice snapshot still matches .agent/plan.json before doing any work.",
|
|
1632
|
-
]
|
|
2396
|
+
];
|
|
2397
|
+
if (activePriority !== undefined) lines.push(`Canonical active-slice priority is currently: ${activePriority}`);
|
|
2398
|
+
if (activeWhyNow) lines.push(`Canonical active-slice why_now is currently: ${activeWhyNow}`);
|
|
2399
|
+
if (implementationSurfaces.length > 0) lines.push(`Canonical implementation surfaces are currently: ${implementationSurfaces.join(", ")}`);
|
|
2400
|
+
if (verificationCommands.length > 0) lines.push(`Canonical verification commands are currently: ${verificationCommands.join(" | ")}`);
|
|
2401
|
+
if (isRubricEvaluationRole(nextRole)) lines.push(buildEvaluationRoleReminderText(snapshot, nextRole));
|
|
2402
|
+
return lines.join(" ");
|
|
1633
2403
|
}
|
|
1634
2404
|
|
|
1635
2405
|
function isStaleContextError(error: unknown): boolean {
|
|
@@ -1705,12 +2475,16 @@ function buildResumeCapsule(snapshot: CompletionStateSnapshot, sliceHistory: Jso
|
|
|
1705
2475
|
: asStringArray(snapshot.activeSlice?.blocked_on);
|
|
1706
2476
|
const lockedNotes = asStringArray(snapshot.active?.locked_notes);
|
|
1707
2477
|
const mustFixFindings = asStringArray(snapshot.active?.must_fix_findings);
|
|
2478
|
+
const implementationSurfaces = asStringArray(snapshot.active?.implementation_surfaces);
|
|
2479
|
+
const verificationCommands = asStringArray(snapshot.active?.verification_commands);
|
|
1708
2480
|
const remainingBefore = asStringArray(snapshot.active?.remaining_contract_ids_before);
|
|
1709
2481
|
const lines = [
|
|
1710
2482
|
"Authoritative completion resume capsule:",
|
|
1711
2483
|
"",
|
|
1712
2484
|
"<completion-state>",
|
|
1713
2485
|
`mission_anchor: ${asString(snapshot.state?.mission_anchor) ?? "(unknown)"}`,
|
|
2486
|
+
`task_type: ${currentTaskType(snapshot) ?? "(missing)"}`,
|
|
2487
|
+
`evaluation_profile: ${currentEvaluationProfile(snapshot) ?? "(missing)"}`,
|
|
1714
2488
|
`current_phase: ${asString(snapshot.state?.current_phase) ?? "unknown"}`,
|
|
1715
2489
|
`continuation_policy: ${asString(snapshot.state?.continuation_policy) ?? "unknown"}`,
|
|
1716
2490
|
`continuation_reason: ${asString(snapshot.state?.continuation_reason) ?? "(unknown)"}`,
|
|
@@ -1727,11 +2501,15 @@ function buildResumeCapsule(snapshot: CompletionStateSnapshot, sliceHistory: Jso
|
|
|
1727
2501
|
`- slice_id: ${asString(snapshot.active?.slice_id) ?? asString(snapshot.activeSlice?.slice_id) ?? "(none)"}`,
|
|
1728
2502
|
`- status: ${asString(snapshot.active?.status) ?? asString(snapshot.activeSlice?.status) ?? "unknown"}`,
|
|
1729
2503
|
`- goal: ${asString(snapshot.active?.goal) ?? asString(snapshot.activeSlice?.goal) ?? "(unknown)"}`,
|
|
2504
|
+
`- priority: ${asNumber(snapshot.active?.priority) ?? "(unknown)"}`,
|
|
2505
|
+
`- why_now: ${asString(snapshot.active?.why_now) ?? "(unknown)"}`,
|
|
1730
2506
|
`- contract_ids: ${contractIds.length > 0 ? contractIds.join(", ") : "(none)"}`,
|
|
1731
2507
|
];
|
|
1732
2508
|
if (blockedOn.length > 0) lines.push(`- blocked_on: ${blockedOn.join(", ")}`);
|
|
1733
2509
|
if (lockedNotes.length > 0) lines.push(`- locked_notes: ${lockedNotes.join(" | ")}`);
|
|
1734
2510
|
if (mustFixFindings.length > 0) lines.push(`- must_fix_findings: ${mustFixFindings.join(" | ")}`);
|
|
2511
|
+
if (implementationSurfaces.length > 0) lines.push(`- implementation_surfaces: ${implementationSurfaces.join(" | ")}`);
|
|
2512
|
+
if (verificationCommands.length > 0) lines.push(`- verification_commands: ${verificationCommands.join(" | ")}`);
|
|
1735
2513
|
lines.push(`- basis_commit: ${asString(snapshot.active?.basis_commit) ?? "(none)"}`);
|
|
1736
2514
|
lines.push(`- remaining_contract_ids_before: ${remainingBefore.length > 0 ? remainingBefore.join(", ") : "(none)"}`);
|
|
1737
2515
|
lines.push(`- release_blocker_count_before: ${asNumber(snapshot.active?.release_blocker_count_before) ?? "(unknown)"}`);
|
|
@@ -1743,7 +2521,7 @@ function buildResumeCapsule(snapshot: CompletionStateSnapshot, sliceHistory: Jso
|
|
|
1743
2521
|
"",
|
|
1744
2522
|
"Rules:",
|
|
1745
2523
|
"- Treat this block as continuity support derived from canonical .agent state.",
|
|
1746
|
-
"- Preserve exact slice_id, contract_ids, acceptance criteria, locked notes, and must-fix findings where still true.",
|
|
2524
|
+
"- Preserve exact slice_id, contract_ids, acceptance criteria, priority, why_now, implementation surfaces, verification commands, locked notes, and must-fix findings where still true.",
|
|
1747
2525
|
"- After compaction, re-read .agent/state.json, .agent/plan.json, .agent/active-slice.json, .agent/slice-history.jsonl, and .agent/stop-check-history.jsonl before resuming long-running completion work.",
|
|
1748
2526
|
"- Invoke completion-regrounder before continuing when requires_reground is true or unknown.",
|
|
1749
2527
|
"- Invoke completion-regrounder before continuing when next_mandatory_role or next_mandatory_action is unknown or ambiguous.",
|
|
@@ -2076,33 +2854,15 @@ async function refreshStatus(ctx: { cwd: string; hasUI: boolean; ui: any }) {
|
|
|
2076
2854
|
}
|
|
2077
2855
|
|
|
2078
2856
|
function parseReportFields(text: string): Record<string, string> {
|
|
2079
|
-
|
|
2080
|
-
for (const rawLine of text.split("\n")) {
|
|
2081
|
-
const line = rawLine.trim();
|
|
2082
|
-
if (!line) continue;
|
|
2083
|
-
const normalized = line.replace(/^-\s*/, "").replace(/^`/, "").replace(/`$/, "");
|
|
2084
|
-
const match = normalized.match(/^([A-Za-z][A-Za-z0-9 _\/-]*?):\s*(.*)$/);
|
|
2085
|
-
if (!match) continue;
|
|
2086
|
-
const [, key, value] = match;
|
|
2087
|
-
fields[key.trim()] = value.trim();
|
|
2088
|
-
}
|
|
2089
|
-
return fields;
|
|
2857
|
+
return roleReporting.parseReportFields(text);
|
|
2090
2858
|
}
|
|
2091
2859
|
|
|
2092
2860
|
function parseYesNo(value: string | undefined): boolean | undefined {
|
|
2093
|
-
|
|
2094
|
-
const normalized = value.trim().toLowerCase();
|
|
2095
|
-
if (normalized.startsWith("yes")) return true;
|
|
2096
|
-
if (normalized.startsWith("no")) return false;
|
|
2097
|
-
return undefined;
|
|
2861
|
+
return roleReporting.parseYesNo(value);
|
|
2098
2862
|
}
|
|
2099
2863
|
|
|
2100
2864
|
function parseFirstNumber(value: string | undefined): number | undefined {
|
|
2101
|
-
|
|
2102
|
-
const match = value.match(/-?\d+/);
|
|
2103
|
-
if (!match) return undefined;
|
|
2104
|
-
const parsed = Number.parseInt(match[0], 10);
|
|
2105
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
2865
|
+
return roleReporting.parseFirstNumber(value);
|
|
2106
2866
|
}
|
|
2107
2867
|
|
|
2108
2868
|
async function gitHeadSha(cwd: string): Promise<string | undefined> {
|
|
@@ -2252,16 +3012,13 @@ function parseStructuredProgress(text: string): {
|
|
|
2252
3012
|
}
|
|
2253
3013
|
|
|
2254
3014
|
async function transcribeRoleOutput(role: CompletionRole, cwd: string, output: string, reportFields: Record<string, string>): Promise<TranscriptionResult> {
|
|
2255
|
-
const result: TranscriptionResult = { appended: [], skipped: [], errors: [] };
|
|
2256
3015
|
const snapshot = await loadCompletionSnapshot(cwd);
|
|
2257
3016
|
if (!snapshot) {
|
|
2258
|
-
|
|
2259
|
-
return result;
|
|
3017
|
+
return { appended: [], skipped: ["No canonical completion snapshot found."], errors: [] };
|
|
2260
3018
|
}
|
|
2261
3019
|
const headSha = await gitHeadSha(snapshot.files.root);
|
|
2262
3020
|
if (!headSha) {
|
|
2263
|
-
|
|
2264
|
-
return result;
|
|
3021
|
+
return { appended: [], skipped: [], errors: ["Could not resolve git HEAD for transcription."] };
|
|
2265
3022
|
}
|
|
2266
3023
|
|
|
2267
3024
|
const sliceId =
|
|
@@ -2269,117 +3026,14 @@ async function transcribeRoleOutput(role: CompletionRole, cwd: string, output: s
|
|
|
2269
3026
|
asString(snapshot.activeSlice?.slice_id) ??
|
|
2270
3027
|
asString(snapshot.state?.latest_completed_slice);
|
|
2271
3028
|
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
return (
|
|
2281
|
-
asString(entry.type) === type &&
|
|
2282
|
-
asString(entry.slice_id) === sliceId &&
|
|
2283
|
-
asString(entry.head_sha) === headSha &&
|
|
2284
|
-
asString(entry.report_text) === output.trim()
|
|
2285
|
-
);
|
|
2286
|
-
});
|
|
2287
|
-
if (duplicate) {
|
|
2288
|
-
result.skipped.push(`Skipped duplicate ${type} record for slice ${sliceId} at ${headSha.slice(0, 12)}.`);
|
|
2289
|
-
return result;
|
|
2290
|
-
}
|
|
2291
|
-
await appendJsonlRecord(snapshot.files.sliceHistoryPath, {
|
|
2292
|
-
schema_version: 1,
|
|
2293
|
-
type,
|
|
2294
|
-
recorded_at: Date.now(),
|
|
2295
|
-
slice_id: sliceId,
|
|
2296
|
-
commit_sha: headSha,
|
|
2297
|
-
head_sha: headSha,
|
|
2298
|
-
role,
|
|
2299
|
-
report_fields: reportFields,
|
|
2300
|
-
report_text: output.trim(),
|
|
2301
|
-
});
|
|
2302
|
-
result.appended.push(`${type}:${sliceId}`);
|
|
2303
|
-
return result;
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
if (role === "completion-stop-judge") {
|
|
2307
|
-
const canStop = parseYesNo(reportFields["Can the project stop now"]);
|
|
2308
|
-
const blockerCount = parseFirstNumber(reportFields["Blocker count"]);
|
|
2309
|
-
const highValueGapCount = parseFirstNumber(reportFields["High-value gap count"]);
|
|
2310
|
-
if (canStop === undefined || blockerCount === undefined || highValueGapCount === undefined) {
|
|
2311
|
-
result.errors.push("Missing required stop-judge fields for canonical judgment transcription.");
|
|
2312
|
-
return result;
|
|
2313
|
-
}
|
|
2314
|
-
const history = await readJsonl(snapshot.files.stopHistoryPath);
|
|
2315
|
-
const duplicate = history.some((entry) => {
|
|
2316
|
-
return asString(entry.type) === "judgment" && asString(entry.head_sha) === headSha && asString(entry.report_text) === output.trim();
|
|
2317
|
-
});
|
|
2318
|
-
if (duplicate) {
|
|
2319
|
-
result.skipped.push(`Skipped duplicate judgment record at ${headSha.slice(0, 12)}.`);
|
|
2320
|
-
return result;
|
|
2321
|
-
}
|
|
2322
|
-
await appendJsonlRecord(snapshot.files.stopHistoryPath, {
|
|
2323
|
-
schema_version: 1,
|
|
2324
|
-
type: "judgment",
|
|
2325
|
-
recorded_at: Date.now(),
|
|
2326
|
-
head_sha: headSha,
|
|
2327
|
-
can_stop: canStop,
|
|
2328
|
-
blocker_count: blockerCount,
|
|
2329
|
-
high_value_gap_count: highValueGapCount,
|
|
2330
|
-
role,
|
|
2331
|
-
report_fields: reportFields,
|
|
2332
|
-
report_text: output.trim(),
|
|
2333
|
-
});
|
|
2334
|
-
result.appended.push(`judgment:${headSha.slice(0, 12)}`);
|
|
2335
|
-
return result;
|
|
2336
|
-
}
|
|
2337
|
-
|
|
2338
|
-
if (role === "completion-regrounder") {
|
|
2339
|
-
const rawDecision = asString(reportFields["Reconciliation decision"])?.toLowerCase();
|
|
2340
|
-
const decision = rawDecision?.match(/\b(accepted|reopened|none)\b/)?.[1];
|
|
2341
|
-
if (!decision || decision === "none") {
|
|
2342
|
-
result.skipped.push("No reconciliation decision emitted by completion-regrounder.");
|
|
2343
|
-
return result;
|
|
2344
|
-
}
|
|
2345
|
-
const reconciledSliceId =
|
|
2346
|
-
asString(reportFields["Reconciled slice ID"]) ??
|
|
2347
|
-
asString(reportFields["Current selected slice"]) ??
|
|
2348
|
-
sliceId;
|
|
2349
|
-
if (!reconciledSliceId || reconciledSliceId === "none" || reconciledSliceId === "(none)") {
|
|
2350
|
-
result.errors.push("Missing reconciled slice id for completion-regrounder transcription.");
|
|
2351
|
-
return result;
|
|
2352
|
-
}
|
|
2353
|
-
const history = await readJsonl(snapshot.files.sliceHistoryPath);
|
|
2354
|
-
const duplicate = history.some((entry) => {
|
|
2355
|
-
return (
|
|
2356
|
-
asString(entry.type) === decision &&
|
|
2357
|
-
asString(entry.slice_id) === reconciledSliceId &&
|
|
2358
|
-
asString(entry.head_sha) === headSha &&
|
|
2359
|
-
asString(entry.report_text) === output.trim()
|
|
2360
|
-
);
|
|
2361
|
-
});
|
|
2362
|
-
if (duplicate) {
|
|
2363
|
-
result.skipped.push(`Skipped duplicate ${decision} record for slice ${reconciledSliceId} at ${headSha.slice(0, 12)}.`);
|
|
2364
|
-
return result;
|
|
2365
|
-
}
|
|
2366
|
-
await appendJsonlRecord(snapshot.files.sliceHistoryPath, {
|
|
2367
|
-
schema_version: 1,
|
|
2368
|
-
type: decision,
|
|
2369
|
-
recorded_at: Date.now(),
|
|
2370
|
-
slice_id: reconciledSliceId,
|
|
2371
|
-
commit_sha: headSha,
|
|
2372
|
-
head_sha: headSha,
|
|
2373
|
-
role,
|
|
2374
|
-
report_fields: reportFields,
|
|
2375
|
-
report_text: output.trim(),
|
|
2376
|
-
});
|
|
2377
|
-
result.appended.push(`${decision}:${reconciledSliceId}`);
|
|
2378
|
-
return result;
|
|
2379
|
-
}
|
|
2380
|
-
|
|
2381
|
-
result.skipped.push(`No automatic transcription configured for ${role}.`);
|
|
2382
|
-
return result;
|
|
3029
|
+
return await roleReporting.transcribeCanonicalRoleReport({
|
|
3030
|
+
role,
|
|
3031
|
+
output,
|
|
3032
|
+
reportFields,
|
|
3033
|
+
snapshotFiles: snapshot.files,
|
|
3034
|
+
headSha,
|
|
3035
|
+
sliceId,
|
|
3036
|
+
});
|
|
2383
3037
|
}
|
|
2384
3038
|
|
|
2385
3039
|
function isPathInside(root: string, candidatePath: string): boolean {
|
|
@@ -2483,23 +3137,32 @@ function lastAssistantText(messages: Array<{ role: string; content: Array<{ type
|
|
|
2483
3137
|
return "";
|
|
2484
3138
|
}
|
|
2485
3139
|
|
|
2486
|
-
function completionKickoff(
|
|
3140
|
+
function completionKickoff(
|
|
3141
|
+
goal: string,
|
|
3142
|
+
taskType: string,
|
|
3143
|
+
evaluationProfile: string,
|
|
3144
|
+
intent: "auto" | "continue" | "refocus" = "auto",
|
|
3145
|
+
missionAnchor?: string,
|
|
3146
|
+
): string {
|
|
2487
3147
|
const intentBlock =
|
|
2488
3148
|
intent === "continue" && missionAnchor
|
|
2489
3149
|
? `Existing canonical mission anchor:\n${missionAnchor}\n\nWorkflow intent:\n- Continue the existing workflow.\n- Treat the new user text as supplemental direction unless canonical reconciliation proves the mission itself must change.\n\n`
|
|
2490
3150
|
: intent === "refocus" && missionAnchor
|
|
2491
3151
|
? `Updated canonical mission anchor:\n${missionAnchor}\n\nWorkflow intent:\n- The user explicitly refocused the workflow before this kickoff.\n- Re-read canonical .agent/** state and continue from the refocused mission.\n\n`
|
|
2492
3152
|
: "";
|
|
2493
|
-
return `/skill:completion-protocol Start or continue the completion workflow for this repo.\n\nBefore acting, read:\n- ${SKILL_PATH}\n- ${REFERENCE_PATH}\n\nUser goal:\n${goal}\n\n${intentBlock}Driver instructions:\n- Canonical truth is in .agent/**. Re-read .agent/state.json, .agent/plan.json, and .agent/active-slice.json before acting when they exist.\n- If tracked completion contract files are missing or onboarding is required, invoke completion_role with role completion-bootstrapper.\n- Otherwise follow the mandatory dispatch rules from completion-protocol.\n- Use completion_role for all completion-* role work. Do not directly implement tracked product changes yourself.\n- Continue dispatching mandatory roles while continuation_policy == continue.\n- Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.`;
|
|
3153
|
+
return `/skill:completion-protocol Start or continue the completion workflow for this repo.\n\nBefore acting, read:\n- ${SKILL_PATH}\n- ${REFERENCE_PATH}\n\nCanonical routing profile:\n- task_type: ${taskType}\n- evaluation_profile: ${evaluationProfile}\n\nUser goal:\n${goal}\n\n${intentBlock}Driver instructions:\n- Canonical truth is in .agent/**. Re-read .agent/state.json, .agent/plan.json, and .agent/active-slice.json before acting when they exist.\n- If tracked completion contract files are missing or onboarding is required, invoke completion_role with role completion-bootstrapper.\n- Otherwise follow the mandatory dispatch rules from completion-protocol.\n- Use completion_role for all completion-* role work. Do not directly implement tracked product changes yourself.\n- Continue dispatching mandatory roles while continuation_policy == continue.\n- Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.`;
|
|
2494
3154
|
}
|
|
2495
3155
|
|
|
2496
|
-
function completionResumePrompt(): string {
|
|
2497
|
-
return `/skill:completion-protocol Resume the completion workflow from canonical state.\n\nBefore acting, read:\n- ${SKILL_PATH}\n- ${REFERENCE_PATH}\n\nResume instructions:\n- Re-read .agent/state.json, .agent/plan.json, and .agent/active-slice.json before acting.\n- If canonical state is missing, invalid, contradictory, stale, or ambiguous, route to completion-regrounder first.\n- Continue from next_mandatory_role and next_mandatory_action.\n- Use completion_role for all completion-* role work.\n- Continue dispatching mandatory roles while continuation_policy == continue.\n- Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.`;
|
|
3156
|
+
function completionResumePrompt(taskType: string, evaluationProfile: string): string {
|
|
3157
|
+
return `/skill:completion-protocol Resume the completion workflow from canonical state.\n\nBefore acting, read:\n- ${SKILL_PATH}\n- ${REFERENCE_PATH}\n\nCanonical routing profile:\n- task_type: ${taskType}\n- evaluation_profile: ${evaluationProfile}\n\nResume instructions:\n- Re-read .agent/state.json, .agent/plan.json, and .agent/active-slice.json before acting.\n- If canonical state is missing, invalid, contradictory, stale, or ambiguous, route to completion-regrounder first.\n- Continue from next_mandatory_role and next_mandatory_action.\n- Use completion_role for all completion-* role work.\n- Continue dispatching mandatory roles while continuation_policy == continue.\n- Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.`;
|
|
2498
3158
|
}
|
|
2499
3159
|
|
|
2500
3160
|
export default function completionExtension(pi: ExtensionAPI) {
|
|
2501
3161
|
pi.on("session_start", async (_event, ctx) => {
|
|
2502
3162
|
await refreshStatus(ctx);
|
|
3163
|
+
if (shouldTestAutoContinueOnSessionStart()) {
|
|
3164
|
+
await autoContinueWorkflowIfNeeded(pi, ctx);
|
|
3165
|
+
}
|
|
2503
3166
|
});
|
|
2504
3167
|
|
|
2505
3168
|
pi.on("turn_end", async (_event, ctx) => {
|
|
@@ -2512,10 +3175,16 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
2512
3175
|
await fsp.rm(snapshot.files.compactionMarkerPath, { force: true });
|
|
2513
3176
|
}
|
|
2514
3177
|
await refreshStatus(ctx);
|
|
3178
|
+
await autoContinueWorkflowIfNeeded(pi, ctx);
|
|
2515
3179
|
});
|
|
2516
3180
|
|
|
2517
3181
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
2518
3182
|
const loaded = await loadCompletionDataForReminder(getCtxCwd(ctx));
|
|
3183
|
+
if (loaded) {
|
|
3184
|
+
const rootKey = completionRootKey(loaded.snapshot, getCtxCwd(ctx));
|
|
3185
|
+
const fingerprint = completionContinuationFingerprint(loaded.snapshot);
|
|
3186
|
+
if (fingerprint) markQueuedDriverPromptInFlight(rootKey, fingerprint);
|
|
3187
|
+
}
|
|
2519
3188
|
if (!loaded) return;
|
|
2520
3189
|
const markerText = await readText(loaded.snapshot.files.compactionMarkerPath);
|
|
2521
3190
|
let marker: JsonRecord | undefined;
|
|
@@ -2529,6 +3198,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
2529
3198
|
}
|
|
2530
3199
|
const additions = [buildSystemReminder(loaded.snapshot, loaded.sliceHistory, loaded.stopHistory)];
|
|
2531
3200
|
if (marker) additions.push(buildPostCompactionDriverInstructions(loaded.snapshot, marker));
|
|
3201
|
+
maybeWriteTestSnapshot(completionTestSystemReminderPath(), additions.join("\n\n"));
|
|
2532
3202
|
const systemPrompt = getSystemPromptSafe(ctx);
|
|
2533
3203
|
if (!systemPrompt) return;
|
|
2534
3204
|
return {
|
|
@@ -2631,6 +3301,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
2631
3301
|
const runCwd = findCompletionRoot(cwd) ?? findRepoRoot(cwd) ?? cwd;
|
|
2632
3302
|
const rootKey = runCwd;
|
|
2633
3303
|
const agent = await loadAgentDefinition(runCwd, role);
|
|
3304
|
+
const loaded = await loadCompletionDataForReminder(runCwd);
|
|
2634
3305
|
type RunningDetails = {
|
|
2635
3306
|
role: string;
|
|
2636
3307
|
status: "running" | "ok" | "error";
|
|
@@ -2660,6 +3331,9 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
2660
3331
|
`- ${REFERENCE_PATH}`,
|
|
2661
3332
|
"Use canonical .agent/** state as the source of truth.",
|
|
2662
3333
|
];
|
|
3334
|
+
if (loaded && isRubricEvaluationRole(role)) {
|
|
3335
|
+
taskLines.push("", ...buildEvaluationRoleContextLines(loaded.snapshot, role));
|
|
3336
|
+
}
|
|
2663
3337
|
if (params.task?.trim()) {
|
|
2664
3338
|
taskLines.push("", "Supplemental task context:", params.task.trim());
|
|
2665
3339
|
}
|
|
@@ -2916,6 +3590,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
2916
3590
|
const workflowDone = isWorkflowDone(snapshot);
|
|
2917
3591
|
let kickoffIntent: "auto" | "continue" | "refocus" = "auto";
|
|
2918
3592
|
let kickoffMissionAnchor = snapshot ? currentMissionAnchor(snapshot) : undefined;
|
|
3593
|
+
let kickoffAnalysis: ContextProposalAnalysis | undefined;
|
|
2919
3594
|
|
|
2920
3595
|
if (!snapshot) {
|
|
2921
3596
|
const root = findRepoRoot(cwd) ?? cwd;
|
|
@@ -2941,6 +3616,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
2941
3616
|
}
|
|
2942
3617
|
goal = decision.goalText;
|
|
2943
3618
|
kickoffMissionAnchor = decision.missionAnchor;
|
|
3619
|
+
kickoffAnalysis = decision.analysis;
|
|
2944
3620
|
} else {
|
|
2945
3621
|
const proposal = await buildGoalAnchoredContextProposal(ctx, goal, projectName);
|
|
2946
3622
|
const decision = await confirmContextProposal(ctx, proposal, projectName, {
|
|
@@ -2955,8 +3631,17 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
2955
3631
|
}
|
|
2956
3632
|
goal = decision.goalText;
|
|
2957
3633
|
kickoffMissionAnchor = decision.missionAnchor;
|
|
3634
|
+
kickoffAnalysis = decision.analysis;
|
|
2958
3635
|
}
|
|
2959
|
-
const
|
|
3636
|
+
const startupRouting = finalizeContextProposalAnalysis(kickoffAnalysis, [goal ?? kickoffMissionAnchor ?? projectName]);
|
|
3637
|
+
const created = await scaffoldCompletionFiles(root, kickoffMissionAnchor ?? projectName, {
|
|
3638
|
+
analysis: startupRouting,
|
|
3639
|
+
continuationReason: buildContextProposalContinuationReason(
|
|
3640
|
+
"User started workflow via /cook:",
|
|
3641
|
+
goal ?? kickoffMissionAnchor ?? projectName,
|
|
3642
|
+
startupRouting,
|
|
3643
|
+
),
|
|
3644
|
+
});
|
|
2960
3645
|
emitCommandText(
|
|
2961
3646
|
ctx,
|
|
2962
3647
|
`Initialized completion control plane in ${created.root}${created.created.length > 0 ? ` (${created.created.length} files created)` : ""}`,
|
|
@@ -2992,18 +3677,24 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
2992
3677
|
goal = decision.goalText;
|
|
2993
3678
|
kickoffIntent = "refocus";
|
|
2994
3679
|
kickoffMissionAnchor = decision.missionAnchor;
|
|
2995
|
-
await refocusCompletionMission(snapshot, decision.missionAnchor, decision.goalText);
|
|
3680
|
+
await refocusCompletionMission(snapshot, decision.missionAnchor, decision.goalText, decision.analysis);
|
|
2996
3681
|
snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
|
|
2997
3682
|
emitCommandText(ctx, `Started a new completion workflow round from recent discussion: ${decision.missionAnchor}`, "info");
|
|
2998
3683
|
} else {
|
|
2999
3684
|
const mission = currentMissionAnchor(snapshot);
|
|
3000
3685
|
pi.setSessionName(`completion: ${mission.slice(0, 60)}`);
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3686
|
+
const resumePrompt = completionResumePrompt(
|
|
3687
|
+
currentTaskType(snapshot) ?? "(missing)",
|
|
3688
|
+
currentEvaluationProfile(snapshot) ?? "(missing)",
|
|
3689
|
+
);
|
|
3690
|
+
const rootKey = completionRootKey(snapshot, getCtxCwd(ctx));
|
|
3691
|
+
const fingerprint = completionContinuationFingerprint(snapshot) ?? JSON.stringify({
|
|
3692
|
+
kind: "resume",
|
|
3693
|
+
mission_anchor: currentMissionAnchor(snapshot),
|
|
3694
|
+
current_phase: asString(snapshot.state?.current_phase) ?? null,
|
|
3695
|
+
next_mandatory_role: asString(snapshot.state?.next_mandatory_role) ?? null,
|
|
3696
|
+
});
|
|
3697
|
+
await queueCompletionDriverPrompt(pi, ctx, rootKey, fingerprint, resumePrompt, "resume");
|
|
3007
3698
|
return;
|
|
3008
3699
|
}
|
|
3009
3700
|
}
|
|
@@ -3025,7 +3716,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
3025
3716
|
goal = decision.goalText;
|
|
3026
3717
|
kickoffIntent = "refocus";
|
|
3027
3718
|
kickoffMissionAnchor = decision.missionAnchor;
|
|
3028
|
-
await refocusCompletionMission(snapshot, decision.missionAnchor, decision.goalText);
|
|
3719
|
+
await refocusCompletionMission(snapshot, decision.missionAnchor, decision.goalText, decision.analysis);
|
|
3029
3720
|
snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
|
|
3030
3721
|
emitCommandText(ctx, `Started a new completion workflow round from explicit goal: ${decision.missionAnchor}`, "info");
|
|
3031
3722
|
} else {
|
|
@@ -3050,7 +3741,12 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
3050
3741
|
return;
|
|
3051
3742
|
}
|
|
3052
3743
|
goal = proposalDecision.goalText;
|
|
3053
|
-
await refocusCompletionMission(
|
|
3744
|
+
await refocusCompletionMission(
|
|
3745
|
+
snapshot,
|
|
3746
|
+
proposalDecision.missionAnchor,
|
|
3747
|
+
proposalDecision.goalText,
|
|
3748
|
+
proposalDecision.analysis,
|
|
3749
|
+
);
|
|
3054
3750
|
snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
|
|
3055
3751
|
kickoffMissionAnchor = proposalDecision.missionAnchor;
|
|
3056
3752
|
emitCommandText(ctx, `Refocused completion mission to: ${proposalDecision.missionAnchor}`, "info");
|
|
@@ -3060,12 +3756,23 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
3060
3756
|
}
|
|
3061
3757
|
}
|
|
3062
3758
|
pi.setSessionName(`completion: ${kickoffMissionAnchor.slice(0, 60)}`);
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3759
|
+
const kickoffPrompt = completionKickoff(
|
|
3760
|
+
goal,
|
|
3761
|
+
currentTaskType(snapshot) ?? "(missing)",
|
|
3762
|
+
currentEvaluationProfile(snapshot) ?? "(missing)",
|
|
3763
|
+
kickoffIntent,
|
|
3764
|
+
kickoffMissionAnchor,
|
|
3765
|
+
);
|
|
3766
|
+
const rootKey = completionRootKey(snapshot, getCtxCwd(ctx));
|
|
3767
|
+
const fingerprint = completionContinuationFingerprint(snapshot) ?? JSON.stringify({
|
|
3768
|
+
kind: "kickoff",
|
|
3769
|
+
mission_anchor: kickoffMissionAnchor,
|
|
3770
|
+
goal,
|
|
3771
|
+
intent: kickoffIntent,
|
|
3772
|
+
task_type: currentTaskType(snapshot) ?? "(missing)",
|
|
3773
|
+
evaluation_profile: currentEvaluationProfile(snapshot) ?? "(missing)",
|
|
3774
|
+
});
|
|
3775
|
+
await queueCompletionDriverPrompt(pi, ctx, rootKey, fingerprint, kickoffPrompt, "kickoff");
|
|
3069
3776
|
},
|
|
3070
3777
|
});
|
|
3071
3778
|
}
|