@linimin/pi-letscook 0.1.30 → 0.1.31
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 +16 -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 +749 -195
- 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 +154 -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;
|
|
@@ -499,6 +521,28 @@ function completionTestContextProposalUiSnapshotPath(): string | undefined {
|
|
|
499
521
|
return asString(process.env.PI_COMPLETION_TEST_CONTEXT_PROPOSAL_UI_PATH);
|
|
500
522
|
}
|
|
501
523
|
|
|
524
|
+
function completionTestContextProposalSnapshotPath(): string | undefined {
|
|
525
|
+
return asString(process.env.PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function completionTestDriverPromptPath(): string | undefined {
|
|
529
|
+
return asString(process.env.PI_COMPLETION_TEST_DRIVER_PROMPT_PATH);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function completionTestSystemReminderPath(): string | undefined {
|
|
533
|
+
return asString(process.env.PI_COMPLETION_TEST_SYSTEM_REMINDER_PATH);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function maybeWriteTestSnapshot(targetPath: string | undefined, content: string): void {
|
|
537
|
+
if (!targetPath) return;
|
|
538
|
+
try {
|
|
539
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
540
|
+
fs.writeFileSync(targetPath, content, "utf8");
|
|
541
|
+
} catch {
|
|
542
|
+
// ignore malformed or unwritable test snapshot paths
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
502
546
|
function shouldDisableContextProposalAnalyst(): boolean {
|
|
503
547
|
return process.env.PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST === "1";
|
|
504
548
|
}
|
|
@@ -541,7 +585,7 @@ function normalizeProposalLine(line: string): string {
|
|
|
541
585
|
.trim();
|
|
542
586
|
}
|
|
543
587
|
|
|
544
|
-
function detectProposalSection(line: string):
|
|
588
|
+
function detectProposalSection(line: string): ContextProposalSection | undefined {
|
|
545
589
|
const normalized = normalizeProposalLine(line)
|
|
546
590
|
.toLowerCase()
|
|
547
591
|
.replace(/[::]$/, "")
|
|
@@ -551,12 +595,12 @@ function detectProposalSection(line: string): "mission" | "scope" | "constraints
|
|
|
551
595
|
if (["scope", "plan", "steps", "implementation", "範圍", "范围", "實作", "实现", "步驟", "步骤"].includes(normalized)) return "scope";
|
|
552
596
|
if (["constraints", "constraint", "guardrails", "non-goals", "限制", "約束", "约束", "非目標", "非目标"].includes(normalized)) return "constraints";
|
|
553
597
|
if (["acceptance", "acceptance criteria", "deliverables", "verification", "驗收", "验收", "交付", "驗證", "验证"].includes(normalized)) return "acceptance";
|
|
598
|
+
if (["critique", "critic", "concerns", "concern", "warnings", "warning", "notes", "note", "評論", "评论", "提醒"].includes(normalized)) return "critique";
|
|
599
|
+
if (["risk", "risks", "hazards", "hazard", "failure modes", "failure mode", "風險", "风险"].includes(normalized)) return "risks";
|
|
554
600
|
return undefined;
|
|
555
601
|
}
|
|
556
602
|
|
|
557
|
-
function matchInlineProposalSection(
|
|
558
|
-
line: string,
|
|
559
|
-
): { section: "mission" | "scope" | "constraints" | "acceptance"; content: string } | undefined {
|
|
603
|
+
function matchInlineProposalSection(line: string): { section: ContextProposalSection; content: string } | undefined {
|
|
560
604
|
const normalized = normalizeProposalLine(line);
|
|
561
605
|
const match = normalized.match(/^([^::]+)[::]\s*(.+)$/u);
|
|
562
606
|
if (!match) return undefined;
|
|
@@ -595,6 +639,113 @@ function uniqueProposalItems(items: string[]): string[] {
|
|
|
595
639
|
return result;
|
|
596
640
|
}
|
|
597
641
|
|
|
642
|
+
function normalizeContextProposalHint(value: unknown): string | undefined {
|
|
643
|
+
const normalized = asString(value)?.replace(/\s+/g, " ").trim();
|
|
644
|
+
return normalized || undefined;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function normalizeContextProposalTaskTypeHint(value: unknown): string | undefined {
|
|
648
|
+
const normalized = normalizeContextProposalHint(value);
|
|
649
|
+
if (!normalized) return undefined;
|
|
650
|
+
const canonical = normalized.toLowerCase().replace(/[\s/]+/g, "-");
|
|
651
|
+
return canonical === DEFAULT_TASK_TYPE ? DEFAULT_TASK_TYPE : normalized;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function normalizeContextProposalEvaluationProfileHint(value: unknown): string | undefined {
|
|
655
|
+
const normalized = normalizeContextProposalHint(value);
|
|
656
|
+
if (!normalized) return undefined;
|
|
657
|
+
const canonical = normalized.toLowerCase().replace(/[\s/]+/g, "-");
|
|
658
|
+
return canonical === DEFAULT_EVALUATION_PROFILE ? DEFAULT_EVALUATION_PROFILE : normalized;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function inferContextProposalTaskType(texts: string[]): string | undefined {
|
|
662
|
+
const corpus = texts
|
|
663
|
+
.map((text) => normalizeProposalLine(text).toLowerCase())
|
|
664
|
+
.filter(Boolean)
|
|
665
|
+
.join("\n");
|
|
666
|
+
if (!corpus) return undefined;
|
|
667
|
+
return /(completion|\/cook|\/complete|\.agent|slice|reground|reviewer|auditor|stop judge|stop-judge|workflow)/i.test(corpus)
|
|
668
|
+
? DEFAULT_TASK_TYPE
|
|
669
|
+
: undefined;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function inferContextProposalEvaluationProfile(texts: string[], taskType?: string): string | undefined {
|
|
673
|
+
const corpus = texts
|
|
674
|
+
.map((text) => normalizeProposalLine(text).toLowerCase())
|
|
675
|
+
.filter(Boolean)
|
|
676
|
+
.join("\n");
|
|
677
|
+
if (!corpus) return undefined;
|
|
678
|
+
if (
|
|
679
|
+
/(rubric|evaluation[_\s-]*profile|pass\|concern\|fail|contract coverage|correctness risk|verification evidence|docs\/state parity|reviewer|auditor|stop judge|stop-judge)/i.test(
|
|
680
|
+
corpus,
|
|
681
|
+
)
|
|
682
|
+
) {
|
|
683
|
+
return DEFAULT_EVALUATION_PROFILE;
|
|
684
|
+
}
|
|
685
|
+
return taskType === DEFAULT_TASK_TYPE && /(completion|\/cook|\/complete|slice|workflow|review|audit)/i.test(corpus)
|
|
686
|
+
? DEFAULT_EVALUATION_PROFILE
|
|
687
|
+
: undefined;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function buildContextProposalAnalysis(args: {
|
|
691
|
+
taskType?: unknown;
|
|
692
|
+
evaluationProfile?: unknown;
|
|
693
|
+
critique?: string[];
|
|
694
|
+
risks?: string[];
|
|
695
|
+
possibleNoise?: string[];
|
|
696
|
+
hintTexts?: string[];
|
|
697
|
+
}): ContextProposalAnalysis {
|
|
698
|
+
const critique = uniqueProposalItems(args.critique ?? []);
|
|
699
|
+
const risks = uniqueProposalItems(args.risks ?? []);
|
|
700
|
+
const possibleNoise = uniqueProposalItems(args.possibleNoise ?? []);
|
|
701
|
+
const hintTexts = [...(args.hintTexts ?? []), ...critique, ...risks, ...possibleNoise];
|
|
702
|
+
const taskType = normalizeContextProposalTaskTypeHint(args.taskType) ?? inferContextProposalTaskType(hintTexts);
|
|
703
|
+
const evaluationProfile =
|
|
704
|
+
normalizeContextProposalEvaluationProfileHint(args.evaluationProfile) ??
|
|
705
|
+
inferContextProposalEvaluationProfile(hintTexts, taskType);
|
|
706
|
+
return {
|
|
707
|
+
taskType,
|
|
708
|
+
evaluationProfile,
|
|
709
|
+
critique,
|
|
710
|
+
risks,
|
|
711
|
+
possibleNoise,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function mergeContextProposalAnalysis(
|
|
716
|
+
sources: Array<ContextProposalAnalysis | undefined>,
|
|
717
|
+
hintTexts: string[] = [],
|
|
718
|
+
): ContextProposalAnalysis {
|
|
719
|
+
const critique = uniqueProposalItems(sources.flatMap((source) => source?.critique ?? []));
|
|
720
|
+
const risks = uniqueProposalItems(sources.flatMap((source) => source?.risks ?? []));
|
|
721
|
+
const possibleNoise = uniqueProposalItems(sources.flatMap((source) => source?.possibleNoise ?? []));
|
|
722
|
+
const taskType =
|
|
723
|
+
sources.map((source) => source?.taskType).find((value): value is string => Boolean(value)) ??
|
|
724
|
+
inferContextProposalTaskType([...hintTexts, ...critique, ...risks, ...possibleNoise]);
|
|
725
|
+
const evaluationProfile =
|
|
726
|
+
sources.map((source) => source?.evaluationProfile).find((value): value is string => Boolean(value)) ??
|
|
727
|
+
inferContextProposalEvaluationProfile([...hintTexts, ...critique, ...risks, ...possibleNoise], taskType);
|
|
728
|
+
return {
|
|
729
|
+
taskType,
|
|
730
|
+
evaluationProfile,
|
|
731
|
+
critique,
|
|
732
|
+
risks,
|
|
733
|
+
possibleNoise,
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function matchContextProposalRoutingHint(
|
|
738
|
+
line: string,
|
|
739
|
+
): { field: "taskType" | "evaluationProfile"; value: string } | undefined {
|
|
740
|
+
const normalized = normalizeProposalLine(line);
|
|
741
|
+
const match = normalized.match(/^(task[\s_-]*type|evaluation[\s_-]*profile)[::]\s*(.+)$/iu);
|
|
742
|
+
if (!match) return undefined;
|
|
743
|
+
const label = match[1].toLowerCase().replace(/[\s_-]+/g, "");
|
|
744
|
+
const value = match[2].trim();
|
|
745
|
+
if (!value) return undefined;
|
|
746
|
+
return label === "tasktype" ? { field: "taskType", value } : { field: "evaluationProfile", value };
|
|
747
|
+
}
|
|
748
|
+
|
|
598
749
|
const MISSION_SCOPE_FILTER_STOPWORDS = new Set([
|
|
599
750
|
"a",
|
|
600
751
|
"an",
|
|
@@ -654,11 +805,14 @@ function isSessionScopeItemMissionRelevant(item: string, mission: string): boole
|
|
|
654
805
|
const CONTEXT_PROPOSAL_ANALYST_SYSTEM_PROMPT = [
|
|
655
806
|
"You analyze recent /cook startup discussion and return a strict JSON object.",
|
|
656
807
|
"Do not emit markdown, code fences, or commentary.",
|
|
657
|
-
"Return exactly one JSON object with keys: mission, scope, constraints, acceptance, confidence, possible_noise.",
|
|
808
|
+
"Return exactly one JSON object with keys: mission, scope, constraints, acceptance, critique, risks, task_type, evaluation_profile, confidence, possible_noise.",
|
|
658
809
|
"mission must be a concise implementation mission anchor sentence.",
|
|
659
810
|
"scope must contain only work items that directly support the mission.",
|
|
660
811
|
"constraints must contain guardrails or non-goals explicitly stated or strongly implied by the discussion.",
|
|
661
812
|
"acceptance must contain verifiable outcomes explicitly stated or strongly implied by the discussion.",
|
|
813
|
+
"critique must contain operator-facing cautions, concerns, or reminders that should be shown separately from mission and scope later.",
|
|
814
|
+
"risks must contain concrete failure modes or regressions that the later workflow should keep in view.",
|
|
815
|
+
"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
816
|
"possible_noise should list discussion points that look stale, weakly related, or unsafe to promote into scope.",
|
|
663
817
|
"When an explicit goal is provided, keep the mission anchored to that goal instead of replacing it with a broader or different mission.",
|
|
664
818
|
"When discussion is insufficient, prefer empty arrays and a low confidence value over invention.",
|
|
@@ -739,12 +893,27 @@ function parseContextProposalAnalystOutput(
|
|
|
739
893
|
const scope = uniqueProposalItems(asStringArray(parsed.scope));
|
|
740
894
|
const constraints = uniqueProposalItems(asStringArray(parsed.constraints));
|
|
741
895
|
const acceptance = uniqueProposalItems(asStringArray(parsed.acceptance));
|
|
896
|
+
const analysis = mergeContextProposalAnalysis(
|
|
897
|
+
[
|
|
898
|
+
explicit?.analysis,
|
|
899
|
+
buildContextProposalAnalysis({
|
|
900
|
+
taskType: parsed.task_type ?? parsed.taskType,
|
|
901
|
+
evaluationProfile: parsed.evaluation_profile ?? parsed.evaluationProfile,
|
|
902
|
+
critique: asStringArray(parsed.critique),
|
|
903
|
+
risks: asStringArray(parsed.risks ?? parsed.risk),
|
|
904
|
+
possibleNoise: asStringArray(parsed.possible_noise ?? parsed.possibleNoise),
|
|
905
|
+
hintTexts: [raw, mission, ...scope, ...constraints, ...acceptance],
|
|
906
|
+
}),
|
|
907
|
+
],
|
|
908
|
+
[explicitGoal ?? "", raw, mission, ...scope, ...constraints, ...acceptance],
|
|
909
|
+
);
|
|
742
910
|
const goalText = buildContextProposalGoalText({ mission, scope, constraints, acceptance });
|
|
743
911
|
return {
|
|
744
912
|
mission,
|
|
745
913
|
scope,
|
|
746
914
|
constraints,
|
|
747
915
|
acceptance,
|
|
916
|
+
analysis,
|
|
748
917
|
goalText,
|
|
749
918
|
basisPreview: raw.replace(/\s+/g, " ").trim(),
|
|
750
919
|
source: "analyst",
|
|
@@ -963,6 +1132,59 @@ function buildContextProposalDisplayText(proposal: ContextProposal): string {
|
|
|
963
1132
|
return lines.join("\n");
|
|
964
1133
|
}
|
|
965
1134
|
|
|
1135
|
+
function finalizeContextProposalAnalysis(analysis: ContextProposalAnalysis | undefined, hintTexts: string[] = []): ContextProposalAnalysis {
|
|
1136
|
+
const merged = mergeContextProposalAnalysis(analysis ? [analysis] : [], hintTexts);
|
|
1137
|
+
return {
|
|
1138
|
+
taskType: merged.taskType ?? DEFAULT_TASK_TYPE,
|
|
1139
|
+
evaluationProfile: merged.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
|
|
1140
|
+
critique: merged.critique,
|
|
1141
|
+
risks: merged.risks,
|
|
1142
|
+
possibleNoise: merged.possibleNoise,
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function buildContextProposalCritiqueText(analysis: ContextProposalAnalysis): string {
|
|
1147
|
+
const lines: string[] = [];
|
|
1148
|
+
if (analysis.critique.length > 0) {
|
|
1149
|
+
lines.push("Critique");
|
|
1150
|
+
for (const item of analysis.critique) lines.push(`- ${item}`);
|
|
1151
|
+
}
|
|
1152
|
+
if (analysis.risks.length > 0) {
|
|
1153
|
+
if (lines.length > 0) lines.push("");
|
|
1154
|
+
lines.push("Risks");
|
|
1155
|
+
for (const item of analysis.risks) lines.push(`- ${item}`);
|
|
1156
|
+
}
|
|
1157
|
+
if (analysis.possibleNoise.length > 0) {
|
|
1158
|
+
if (lines.length > 0) lines.push("");
|
|
1159
|
+
lines.push("Possible noise");
|
|
1160
|
+
for (const item of analysis.possibleNoise) lines.push(`- ${item}`);
|
|
1161
|
+
}
|
|
1162
|
+
if (lines.length === 0) {
|
|
1163
|
+
return "No critique, risk, or possible-noise notes were derived for this startup proposal.";
|
|
1164
|
+
}
|
|
1165
|
+
return lines.join("\n");
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function buildContextProposalRoutingText(analysis: ContextProposalAnalysis): string {
|
|
1169
|
+
return [`- task_type: ${analysis.taskType ?? DEFAULT_TASK_TYPE}`, `- evaluation_profile: ${analysis.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE}`].join(
|
|
1170
|
+
"\n",
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function summarizeContextProposalAnalysisItems(label: string, items: string[]): string | undefined {
|
|
1175
|
+
if (items.length === 0) return undefined;
|
|
1176
|
+
return `${label}=${truncateInline(items.join(" | "), 160)}`;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function buildContextProposalContinuationReason(prefix: string, goalText: string, analysis: ContextProposalAnalysis): string {
|
|
1180
|
+
const critiqueParts = [
|
|
1181
|
+
analysis.critique.length > 0 ? `accepted critique=${truncateInline(analysis.critique.join(" | "), 160)}` : "accepted critique=none",
|
|
1182
|
+
summarizeContextProposalAnalysisItems("risks", analysis.risks),
|
|
1183
|
+
summarizeContextProposalAnalysisItems("possible_noise", analysis.possibleNoise),
|
|
1184
|
+
].filter((part): part is string => Boolean(part));
|
|
1185
|
+
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("; ")}`;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
966
1188
|
function buildContextProposalConfirmationActions(): ContextProposalConfirmationActionItem[] {
|
|
967
1189
|
return [
|
|
968
1190
|
{
|
|
@@ -987,11 +1209,16 @@ function buildContextProposalConfirmationLayout(
|
|
|
987
1209
|
title: string,
|
|
988
1210
|
proposal: ContextProposal,
|
|
989
1211
|
): ContextProposalConfirmationLayout {
|
|
1212
|
+
const analysis = finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]);
|
|
990
1213
|
return {
|
|
991
1214
|
title,
|
|
992
|
-
intro: "Review the proposed mission, scope, constraints, and
|
|
1215
|
+
intro: "Review the proposed mission, scope, constraints, acceptance, critique, and routing details before /cook writes canonical workflow state.",
|
|
993
1216
|
proposalHeading: "Proposed workflow",
|
|
994
1217
|
proposalBody: buildContextProposalDisplayText(proposal),
|
|
1218
|
+
critiqueHeading: "Critique and risks",
|
|
1219
|
+
critiqueBody: buildContextProposalCritiqueText(analysis),
|
|
1220
|
+
routingHeading: "Routing recommendations",
|
|
1221
|
+
routingBody: buildContextProposalRoutingText(analysis),
|
|
995
1222
|
actionsHeading: "Actions",
|
|
996
1223
|
actions: buildContextProposalConfirmationActions(),
|
|
997
1224
|
footer: "↑↓ navigate • enter select • esc cancel",
|
|
@@ -1009,6 +1236,17 @@ function maybeWriteContextProposalConfirmationSnapshot(layout: ContextProposalCo
|
|
|
1009
1236
|
}
|
|
1010
1237
|
}
|
|
1011
1238
|
|
|
1239
|
+
function maybeWriteContextProposalSnapshot(proposal: ContextProposal): void {
|
|
1240
|
+
const snapshotPath = completionTestContextProposalSnapshotPath();
|
|
1241
|
+
if (!snapshotPath) return;
|
|
1242
|
+
try {
|
|
1243
|
+
fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });
|
|
1244
|
+
fs.writeFileSync(snapshotPath, `${JSON.stringify(proposal, null, 2)}\n`, "utf8");
|
|
1245
|
+
} catch {
|
|
1246
|
+
// ignore malformed or unwritable test snapshot paths
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1012
1250
|
function buildContextProposalConfirmationSelectItems(layout: ContextProposalConfirmationLayout): SelectItem[] {
|
|
1013
1251
|
return layout.actions.map((action) => ({
|
|
1014
1252
|
value: action.id,
|
|
@@ -1030,6 +1268,16 @@ async function promptContextProposalConfirmationAction(
|
|
|
1030
1268
|
container.addChild(new Text("", 0, 0));
|
|
1031
1269
|
container.addChild(new Text(theme.fg("accent", theme.bold(layout.proposalHeading)), 1, 0));
|
|
1032
1270
|
container.addChild(new Text(layout.proposalBody, 1, 0));
|
|
1271
|
+
if (layout.critiqueHeading && layout.critiqueBody) {
|
|
1272
|
+
container.addChild(new Text("", 0, 0));
|
|
1273
|
+
container.addChild(new Text(theme.fg("accent", theme.bold(layout.critiqueHeading)), 1, 0));
|
|
1274
|
+
container.addChild(new Text(layout.critiqueBody, 1, 0));
|
|
1275
|
+
}
|
|
1276
|
+
if (layout.routingHeading && layout.routingBody) {
|
|
1277
|
+
container.addChild(new Text("", 0, 0));
|
|
1278
|
+
container.addChild(new Text(theme.fg("accent", theme.bold(layout.routingHeading)), 1, 0));
|
|
1279
|
+
container.addChild(new Text(layout.routingBody, 1, 0));
|
|
1280
|
+
}
|
|
1033
1281
|
container.addChild(new Text("", 0, 0));
|
|
1034
1282
|
container.addChild(new Text(theme.fg("accent", theme.bold(layout.actionsHeading)), 1, 0));
|
|
1035
1283
|
const selectList = new SelectList(items, items.length, {
|
|
@@ -1065,17 +1313,28 @@ async function resolveEditedContextProposalDecision(
|
|
|
1065
1313
|
projectName: string,
|
|
1066
1314
|
editedText: string,
|
|
1067
1315
|
confirmMissionWhenNeeded: boolean,
|
|
1316
|
+
fallbackAnalysis?: ContextProposalAnalysis,
|
|
1068
1317
|
): Promise<ContextProposalDecision | undefined> {
|
|
1069
1318
|
if (!editedText.trim()) return undefined;
|
|
1070
1319
|
const editedProposal = parseContextProposal(editedText, projectName);
|
|
1071
|
-
if (editedProposal)
|
|
1320
|
+
if (editedProposal) {
|
|
1321
|
+
return {
|
|
1322
|
+
missionAnchor: editedProposal.mission,
|
|
1323
|
+
goalText: editedProposal.goalText,
|
|
1324
|
+
analysis: finalizeContextProposalAnalysis(
|
|
1325
|
+
mergeContextProposalAnalysis([editedProposal.analysis, fallbackAnalysis], [editedText, editedProposal.mission]),
|
|
1326
|
+
[editedText, editedProposal.mission],
|
|
1327
|
+
),
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1072
1330
|
const assessment = assessMissionAnchor(editedText, projectName);
|
|
1331
|
+
const analysis = finalizeContextProposalAnalysis(fallbackAnalysis, [editedText, assessment.derived]);
|
|
1073
1332
|
if (!confirmMissionWhenNeeded) {
|
|
1074
|
-
return { missionAnchor: assessment.derived, goalText: editedText.trim() };
|
|
1333
|
+
return { missionAnchor: assessment.derived, goalText: editedText.trim(), analysis };
|
|
1075
1334
|
}
|
|
1076
1335
|
const missionAnchor = await confirmMissionAnchor(ctx, assessment);
|
|
1077
1336
|
if (!missionAnchor) return undefined;
|
|
1078
|
-
return { missionAnchor, goalText: editedText.trim() };
|
|
1337
|
+
return { missionAnchor, goalText: editedText.trim(), analysis };
|
|
1079
1338
|
}
|
|
1080
1339
|
|
|
1081
1340
|
async function resolveContextProposalConfirmationAction(
|
|
@@ -1087,8 +1346,9 @@ async function resolveContextProposalConfirmationAction(
|
|
|
1087
1346
|
editedTextOverride?: string,
|
|
1088
1347
|
): Promise<ContextProposalDecision | undefined> {
|
|
1089
1348
|
if (action === "cancel") return undefined;
|
|
1349
|
+
const analysis = finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]);
|
|
1090
1350
|
if (action === "start") {
|
|
1091
|
-
return { missionAnchor: proposal.mission, goalText: proposal.goalText };
|
|
1351
|
+
return { missionAnchor: proposal.mission, goalText: proposal.goalText, analysis };
|
|
1092
1352
|
}
|
|
1093
1353
|
const editedText =
|
|
1094
1354
|
editedTextOverride ??
|
|
@@ -1097,7 +1357,7 @@ async function resolveContextProposalConfirmationAction(
|
|
|
1097
1357
|
buildContextProposalEditorText(proposal),
|
|
1098
1358
|
));
|
|
1099
1359
|
if (!editedText?.trim()) return undefined;
|
|
1100
|
-
return await resolveEditedContextProposalDecision(ctx, projectName, editedText, editedTextOverride === undefined);
|
|
1360
|
+
return await resolveEditedContextProposalDecision(ctx, projectName, editedText, editedTextOverride === undefined, analysis);
|
|
1101
1361
|
}
|
|
1102
1362
|
|
|
1103
1363
|
function buildContextProposalEditorText(proposal: ContextProposal): string {
|
|
@@ -1113,14 +1373,25 @@ function parseContextProposal(text: string, projectName: string): ContextProposa
|
|
|
1113
1373
|
.filter((line) => line.length > 0);
|
|
1114
1374
|
if (lines.length === 0) return undefined;
|
|
1115
1375
|
|
|
1116
|
-
let section:
|
|
1376
|
+
let section: ContextProposalSection | undefined;
|
|
1117
1377
|
let missionLine: string | undefined;
|
|
1378
|
+
let taskTypeHint: string | undefined;
|
|
1379
|
+
let evaluationProfileHint: string | undefined;
|
|
1118
1380
|
const scope: string[] = [];
|
|
1119
1381
|
const constraints: string[] = [];
|
|
1120
1382
|
const acceptance: string[] = [];
|
|
1383
|
+
const critique: string[] = [];
|
|
1384
|
+
const risks: string[] = [];
|
|
1121
1385
|
let structuredSignalCount = 0;
|
|
1122
1386
|
|
|
1123
1387
|
for (const rawLine of lines) {
|
|
1388
|
+
const routingHint = matchContextProposalRoutingHint(rawLine);
|
|
1389
|
+
if (routingHint) {
|
|
1390
|
+
structuredSignalCount += 1;
|
|
1391
|
+
if (routingHint.field === "taskType") taskTypeHint = routingHint.value;
|
|
1392
|
+
else evaluationProfileHint = routingHint.value;
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1124
1395
|
const inlineSection = matchInlineProposalSection(rawLine);
|
|
1125
1396
|
if (inlineSection) {
|
|
1126
1397
|
section = inlineSection.section;
|
|
@@ -1133,6 +1404,10 @@ function parseContextProposal(text: string, projectName: string): ContextProposa
|
|
|
1133
1404
|
acceptance.push(inlineSection.content);
|
|
1134
1405
|
} else if (inlineSection.section === "scope") {
|
|
1135
1406
|
scope.push(inlineSection.content);
|
|
1407
|
+
} else if (inlineSection.section === "critique") {
|
|
1408
|
+
critique.push(inlineSection.content);
|
|
1409
|
+
} else if (inlineSection.section === "risks") {
|
|
1410
|
+
risks.push(inlineSection.content);
|
|
1136
1411
|
}
|
|
1137
1412
|
continue;
|
|
1138
1413
|
}
|
|
@@ -1161,6 +1436,14 @@ function parseContextProposal(text: string, projectName: string): ContextProposa
|
|
|
1161
1436
|
scope.push(bullet);
|
|
1162
1437
|
continue;
|
|
1163
1438
|
}
|
|
1439
|
+
if (section === "critique") {
|
|
1440
|
+
critique.push(bullet);
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1443
|
+
if (section === "risks") {
|
|
1444
|
+
risks.push(bullet);
|
|
1445
|
+
continue;
|
|
1446
|
+
}
|
|
1164
1447
|
if (!missionLine) {
|
|
1165
1448
|
missionLine = bullet;
|
|
1166
1449
|
continue;
|
|
@@ -1176,6 +1459,14 @@ function parseContextProposal(text: string, projectName: string): ContextProposa
|
|
|
1176
1459
|
missionLine = normalized;
|
|
1177
1460
|
continue;
|
|
1178
1461
|
}
|
|
1462
|
+
if (section === "critique") {
|
|
1463
|
+
critique.push(normalized);
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
if (section === "risks") {
|
|
1467
|
+
risks.push(normalized);
|
|
1468
|
+
continue;
|
|
1469
|
+
}
|
|
1179
1470
|
if (section === "constraints" || looksLikeConstraint(normalized)) {
|
|
1180
1471
|
constraints.push(normalized);
|
|
1181
1472
|
continue;
|
|
@@ -1193,17 +1484,25 @@ function parseContextProposal(text: string, projectName: string): ContextProposa
|
|
|
1193
1484
|
const missionSource = missionLine ?? scope[0] ?? acceptance[0] ?? constraints[0] ?? basisPreview;
|
|
1194
1485
|
const assessment = assessMissionAnchor(missionSource, projectName);
|
|
1195
1486
|
const normalizedMission = normalizeMissionAnchorText(missionSource);
|
|
1196
|
-
const itemCount = scope.length + constraints.length + acceptance.length;
|
|
1487
|
+
const itemCount = scope.length + constraints.length + acceptance.length + critique.length + risks.length;
|
|
1197
1488
|
const hasStrongStructure = structuredSignalCount >= 2 || itemCount >= 2;
|
|
1198
1489
|
if (!normalizedMission || isWeakMissionAnchor(normalizedMission)) return undefined;
|
|
1199
1490
|
if (!hasStrongStructure && basisPreview.length < 140) return undefined;
|
|
1200
1491
|
const mission = assessment.derived;
|
|
1492
|
+
const analysis = buildContextProposalAnalysis({
|
|
1493
|
+
taskType: taskTypeHint,
|
|
1494
|
+
evaluationProfile: evaluationProfileHint,
|
|
1495
|
+
critique,
|
|
1496
|
+
risks,
|
|
1497
|
+
hintTexts: [cleaned, mission, ...scope, ...constraints, ...acceptance, ...critique, ...risks],
|
|
1498
|
+
});
|
|
1201
1499
|
const goalText = buildContextProposalGoalText({ mission, scope, constraints, acceptance });
|
|
1202
1500
|
return {
|
|
1203
1501
|
mission,
|
|
1204
1502
|
scope,
|
|
1205
1503
|
constraints,
|
|
1206
1504
|
acceptance,
|
|
1505
|
+
analysis,
|
|
1207
1506
|
goalText,
|
|
1208
1507
|
basisPreview,
|
|
1209
1508
|
source: "session",
|
|
@@ -1234,15 +1533,20 @@ async function buildGoalAnchoredContextProposal(
|
|
|
1234
1533
|
const scope = uniqueProposalItems([...explicitScope, ...sessionScope]);
|
|
1235
1534
|
const constraints = uniqueProposalItems([...(explicit?.constraints ?? []), ...(sessionProposal?.constraints ?? [])]);
|
|
1236
1535
|
const acceptance = uniqueProposalItems([...(explicit?.acceptance ?? []), ...(sessionProposal?.acceptance ?? [])]);
|
|
1536
|
+
const analysis = mergeContextProposalAnalysis(
|
|
1537
|
+
[explicit?.analysis, sessionProposal?.analysis],
|
|
1538
|
+
[goal, mission, ...(sessionProposal?.analysis.possibleNoise ?? []), ...scope, ...constraints, ...acceptance],
|
|
1539
|
+
);
|
|
1237
1540
|
const goalText = buildContextProposalGoalText({ mission, scope, constraints, acceptance });
|
|
1238
1541
|
return {
|
|
1239
1542
|
mission,
|
|
1240
1543
|
scope,
|
|
1241
1544
|
constraints,
|
|
1242
1545
|
acceptance,
|
|
1546
|
+
analysis,
|
|
1243
1547
|
goalText,
|
|
1244
1548
|
basisPreview: sessionProposal?.basisPreview ?? explicit?.basisPreview ?? goal,
|
|
1245
|
-
source: sessionProposal?.source ?? "session",
|
|
1549
|
+
source: sessionProposal?.source ?? explicit?.source ?? "session",
|
|
1246
1550
|
};
|
|
1247
1551
|
}
|
|
1248
1552
|
|
|
@@ -1252,15 +1556,26 @@ async function confirmContextProposal(
|
|
|
1252
1556
|
projectName: string,
|
|
1253
1557
|
options: ContextProposalConfirmOptions,
|
|
1254
1558
|
): Promise<ContextProposalDecision | undefined> {
|
|
1559
|
+
maybeWriteContextProposalSnapshot(proposal);
|
|
1255
1560
|
const actionOverride = completionTestContextProposalActionOverride();
|
|
1256
1561
|
if (actionOverride === "cancel") return undefined;
|
|
1257
1562
|
if (actionOverride === "accept") {
|
|
1258
|
-
return {
|
|
1563
|
+
return {
|
|
1564
|
+
missionAnchor: proposal.mission,
|
|
1565
|
+
goalText: proposal.goalText,
|
|
1566
|
+
analysis: finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]),
|
|
1567
|
+
};
|
|
1259
1568
|
}
|
|
1260
1569
|
if (actionOverride === "edit") {
|
|
1261
1570
|
const editedText = completionTestContextProposalEditText();
|
|
1262
1571
|
if (!editedText) return undefined;
|
|
1263
|
-
return await resolveEditedContextProposalDecision(
|
|
1572
|
+
return await resolveEditedContextProposalDecision(
|
|
1573
|
+
ctx,
|
|
1574
|
+
projectName,
|
|
1575
|
+
editedText,
|
|
1576
|
+
false,
|
|
1577
|
+
finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]),
|
|
1578
|
+
);
|
|
1264
1579
|
}
|
|
1265
1580
|
const layout = buildContextProposalConfirmationLayout(options.title, proposal);
|
|
1266
1581
|
maybeWriteContextProposalConfirmationSnapshot(layout);
|
|
@@ -1277,13 +1592,21 @@ async function confirmContextProposal(
|
|
|
1277
1592
|
}
|
|
1278
1593
|
if (!getCtxHasUI(ctx)) {
|
|
1279
1594
|
return options.nonInteractiveBehavior === "accept"
|
|
1280
|
-
? {
|
|
1595
|
+
? {
|
|
1596
|
+
missionAnchor: proposal.mission,
|
|
1597
|
+
goalText: proposal.goalText,
|
|
1598
|
+
analysis: finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]),
|
|
1599
|
+
}
|
|
1281
1600
|
: undefined;
|
|
1282
1601
|
}
|
|
1283
1602
|
const ui = getCtxUi(ctx);
|
|
1284
1603
|
if (!ui) {
|
|
1285
1604
|
return options.nonInteractiveBehavior === "accept"
|
|
1286
|
-
? {
|
|
1605
|
+
? {
|
|
1606
|
+
missionAnchor: proposal.mission,
|
|
1607
|
+
goalText: proposal.goalText,
|
|
1608
|
+
analysis: finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]),
|
|
1609
|
+
}
|
|
1287
1610
|
: undefined;
|
|
1288
1611
|
}
|
|
1289
1612
|
const choice = await promptContextProposalConfirmationAction(ui, layout);
|
|
@@ -1300,6 +1623,80 @@ function currentMissionAnchor(snapshot: CompletionStateSnapshot): string {
|
|
|
1300
1623
|
);
|
|
1301
1624
|
}
|
|
1302
1625
|
|
|
1626
|
+
function currentTaskType(snapshot: CompletionStateSnapshot): string | undefined {
|
|
1627
|
+
return (
|
|
1628
|
+
asString(snapshot.active?.task_type) ??
|
|
1629
|
+
asString(snapshot.state?.task_type) ??
|
|
1630
|
+
asString(snapshot.plan?.task_type) ??
|
|
1631
|
+
asString(snapshot.profile?.task_type)
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
function currentEvaluationProfile(snapshot: CompletionStateSnapshot): string | undefined {
|
|
1636
|
+
return (
|
|
1637
|
+
asString(snapshot.active?.evaluation_profile) ??
|
|
1638
|
+
asString(snapshot.state?.evaluation_profile) ??
|
|
1639
|
+
asString(snapshot.plan?.evaluation_profile) ??
|
|
1640
|
+
asString(snapshot.profile?.evaluation_profile)
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
function isRubricEvaluationRole(role: string | undefined): role is RubricEvaluationRole {
|
|
1645
|
+
return RUBRIC_EVALUATION_ROLES.includes(role as RubricEvaluationRole);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
function activeSliceContext(snapshot: CompletionStateSnapshot) {
|
|
1649
|
+
const active = snapshot.active;
|
|
1650
|
+
const activeSlice = snapshot.activeSlice;
|
|
1651
|
+
return {
|
|
1652
|
+
sliceId: asString(active?.slice_id) ?? asString(activeSlice?.slice_id),
|
|
1653
|
+
status: asString(active?.status) ?? asString(activeSlice?.status),
|
|
1654
|
+
goal: asString(active?.goal) ?? asString(activeSlice?.goal),
|
|
1655
|
+
contractIds:
|
|
1656
|
+
asStringArray(active?.contract_ids).length > 0 ? asStringArray(active?.contract_ids) : asStringArray(activeSlice?.contract_ids),
|
|
1657
|
+
acceptance:
|
|
1658
|
+
asStringArray(active?.acceptance_criteria).length > 0
|
|
1659
|
+
? asStringArray(active?.acceptance_criteria)
|
|
1660
|
+
: asStringArray(activeSlice?.acceptance_criteria),
|
|
1661
|
+
implementationSurfaces: asStringArray(active?.implementation_surfaces),
|
|
1662
|
+
verificationCommands: asStringArray(active?.verification_commands),
|
|
1663
|
+
lockedNotes: asStringArray(active?.locked_notes),
|
|
1664
|
+
mustFixFindings: asStringArray(active?.must_fix_findings),
|
|
1665
|
+
remainingBefore: asStringArray(active?.remaining_contract_ids_before),
|
|
1666
|
+
basisCommit: asString(active?.basis_commit),
|
|
1667
|
+
releaseBlockerCountBefore: asNumber(active?.release_blocker_count_before),
|
|
1668
|
+
highValueGapCountBefore: asNumber(active?.high_value_gap_count_before),
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
function buildEvaluationRoleContextLines(snapshot: CompletionStateSnapshot, role: RubricEvaluationRole): string[] {
|
|
1673
|
+
const context = activeSliceContext(snapshot);
|
|
1674
|
+
const lines = [
|
|
1675
|
+
`Canonical evaluation handoff for ${role}:`,
|
|
1676
|
+
`- task_type: ${currentTaskType(snapshot) ?? "(missing)"}`,
|
|
1677
|
+
`- evaluation_profile: ${currentEvaluationProfile(snapshot) ?? "(missing)"}`,
|
|
1678
|
+
`- latest_completed_slice: ${asString(snapshot.state?.latest_completed_slice) ?? "(none)"}`,
|
|
1679
|
+
`- active_slice_id: ${context.sliceId ?? "(none)"}`,
|
|
1680
|
+
`- active_slice_status: ${context.status ?? "(unknown)"}`,
|
|
1681
|
+
`- active_slice_goal: ${context.goal ?? "(unknown)"}`,
|
|
1682
|
+
`- contract_ids: ${context.contractIds.length > 0 ? context.contractIds.join(", ") : "(none)"}`,
|
|
1683
|
+
`- acceptance_criteria: ${context.acceptance.length > 0 ? context.acceptance.join(" | ") : "(none)"}`,
|
|
1684
|
+
`- implementation_surfaces: ${context.implementationSurfaces.length > 0 ? context.implementationSurfaces.join(" | ") : "(none)"}`,
|
|
1685
|
+
`- verification_commands: ${context.verificationCommands.length > 0 ? context.verificationCommands.join(" | ") : "(none)"}`,
|
|
1686
|
+
`- locked_notes: ${context.lockedNotes.length > 0 ? context.lockedNotes.join(" | ") : "(none)"}`,
|
|
1687
|
+
`- must_fix_findings: ${context.mustFixFindings.length > 0 ? context.mustFixFindings.join(" | ") : "(none)"}`,
|
|
1688
|
+
`- basis_commit: ${context.basisCommit ?? "(none)"}`,
|
|
1689
|
+
`- remaining_contract_ids_before: ${context.remainingBefore.length > 0 ? context.remainingBefore.join(", ") : "(none)"}`,
|
|
1690
|
+
`- release_blocker_count_before: ${context.releaseBlockerCountBefore ?? "(unknown)"}`,
|
|
1691
|
+
`- high_value_gap_count_before: ${context.highValueGapCountBefore ?? "(unknown)"}`,
|
|
1692
|
+
];
|
|
1693
|
+
return lines;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function buildEvaluationRoleReminderText(snapshot: CompletionStateSnapshot, role: RubricEvaluationRole): string {
|
|
1697
|
+
return buildEvaluationRoleContextLines(snapshot, role).join(" ");
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1303
1700
|
async function confirmExistingWorkflowGoal(
|
|
1304
1701
|
ctx: { hasUI: boolean; ui: any },
|
|
1305
1702
|
snapshot: CompletionStateSnapshot,
|
|
@@ -1351,22 +1748,41 @@ async function confirmExistingWorkflowGoal(
|
|
|
1351
1748
|
return { action: "continue", currentMissionAnchor: currentMission };
|
|
1352
1749
|
}
|
|
1353
1750
|
|
|
1354
|
-
async function refocusCompletionMission(
|
|
1751
|
+
async function refocusCompletionMission(
|
|
1752
|
+
snapshot: CompletionStateSnapshot,
|
|
1753
|
+
missionAnchor: string,
|
|
1754
|
+
rawGoal: string,
|
|
1755
|
+
analysis?: ContextProposalAnalysis,
|
|
1756
|
+
): Promise<void> {
|
|
1355
1757
|
const requiredStopJudges = asNumber(snapshot.profile?.required_stop_judges) ?? 3;
|
|
1356
1758
|
const root = snapshot.files.root;
|
|
1759
|
+
const routing = finalizeContextProposalAnalysis(analysis, [rawGoal, missionAnchor]);
|
|
1760
|
+
const docsSurfaces = asStringArray(snapshot.profile?.docs_surfaces);
|
|
1761
|
+
const nextProfile = buildProfileRecord({
|
|
1762
|
+
projectName: asString(snapshot.profile?.project_name) ?? path.basename(root),
|
|
1763
|
+
requiredStopJudges,
|
|
1764
|
+
priorityPolicyId: asString(snapshot.profile?.priority_policy_id) ?? "completion-default",
|
|
1765
|
+
docsSurfaces: docsSurfaces.length > 0 ? docsSurfaces : await detectDocsSurfaces(root),
|
|
1766
|
+
taskType: routing.taskType,
|
|
1767
|
+
evaluationProfile: routing.evaluationProfile,
|
|
1768
|
+
});
|
|
1357
1769
|
const nextState = {
|
|
1358
|
-
...defaultState(missionAnchor
|
|
1770
|
+
...defaultState(missionAnchor, {
|
|
1771
|
+
taskType: routing.taskType,
|
|
1772
|
+
evaluationProfile: routing.evaluationProfile,
|
|
1773
|
+
continuationReason: buildContextProposalContinuationReason("User refocused workflow via /cook:", rawGoal, routing),
|
|
1774
|
+
}),
|
|
1359
1775
|
remaining_stop_judges: requiredStopJudges,
|
|
1360
|
-
continuation_reason: `User refocused workflow via /cook: ${truncateInline(rawGoal, 160)}`,
|
|
1361
1776
|
next_mandatory_action: "Reconcile canonical state from current repo truth for the refocused mission",
|
|
1362
1777
|
};
|
|
1363
1778
|
const nextPlan = {
|
|
1364
|
-
...defaultPlan(missionAnchor),
|
|
1779
|
+
...defaultPlan(missionAnchor, { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile }),
|
|
1365
1780
|
plan_basis: "user_refocus",
|
|
1366
1781
|
};
|
|
1367
|
-
const nextActive = defaultActiveSlice(missionAnchor);
|
|
1782
|
+
const nextActive = defaultActiveSlice(missionAnchor, { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile });
|
|
1368
1783
|
await Promise.all([
|
|
1369
1784
|
fsp.writeFile(path.join(snapshot.files.agentDir, "mission.md"), buildMission(path.basename(root), missionAnchor), "utf8"),
|
|
1785
|
+
writeJsonFile(snapshot.files.profilePath, nextProfile),
|
|
1370
1786
|
writeJsonFile(snapshot.files.statePath, nextState),
|
|
1371
1787
|
writeJsonFile(snapshot.files.planPath, nextPlan),
|
|
1372
1788
|
writeJsonFile(snapshot.files.activePath, nextActive),
|
|
@@ -1398,14 +1814,39 @@ function deriveMissionAnchor(rawGoal: string, projectName: string): string {
|
|
|
1398
1814
|
return mission;
|
|
1399
1815
|
}
|
|
1400
1816
|
|
|
1401
|
-
function
|
|
1817
|
+
function buildProfileRecord(args: {
|
|
1818
|
+
projectName: string;
|
|
1819
|
+
requiredStopJudges: number;
|
|
1820
|
+
priorityPolicyId?: string;
|
|
1821
|
+
docsSurfaces: string[];
|
|
1822
|
+
taskType?: string;
|
|
1823
|
+
evaluationProfile?: string;
|
|
1824
|
+
}): JsonRecord {
|
|
1825
|
+
return {
|
|
1826
|
+
schema_version: 1,
|
|
1827
|
+
protocol_id: PROTOCOL_ID,
|
|
1828
|
+
project_name: args.projectName,
|
|
1829
|
+
required_stop_judges: args.requiredStopJudges,
|
|
1830
|
+
priority_policy_id: args.priorityPolicyId ?? "completion-default",
|
|
1831
|
+
task_type: args.taskType ?? DEFAULT_TASK_TYPE,
|
|
1832
|
+
evaluation_profile: args.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
|
|
1833
|
+
docs_surfaces: args.docsSurfaces,
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
function defaultState(
|
|
1838
|
+
missionAnchor: string,
|
|
1839
|
+
routing?: { taskType?: string; evaluationProfile?: string; continuationReason?: string },
|
|
1840
|
+
): JsonRecord {
|
|
1402
1841
|
return {
|
|
1403
1842
|
schema_version: 1,
|
|
1404
1843
|
mission_anchor: missionAnchor,
|
|
1405
1844
|
current_phase: "reground",
|
|
1406
1845
|
continuation_policy: "continue",
|
|
1407
|
-
continuation_reason: "Fresh completion bootstrap requires canonical re-ground",
|
|
1846
|
+
continuation_reason: routing?.continuationReason ?? "Fresh completion bootstrap requires canonical re-ground",
|
|
1408
1847
|
project_done: false,
|
|
1848
|
+
task_type: routing?.taskType ?? DEFAULT_TASK_TYPE,
|
|
1849
|
+
evaluation_profile: routing?.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
|
|
1409
1850
|
requires_reground: true,
|
|
1410
1851
|
slices_since_last_reground: 0,
|
|
1411
1852
|
remaining_release_blockers: null,
|
|
@@ -1423,20 +1864,30 @@ function defaultState(missionAnchor: string): JsonRecord {
|
|
|
1423
1864
|
};
|
|
1424
1865
|
}
|
|
1425
1866
|
|
|
1426
|
-
function defaultPlan(
|
|
1867
|
+
function defaultPlan(
|
|
1868
|
+
missionAnchor: string,
|
|
1869
|
+
routing?: { taskType?: string; evaluationProfile?: string },
|
|
1870
|
+
): JsonRecord {
|
|
1427
1871
|
return {
|
|
1428
1872
|
schema_version: 1,
|
|
1429
1873
|
mission_anchor: missionAnchor,
|
|
1874
|
+
task_type: routing?.taskType ?? DEFAULT_TASK_TYPE,
|
|
1875
|
+
evaluation_profile: routing?.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
|
|
1430
1876
|
last_reground_at: null,
|
|
1431
1877
|
plan_basis: "bootstrap",
|
|
1432
1878
|
candidate_slices: [],
|
|
1433
1879
|
};
|
|
1434
1880
|
}
|
|
1435
1881
|
|
|
1436
|
-
function defaultActiveSlice(
|
|
1882
|
+
function defaultActiveSlice(
|
|
1883
|
+
missionAnchor: string,
|
|
1884
|
+
routing?: { taskType?: string; evaluationProfile?: string },
|
|
1885
|
+
): JsonRecord {
|
|
1437
1886
|
return {
|
|
1438
1887
|
schema_version: 1,
|
|
1439
1888
|
mission_anchor: missionAnchor,
|
|
1889
|
+
task_type: routing?.taskType ?? DEFAULT_TASK_TYPE,
|
|
1890
|
+
evaluation_profile: routing?.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
|
|
1440
1891
|
status: "idle",
|
|
1441
1892
|
slice_id: null,
|
|
1442
1893
|
goal: null,
|
|
@@ -1447,6 +1898,8 @@ function defaultActiveSlice(missionAnchor: string): JsonRecord {
|
|
|
1447
1898
|
blocked_on: [],
|
|
1448
1899
|
locked_notes: [],
|
|
1449
1900
|
must_fix_findings: [],
|
|
1901
|
+
implementation_surfaces: [],
|
|
1902
|
+
verification_commands: [],
|
|
1450
1903
|
basis_commit: null,
|
|
1451
1904
|
remaining_contract_ids_before: [],
|
|
1452
1905
|
release_blocker_count_before: null,
|
|
@@ -1470,15 +1923,151 @@ function buildVerifyStopScript(verifierCommand?: string): string {
|
|
|
1470
1923
|
}
|
|
1471
1924
|
|
|
1472
1925
|
function buildVerifyControlPlaneScript(): string {
|
|
1473
|
-
return `#!/usr/bin/env bash
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
.agent/
|
|
1478
|
-
.agent/
|
|
1479
|
-
.agent/
|
|
1480
|
-
.agent/
|
|
1481
|
-
.agent/
|
|
1926
|
+
return `#!/usr/bin/env bash
|
|
1927
|
+
set -euo pipefail
|
|
1928
|
+
|
|
1929
|
+
for file in \
|
|
1930
|
+
.agent/README.md \
|
|
1931
|
+
.agent/mission.md \
|
|
1932
|
+
.agent/profile.json \
|
|
1933
|
+
.agent/verify_completion_stop.sh \
|
|
1934
|
+
.agent/verify_completion_control_plane.sh \
|
|
1935
|
+
.agent/state.json \
|
|
1936
|
+
.agent/plan.json \
|
|
1937
|
+
.agent/active-slice.json; do
|
|
1938
|
+
[[ -e "$file" ]] || { echo "missing required file: $file"; exit 1; }
|
|
1939
|
+
done
|
|
1940
|
+
|
|
1941
|
+
node <<'NODE'
|
|
1942
|
+
const fs = require('node:fs');
|
|
1943
|
+
|
|
1944
|
+
const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1945
|
+
const assert = (condition, message) => {
|
|
1946
|
+
if (!condition) {
|
|
1947
|
+
console.error(message);
|
|
1948
|
+
process.exit(1);
|
|
1949
|
+
}
|
|
1950
|
+
};
|
|
1951
|
+
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
1952
|
+
const isString = (value) => typeof value === 'string';
|
|
1953
|
+
const isNonEmptyString = (value) => isString(value) && value.length > 0;
|
|
1954
|
+
const isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
1955
|
+
const hasOnlyKeys = (object, allowed, label) => {
|
|
1956
|
+
const unknown = Object.keys(object).filter((key) => !allowed.includes(key));
|
|
1957
|
+
assert(unknown.length === 0, label + ': unknown keys: ' + unknown.join(', '));
|
|
1958
|
+
};
|
|
1959
|
+
const requireKeys = (object, required, label) => {
|
|
1960
|
+
for (const key of required) {
|
|
1961
|
+
assert(Object.prototype.hasOwnProperty.call(object, key), label + ': missing required field: ' + key);
|
|
1962
|
+
}
|
|
1963
|
+
};
|
|
1964
|
+
|
|
1965
|
+
for (const file of ['.agent/profile.json', '.agent/state.json', '.agent/plan.json', '.agent/active-slice.json']) {
|
|
1966
|
+
readJson(file);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
const profile = readJson('.agent/profile.json');
|
|
1970
|
+
const state = readJson('.agent/state.json');
|
|
1971
|
+
const plan = readJson('.agent/plan.json');
|
|
1972
|
+
const active = readJson('.agent/active-slice.json');
|
|
1973
|
+
|
|
1974
|
+
assert(isObject(profile), '.agent/profile.json must be an object');
|
|
1975
|
+
assert(isObject(state), '.agent/state.json must be an object');
|
|
1976
|
+
assert(isObject(plan), '.agent/plan.json must be an object');
|
|
1977
|
+
assert(isObject(active), '.agent/active-slice.json must be an object');
|
|
1978
|
+
|
|
1979
|
+
const requiredProfile = ['schema_version', 'protocol_id', 'project_name', 'required_stop_judges', 'priority_policy_id', 'task_type', 'evaluation_profile', 'docs_surfaces'];
|
|
1980
|
+
requireKeys(profile, requiredProfile, '.agent/profile.json');
|
|
1981
|
+
hasOnlyKeys(profile, requiredProfile, '.agent/profile.json');
|
|
1982
|
+
assert(profile.protocol_id === 'completion', '.agent/profile.json: protocol_id must be completion');
|
|
1983
|
+
assert(Array.isArray(profile.docs_surfaces), '.agent/profile.json: docs_surfaces must be an array');
|
|
1984
|
+
assert(isNonEmptyString(profile.task_type), '.agent/profile.json: task_type must be a non-empty string');
|
|
1985
|
+
assert(isNonEmptyString(profile.evaluation_profile), '.agent/profile.json: evaluation_profile must be a non-empty string');
|
|
1986
|
+
|
|
1987
|
+
const requiredState = [
|
|
1988
|
+
'schema_version','mission_anchor','task_type','evaluation_profile','current_phase','continuation_policy','continuation_reason','project_done',
|
|
1989
|
+
'requires_reground','slices_since_last_reground','remaining_release_blockers','remaining_high_value_gaps',
|
|
1990
|
+
'unsatisfied_contract_ids','release_blocker_ids','next_mandatory_action','next_mandatory_role',
|
|
1991
|
+
'remaining_stop_judges','last_reground_at','last_auditor_verdict','contract_status','latest_completed_slice','latest_verified_slice'
|
|
1992
|
+
];
|
|
1993
|
+
const continuationPolicies = ['continue', 'await_user_input', 'blocked', 'paused', 'done'];
|
|
1994
|
+
const workflowRoles = ['completion-bootstrapper', 'completion-regrounder', 'completion-implementer', 'completion-reviewer', 'completion-auditor', 'completion-stop-judge', null];
|
|
1995
|
+
const workflowPhases = ['reground', 'implement', 'post_commit_review', 'post_commit_audit', 'post_commit_reconcile', 'stop_wave', 'awaiting_user', 'blocked', 'done'];
|
|
1996
|
+
requireKeys(state, requiredState, '.agent/state.json');
|
|
1997
|
+
hasOnlyKeys(state, requiredState, '.agent/state.json');
|
|
1998
|
+
assert(continuationPolicies.includes(state.continuation_policy), '.agent/state.json: invalid continuation_policy');
|
|
1999
|
+
assert(workflowRoles.includes(state.next_mandatory_role), '.agent/state.json: invalid next_mandatory_role');
|
|
2000
|
+
assert(workflowPhases.includes(state.current_phase), '.agent/state.json: invalid current_phase');
|
|
2001
|
+
assert(isNonEmptyString(state.task_type), '.agent/state.json: task_type must be a non-empty string');
|
|
2002
|
+
assert(isNonEmptyString(state.evaluation_profile), '.agent/state.json: evaluation_profile must be a non-empty string');
|
|
2003
|
+
assert(isStringArray(state.unsatisfied_contract_ids), '.agent/state.json: unsatisfied_contract_ids must be an array of strings');
|
|
2004
|
+
assert(isStringArray(state.release_blocker_ids), '.agent/state.json: release_blocker_ids must be an array of strings');
|
|
2005
|
+
|
|
2006
|
+
const requiredPlan = ['schema_version', 'mission_anchor', 'task_type', 'evaluation_profile', 'last_reground_at', 'plan_basis', 'candidate_slices'];
|
|
2007
|
+
const requiredSlice = ['slice_id', 'goal', 'acceptance_criteria', 'contract_ids', 'priority', 'status', 'why_now', 'blocked_on', 'evidence'];
|
|
2008
|
+
const sliceStatuses = ['planned', 'selected', 'in_progress', 'blocked', 'done', 'cancelled'];
|
|
2009
|
+
requireKeys(plan, requiredPlan, '.agent/plan.json');
|
|
2010
|
+
hasOnlyKeys(plan, requiredPlan, '.agent/plan.json');
|
|
2011
|
+
assert(isNonEmptyString(plan.task_type), '.agent/plan.json: task_type must be a non-empty string');
|
|
2012
|
+
assert(isNonEmptyString(plan.evaluation_profile), '.agent/plan.json: evaluation_profile must be a non-empty string');
|
|
2013
|
+
assert(Array.isArray(plan.candidate_slices), '.agent/plan.json: candidate_slices must be an array');
|
|
2014
|
+
for (const [index, slice] of plan.candidate_slices.entries()) {
|
|
2015
|
+
const label = '.agent/plan.json candidate_slices[' + index + ']';
|
|
2016
|
+
assert(isObject(slice), label + ' must be an object');
|
|
2017
|
+
requireKeys(slice, requiredSlice, label);
|
|
2018
|
+
hasOnlyKeys(slice, requiredSlice, label);
|
|
2019
|
+
assert(isString(slice.slice_id) && slice.slice_id.length > 0, label + ': slice_id must be a non-empty string');
|
|
2020
|
+
assert(isString(slice.goal) && slice.goal.length > 0, label + ': goal must be a non-empty string');
|
|
2021
|
+
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');
|
|
2022
|
+
assert(isStringArray(slice.contract_ids), label + ': contract_ids must be an array of strings');
|
|
2023
|
+
assert(typeof slice.priority === 'number' && Number.isFinite(slice.priority), label + ': priority must be a finite number');
|
|
2024
|
+
assert(sliceStatuses.includes(slice.status), label + ': invalid status');
|
|
2025
|
+
assert(isString(slice.why_now) && slice.why_now.length > 0, label + ': why_now must be a non-empty string');
|
|
2026
|
+
assert(isStringArray(slice.blocked_on), label + ': blocked_on must be an array of strings');
|
|
2027
|
+
assert(isStringArray(slice.evidence), label + ': evidence must be an array of strings');
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
const isNonEmptyStringArray = (value) => Array.isArray(value) && value.length > 0 && value.every((item) => isNonEmptyString(item));
|
|
2031
|
+
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'];
|
|
2032
|
+
const allowedActive = [...requiredActiveBase, 'priority', 'why_now'];
|
|
2033
|
+
const activeStatuses = ['idle', 'selected', 'in_progress', 'committed', 'done'];
|
|
2034
|
+
requireKeys(active, requiredActiveBase, '.agent/active-slice.json');
|
|
2035
|
+
hasOnlyKeys(active, allowedActive, '.agent/active-slice.json');
|
|
2036
|
+
assert(activeStatuses.includes(active.status), '.agent/active-slice.json: invalid status');
|
|
2037
|
+
assert(isNonEmptyString(active.task_type), '.agent/active-slice.json: task_type must be a non-empty string');
|
|
2038
|
+
assert(isNonEmptyString(active.evaluation_profile), '.agent/active-slice.json: evaluation_profile must be a non-empty string');
|
|
2039
|
+
assert(isStringArray(active.contract_ids), '.agent/active-slice.json: contract_ids must be an array of strings');
|
|
2040
|
+
assert(Array.isArray(active.acceptance_criteria), '.agent/active-slice.json: acceptance_criteria must be an array');
|
|
2041
|
+
assert(isStringArray(active.blocked_on), '.agent/active-slice.json: blocked_on must be an array of strings');
|
|
2042
|
+
assert(isStringArray(active.locked_notes), '.agent/active-slice.json: locked_notes must be an array of strings');
|
|
2043
|
+
assert(isStringArray(active.must_fix_findings), '.agent/active-slice.json: must_fix_findings must be an array of strings');
|
|
2044
|
+
assert(isStringArray(active.implementation_surfaces), '.agent/active-slice.json: implementation_surfaces must be an array of strings');
|
|
2045
|
+
assert(isStringArray(active.verification_commands), '.agent/active-slice.json: verification_commands must be an array of strings');
|
|
2046
|
+
assert(isStringArray(active.remaining_contract_ids_before), '.agent/active-slice.json: remaining_contract_ids_before must be an array of strings');
|
|
2047
|
+
|
|
2048
|
+
assert(state.task_type === profile.task_type, '.agent/state.json: task_type must match .agent/profile.json');
|
|
2049
|
+
assert(plan.task_type === profile.task_type, '.agent/plan.json: task_type must match .agent/profile.json');
|
|
2050
|
+
assert(active.task_type === profile.task_type, '.agent/active-slice.json: task_type must match .agent/profile.json');
|
|
2051
|
+
assert(state.evaluation_profile === profile.evaluation_profile, '.agent/state.json: evaluation_profile must match .agent/profile.json');
|
|
2052
|
+
assert(plan.evaluation_profile === profile.evaluation_profile, '.agent/plan.json: evaluation_profile must match .agent/profile.json');
|
|
2053
|
+
assert(active.evaluation_profile === profile.evaluation_profile, '.agent/active-slice.json: evaluation_profile must match .agent/profile.json');
|
|
2054
|
+
|
|
2055
|
+
const requiresExactHandoff = ['selected', 'in_progress', 'committed', 'done'].includes(active.status);
|
|
2056
|
+
if (requiresExactHandoff) {
|
|
2057
|
+
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');
|
|
2058
|
+
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');
|
|
2059
|
+
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');
|
|
2060
|
+
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');
|
|
2061
|
+
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');
|
|
2062
|
+
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');
|
|
2063
|
+
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');
|
|
2064
|
+
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');
|
|
2065
|
+
} else {
|
|
2066
|
+
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');
|
|
2067
|
+
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');
|
|
2068
|
+
}
|
|
2069
|
+
NODE
|
|
2070
|
+
`;
|
|
1482
2071
|
}
|
|
1483
2072
|
|
|
1484
2073
|
async function ensureGitignore(root: string): Promise<boolean> {
|
|
@@ -1515,13 +2104,18 @@ type ScaffoldResult = {
|
|
|
1515
2104
|
missionAnchor: string;
|
|
1516
2105
|
};
|
|
1517
2106
|
|
|
1518
|
-
async function scaffoldCompletionFiles(
|
|
2107
|
+
async function scaffoldCompletionFiles(
|
|
2108
|
+
root: string,
|
|
2109
|
+
missionAnchor: string,
|
|
2110
|
+
options?: { analysis?: ContextProposalAnalysis; continuationReason?: string },
|
|
2111
|
+
): Promise<ScaffoldResult> {
|
|
1519
2112
|
const files = resolveFiles(root);
|
|
1520
2113
|
const created: string[] = [];
|
|
1521
2114
|
const updated: string[] = [];
|
|
1522
2115
|
await fsp.mkdir(files.agentDir, { recursive: true });
|
|
1523
2116
|
await fsp.mkdir(path.join(files.agentDir, "tmp"), { recursive: true });
|
|
1524
2117
|
const projectName = path.basename(root);
|
|
2118
|
+
const routing = finalizeContextProposalAnalysis(options?.analysis, [missionAnchor]);
|
|
1525
2119
|
const docsSurfaces = await detectDocsSurfaces(root);
|
|
1526
2120
|
const verifierCommand = await detectVerifierCommand(root);
|
|
1527
2121
|
const trackedFiles: Array<{ path: string; content: string; executable?: boolean }> = [
|
|
@@ -1529,13 +2123,16 @@ async function scaffoldCompletionFiles(root: string, missionAnchor: string): Pro
|
|
|
1529
2123
|
{ path: path.join(files.agentDir, "mission.md"), content: buildMission(projectName, missionAnchor) },
|
|
1530
2124
|
{
|
|
1531
2125
|
path: files.profilePath,
|
|
1532
|
-
content: `${JSON.stringify({
|
|
2126
|
+
content: `${JSON.stringify(buildProfileRecord({ projectName, requiredStopJudges: 3, docsSurfaces, taskType: routing.taskType, evaluationProfile: routing.evaluationProfile }), null, 2)}\n`,
|
|
1533
2127
|
},
|
|
1534
2128
|
{ path: path.join(files.agentDir, "verify_completion_stop.sh"), content: buildVerifyStopScript(verifierCommand), executable: true },
|
|
1535
2129
|
{ path: path.join(files.agentDir, "verify_completion_control_plane.sh"), content: buildVerifyControlPlaneScript(), executable: true },
|
|
1536
|
-
{
|
|
1537
|
-
|
|
1538
|
-
|
|
2130
|
+
{
|
|
2131
|
+
path: files.statePath,
|
|
2132
|
+
content: `${JSON.stringify(defaultState(missionAnchor, { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile, continuationReason: options?.continuationReason }), null, 2)}\n`,
|
|
2133
|
+
},
|
|
2134
|
+
{ path: files.planPath, content: `${JSON.stringify(defaultPlan(missionAnchor, { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile }), null, 2)}\n` },
|
|
2135
|
+
{ path: files.activePath, content: `${JSON.stringify(defaultActiveSlice(missionAnchor, { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile }), null, 2)}\n` },
|
|
1539
2136
|
{ path: files.sliceHistoryPath, content: "" },
|
|
1540
2137
|
{ path: files.stopHistoryPath, content: "" },
|
|
1541
2138
|
];
|
|
@@ -1573,8 +2170,12 @@ function activeSliceMatchesPlan(snapshot: CompletionStateSnapshot): "yes" | "no"
|
|
|
1573
2170
|
}
|
|
1574
2171
|
|
|
1575
2172
|
function handoffSnapshotState(active: JsonRecord | undefined): "present" | "missing_or_unclear" {
|
|
2173
|
+
const exactArrays = [
|
|
2174
|
+
asStringArray(active?.acceptance_criteria),
|
|
2175
|
+
asStringArray(active?.implementation_surfaces),
|
|
2176
|
+
asStringArray(active?.verification_commands),
|
|
2177
|
+
];
|
|
1576
2178
|
const required = [
|
|
1577
|
-
active?.acceptance_criteria,
|
|
1578
2179
|
active?.priority,
|
|
1579
2180
|
active?.why_now,
|
|
1580
2181
|
active?.blocked_on,
|
|
@@ -1585,15 +2186,24 @@ function handoffSnapshotState(active: JsonRecord | undefined): "present" | "miss
|
|
|
1585
2186
|
active?.release_blocker_count_before,
|
|
1586
2187
|
active?.high_value_gap_count_before,
|
|
1587
2188
|
];
|
|
1588
|
-
return required.every((value) => value !== undefined && value !== null)
|
|
2189
|
+
return exactArrays.every((items) => items.length > 0) && required.every((value) => value !== undefined && value !== null)
|
|
2190
|
+
? "present"
|
|
2191
|
+
: "missing_or_unclear";
|
|
1589
2192
|
}
|
|
1590
2193
|
|
|
1591
2194
|
function buildSystemReminder(snapshot: CompletionStateSnapshot, sliceHistory: JsonRecord[], stopHistory: JsonRecord[]): string {
|
|
1592
2195
|
const history = historyCounts(sliceHistory, stopHistory);
|
|
1593
|
-
|
|
2196
|
+
const implementationSurfaces = asStringArray(snapshot.active?.implementation_surfaces);
|
|
2197
|
+
const verificationCommands = asStringArray(snapshot.active?.verification_commands);
|
|
2198
|
+
const activePriority = asNumber(snapshot.active?.priority);
|
|
2199
|
+
const activeWhyNow = asString(snapshot.active?.why_now);
|
|
2200
|
+
const nextRole = asString(snapshot.state?.next_mandatory_role);
|
|
2201
|
+
const lines = [
|
|
1594
2202
|
"Completion workflow detected.",
|
|
1595
2203
|
"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
2204
|
`Mission anchor: ${asString(snapshot.state?.mission_anchor) ?? "(unknown)"}`,
|
|
2205
|
+
`Task type: ${currentTaskType(snapshot) ?? "(missing)"}`,
|
|
2206
|
+
`Evaluation profile: ${currentEvaluationProfile(snapshot) ?? "(missing)"}`,
|
|
1597
2207
|
`Current phase: ${asString(snapshot.state?.current_phase) ?? "unknown"}`,
|
|
1598
2208
|
`Continuation policy: ${asString(snapshot.state?.continuation_policy) ?? "unknown"}`,
|
|
1599
2209
|
`Continuation reason: ${asString(snapshot.state?.continuation_reason) ?? "(unknown)"}`,
|
|
@@ -1607,7 +2217,13 @@ function buildSystemReminder(snapshot: CompletionStateSnapshot, sliceHistory: Js
|
|
|
1607
2217
|
"Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.",
|
|
1608
2218
|
"If canonical state is stale, invalid, ambiguous, or missing, route to completion-regrounder.",
|
|
1609
2219
|
"When recovering from compaction, prefer a deterministic restart from canonical files over conversational inference.",
|
|
1610
|
-
]
|
|
2220
|
+
];
|
|
2221
|
+
if (activePriority !== undefined) lines.push(`Active slice priority: ${activePriority}`);
|
|
2222
|
+
if (activeWhyNow) lines.push(`Active slice why_now: ${activeWhyNow}`);
|
|
2223
|
+
if (implementationSurfaces.length > 0) lines.push(`Active implementation surfaces: ${implementationSurfaces.join(", ")}`);
|
|
2224
|
+
if (verificationCommands.length > 0) lines.push(`Active verification commands: ${verificationCommands.join(" | ")}`);
|
|
2225
|
+
if (isRubricEvaluationRole(nextRole)) lines.push(buildEvaluationRoleReminderText(snapshot, nextRole));
|
|
2226
|
+
return lines.join(" ");
|
|
1611
2227
|
}
|
|
1612
2228
|
|
|
1613
2229
|
function buildPostCompactionDriverInstructions(snapshot: CompletionStateSnapshot, marker: JsonRecord | undefined): string {
|
|
@@ -1616,11 +2232,19 @@ function buildPostCompactionDriverInstructions(snapshot: CompletionStateSnapshot
|
|
|
1616
2232
|
const nextAction = asString(snapshot.state?.next_mandatory_action) ?? "unknown";
|
|
1617
2233
|
const continuation = asString(snapshot.state?.continuation_policy) ?? "unknown";
|
|
1618
2234
|
const activeSliceId = asString(snapshot.active?.slice_id) ?? asString(snapshot.activeSlice?.slice_id) ?? "(none)";
|
|
1619
|
-
|
|
2235
|
+
const taskType = currentTaskType(snapshot) ?? "(missing)";
|
|
2236
|
+
const evaluationProfile = currentEvaluationProfile(snapshot) ?? "(missing)";
|
|
2237
|
+
const implementationSurfaces = asStringArray(snapshot.active?.implementation_surfaces);
|
|
2238
|
+
const verificationCommands = asStringArray(snapshot.active?.verification_commands);
|
|
2239
|
+
const activePriority = asNumber(snapshot.active?.priority);
|
|
2240
|
+
const activeWhyNow = asString(snapshot.active?.why_now);
|
|
2241
|
+
const lines = [
|
|
1620
2242
|
"POST-COMPACTION RECOVERY MODE is active.",
|
|
1621
2243
|
`Compaction marker time: ${markerAt}`,
|
|
1622
2244
|
"Treat the previous conversation as lossy continuity support only.",
|
|
1623
2245
|
"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.",
|
|
2246
|
+
`Canonical task_type is currently: ${taskType}`,
|
|
2247
|
+
`Canonical evaluation_profile is currently: ${evaluationProfile}`,
|
|
1624
2248
|
`Canonical next mandatory role is currently: ${nextRole}`,
|
|
1625
2249
|
`Canonical next mandatory action is currently: ${nextAction}`,
|
|
1626
2250
|
`Canonical continuation policy is currently: ${continuation}`,
|
|
@@ -1629,7 +2253,13 @@ function buildPostCompactionDriverInstructions(snapshot: CompletionStateSnapshot
|
|
|
1629
2253
|
"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
2254
|
"If continuation_policy == continue and canonical state is coherent, continue dispatching the mandatory role directly without asking the user whether to continue.",
|
|
1631
2255
|
"If you are about to implement after compaction, confirm the active slice snapshot still matches .agent/plan.json before doing any work.",
|
|
1632
|
-
]
|
|
2256
|
+
];
|
|
2257
|
+
if (activePriority !== undefined) lines.push(`Canonical active-slice priority is currently: ${activePriority}`);
|
|
2258
|
+
if (activeWhyNow) lines.push(`Canonical active-slice why_now is currently: ${activeWhyNow}`);
|
|
2259
|
+
if (implementationSurfaces.length > 0) lines.push(`Canonical implementation surfaces are currently: ${implementationSurfaces.join(", ")}`);
|
|
2260
|
+
if (verificationCommands.length > 0) lines.push(`Canonical verification commands are currently: ${verificationCommands.join(" | ")}`);
|
|
2261
|
+
if (isRubricEvaluationRole(nextRole)) lines.push(buildEvaluationRoleReminderText(snapshot, nextRole));
|
|
2262
|
+
return lines.join(" ");
|
|
1633
2263
|
}
|
|
1634
2264
|
|
|
1635
2265
|
function isStaleContextError(error: unknown): boolean {
|
|
@@ -1705,12 +2335,16 @@ function buildResumeCapsule(snapshot: CompletionStateSnapshot, sliceHistory: Jso
|
|
|
1705
2335
|
: asStringArray(snapshot.activeSlice?.blocked_on);
|
|
1706
2336
|
const lockedNotes = asStringArray(snapshot.active?.locked_notes);
|
|
1707
2337
|
const mustFixFindings = asStringArray(snapshot.active?.must_fix_findings);
|
|
2338
|
+
const implementationSurfaces = asStringArray(snapshot.active?.implementation_surfaces);
|
|
2339
|
+
const verificationCommands = asStringArray(snapshot.active?.verification_commands);
|
|
1708
2340
|
const remainingBefore = asStringArray(snapshot.active?.remaining_contract_ids_before);
|
|
1709
2341
|
const lines = [
|
|
1710
2342
|
"Authoritative completion resume capsule:",
|
|
1711
2343
|
"",
|
|
1712
2344
|
"<completion-state>",
|
|
1713
2345
|
`mission_anchor: ${asString(snapshot.state?.mission_anchor) ?? "(unknown)"}`,
|
|
2346
|
+
`task_type: ${currentTaskType(snapshot) ?? "(missing)"}`,
|
|
2347
|
+
`evaluation_profile: ${currentEvaluationProfile(snapshot) ?? "(missing)"}`,
|
|
1714
2348
|
`current_phase: ${asString(snapshot.state?.current_phase) ?? "unknown"}`,
|
|
1715
2349
|
`continuation_policy: ${asString(snapshot.state?.continuation_policy) ?? "unknown"}`,
|
|
1716
2350
|
`continuation_reason: ${asString(snapshot.state?.continuation_reason) ?? "(unknown)"}`,
|
|
@@ -1727,11 +2361,15 @@ function buildResumeCapsule(snapshot: CompletionStateSnapshot, sliceHistory: Jso
|
|
|
1727
2361
|
`- slice_id: ${asString(snapshot.active?.slice_id) ?? asString(snapshot.activeSlice?.slice_id) ?? "(none)"}`,
|
|
1728
2362
|
`- status: ${asString(snapshot.active?.status) ?? asString(snapshot.activeSlice?.status) ?? "unknown"}`,
|
|
1729
2363
|
`- goal: ${asString(snapshot.active?.goal) ?? asString(snapshot.activeSlice?.goal) ?? "(unknown)"}`,
|
|
2364
|
+
`- priority: ${asNumber(snapshot.active?.priority) ?? "(unknown)"}`,
|
|
2365
|
+
`- why_now: ${asString(snapshot.active?.why_now) ?? "(unknown)"}`,
|
|
1730
2366
|
`- contract_ids: ${contractIds.length > 0 ? contractIds.join(", ") : "(none)"}`,
|
|
1731
2367
|
];
|
|
1732
2368
|
if (blockedOn.length > 0) lines.push(`- blocked_on: ${blockedOn.join(", ")}`);
|
|
1733
2369
|
if (lockedNotes.length > 0) lines.push(`- locked_notes: ${lockedNotes.join(" | ")}`);
|
|
1734
2370
|
if (mustFixFindings.length > 0) lines.push(`- must_fix_findings: ${mustFixFindings.join(" | ")}`);
|
|
2371
|
+
if (implementationSurfaces.length > 0) lines.push(`- implementation_surfaces: ${implementationSurfaces.join(" | ")}`);
|
|
2372
|
+
if (verificationCommands.length > 0) lines.push(`- verification_commands: ${verificationCommands.join(" | ")}`);
|
|
1735
2373
|
lines.push(`- basis_commit: ${asString(snapshot.active?.basis_commit) ?? "(none)"}`);
|
|
1736
2374
|
lines.push(`- remaining_contract_ids_before: ${remainingBefore.length > 0 ? remainingBefore.join(", ") : "(none)"}`);
|
|
1737
2375
|
lines.push(`- release_blocker_count_before: ${asNumber(snapshot.active?.release_blocker_count_before) ?? "(unknown)"}`);
|
|
@@ -1743,7 +2381,7 @@ function buildResumeCapsule(snapshot: CompletionStateSnapshot, sliceHistory: Jso
|
|
|
1743
2381
|
"",
|
|
1744
2382
|
"Rules:",
|
|
1745
2383
|
"- 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.",
|
|
2384
|
+
"- 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
2385
|
"- 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
2386
|
"- Invoke completion-regrounder before continuing when requires_reground is true or unknown.",
|
|
1749
2387
|
"- Invoke completion-regrounder before continuing when next_mandatory_role or next_mandatory_action is unknown or ambiguous.",
|
|
@@ -2076,33 +2714,15 @@ async function refreshStatus(ctx: { cwd: string; hasUI: boolean; ui: any }) {
|
|
|
2076
2714
|
}
|
|
2077
2715
|
|
|
2078
2716
|
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;
|
|
2717
|
+
return roleReporting.parseReportFields(text);
|
|
2090
2718
|
}
|
|
2091
2719
|
|
|
2092
2720
|
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;
|
|
2721
|
+
return roleReporting.parseYesNo(value);
|
|
2098
2722
|
}
|
|
2099
2723
|
|
|
2100
2724
|
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;
|
|
2725
|
+
return roleReporting.parseFirstNumber(value);
|
|
2106
2726
|
}
|
|
2107
2727
|
|
|
2108
2728
|
async function gitHeadSha(cwd: string): Promise<string | undefined> {
|
|
@@ -2252,16 +2872,13 @@ function parseStructuredProgress(text: string): {
|
|
|
2252
2872
|
}
|
|
2253
2873
|
|
|
2254
2874
|
async function transcribeRoleOutput(role: CompletionRole, cwd: string, output: string, reportFields: Record<string, string>): Promise<TranscriptionResult> {
|
|
2255
|
-
const result: TranscriptionResult = { appended: [], skipped: [], errors: [] };
|
|
2256
2875
|
const snapshot = await loadCompletionSnapshot(cwd);
|
|
2257
2876
|
if (!snapshot) {
|
|
2258
|
-
|
|
2259
|
-
return result;
|
|
2877
|
+
return { appended: [], skipped: ["No canonical completion snapshot found."], errors: [] };
|
|
2260
2878
|
}
|
|
2261
2879
|
const headSha = await gitHeadSha(snapshot.files.root);
|
|
2262
2880
|
if (!headSha) {
|
|
2263
|
-
|
|
2264
|
-
return result;
|
|
2881
|
+
return { appended: [], skipped: [], errors: ["Could not resolve git HEAD for transcription."] };
|
|
2265
2882
|
}
|
|
2266
2883
|
|
|
2267
2884
|
const sliceId =
|
|
@@ -2269,117 +2886,14 @@ async function transcribeRoleOutput(role: CompletionRole, cwd: string, output: s
|
|
|
2269
2886
|
asString(snapshot.activeSlice?.slice_id) ??
|
|
2270
2887
|
asString(snapshot.state?.latest_completed_slice);
|
|
2271
2888
|
|
|
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;
|
|
2889
|
+
return await roleReporting.transcribeCanonicalRoleReport({
|
|
2890
|
+
role,
|
|
2891
|
+
output,
|
|
2892
|
+
reportFields,
|
|
2893
|
+
snapshotFiles: snapshot.files,
|
|
2894
|
+
headSha,
|
|
2895
|
+
sliceId,
|
|
2896
|
+
});
|
|
2383
2897
|
}
|
|
2384
2898
|
|
|
2385
2899
|
function isPathInside(root: string, candidatePath: string): boolean {
|
|
@@ -2483,18 +2997,24 @@ function lastAssistantText(messages: Array<{ role: string; content: Array<{ type
|
|
|
2483
2997
|
return "";
|
|
2484
2998
|
}
|
|
2485
2999
|
|
|
2486
|
-
function completionKickoff(
|
|
3000
|
+
function completionKickoff(
|
|
3001
|
+
goal: string,
|
|
3002
|
+
taskType: string,
|
|
3003
|
+
evaluationProfile: string,
|
|
3004
|
+
intent: "auto" | "continue" | "refocus" = "auto",
|
|
3005
|
+
missionAnchor?: string,
|
|
3006
|
+
): string {
|
|
2487
3007
|
const intentBlock =
|
|
2488
3008
|
intent === "continue" && missionAnchor
|
|
2489
3009
|
? `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
3010
|
: intent === "refocus" && missionAnchor
|
|
2491
3011
|
? `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
3012
|
: "";
|
|
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.`;
|
|
3013
|
+
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
3014
|
}
|
|
2495
3015
|
|
|
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.`;
|
|
3016
|
+
function completionResumePrompt(taskType: string, evaluationProfile: string): string {
|
|
3017
|
+
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
3018
|
}
|
|
2499
3019
|
|
|
2500
3020
|
export default function completionExtension(pi: ExtensionAPI) {
|
|
@@ -2529,6 +3049,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
2529
3049
|
}
|
|
2530
3050
|
const additions = [buildSystemReminder(loaded.snapshot, loaded.sliceHistory, loaded.stopHistory)];
|
|
2531
3051
|
if (marker) additions.push(buildPostCompactionDriverInstructions(loaded.snapshot, marker));
|
|
3052
|
+
maybeWriteTestSnapshot(completionTestSystemReminderPath(), additions.join("\n\n"));
|
|
2532
3053
|
const systemPrompt = getSystemPromptSafe(ctx);
|
|
2533
3054
|
if (!systemPrompt) return;
|
|
2534
3055
|
return {
|
|
@@ -2631,6 +3152,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
2631
3152
|
const runCwd = findCompletionRoot(cwd) ?? findRepoRoot(cwd) ?? cwd;
|
|
2632
3153
|
const rootKey = runCwd;
|
|
2633
3154
|
const agent = await loadAgentDefinition(runCwd, role);
|
|
3155
|
+
const loaded = await loadCompletionDataForReminder(runCwd);
|
|
2634
3156
|
type RunningDetails = {
|
|
2635
3157
|
role: string;
|
|
2636
3158
|
status: "running" | "ok" | "error";
|
|
@@ -2660,6 +3182,9 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
2660
3182
|
`- ${REFERENCE_PATH}`,
|
|
2661
3183
|
"Use canonical .agent/** state as the source of truth.",
|
|
2662
3184
|
];
|
|
3185
|
+
if (loaded && isRubricEvaluationRole(role)) {
|
|
3186
|
+
taskLines.push("", ...buildEvaluationRoleContextLines(loaded.snapshot, role));
|
|
3187
|
+
}
|
|
2663
3188
|
if (params.task?.trim()) {
|
|
2664
3189
|
taskLines.push("", "Supplemental task context:", params.task.trim());
|
|
2665
3190
|
}
|
|
@@ -2916,6 +3441,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
2916
3441
|
const workflowDone = isWorkflowDone(snapshot);
|
|
2917
3442
|
let kickoffIntent: "auto" | "continue" | "refocus" = "auto";
|
|
2918
3443
|
let kickoffMissionAnchor = snapshot ? currentMissionAnchor(snapshot) : undefined;
|
|
3444
|
+
let kickoffAnalysis: ContextProposalAnalysis | undefined;
|
|
2919
3445
|
|
|
2920
3446
|
if (!snapshot) {
|
|
2921
3447
|
const root = findRepoRoot(cwd) ?? cwd;
|
|
@@ -2941,6 +3467,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
2941
3467
|
}
|
|
2942
3468
|
goal = decision.goalText;
|
|
2943
3469
|
kickoffMissionAnchor = decision.missionAnchor;
|
|
3470
|
+
kickoffAnalysis = decision.analysis;
|
|
2944
3471
|
} else {
|
|
2945
3472
|
const proposal = await buildGoalAnchoredContextProposal(ctx, goal, projectName);
|
|
2946
3473
|
const decision = await confirmContextProposal(ctx, proposal, projectName, {
|
|
@@ -2955,8 +3482,17 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
2955
3482
|
}
|
|
2956
3483
|
goal = decision.goalText;
|
|
2957
3484
|
kickoffMissionAnchor = decision.missionAnchor;
|
|
3485
|
+
kickoffAnalysis = decision.analysis;
|
|
2958
3486
|
}
|
|
2959
|
-
const
|
|
3487
|
+
const startupRouting = finalizeContextProposalAnalysis(kickoffAnalysis, [goal ?? kickoffMissionAnchor ?? projectName]);
|
|
3488
|
+
const created = await scaffoldCompletionFiles(root, kickoffMissionAnchor ?? projectName, {
|
|
3489
|
+
analysis: startupRouting,
|
|
3490
|
+
continuationReason: buildContextProposalContinuationReason(
|
|
3491
|
+
"User started workflow via /cook:",
|
|
3492
|
+
goal ?? kickoffMissionAnchor ?? projectName,
|
|
3493
|
+
startupRouting,
|
|
3494
|
+
),
|
|
3495
|
+
});
|
|
2960
3496
|
emitCommandText(
|
|
2961
3497
|
ctx,
|
|
2962
3498
|
`Initialized completion control plane in ${created.root}${created.created.length > 0 ? ` (${created.created.length} files created)` : ""}`,
|
|
@@ -2992,17 +3528,22 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
2992
3528
|
goal = decision.goalText;
|
|
2993
3529
|
kickoffIntent = "refocus";
|
|
2994
3530
|
kickoffMissionAnchor = decision.missionAnchor;
|
|
2995
|
-
await refocusCompletionMission(snapshot, decision.missionAnchor, decision.goalText);
|
|
3531
|
+
await refocusCompletionMission(snapshot, decision.missionAnchor, decision.goalText, decision.analysis);
|
|
2996
3532
|
snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
|
|
2997
3533
|
emitCommandText(ctx, `Started a new completion workflow round from recent discussion: ${decision.missionAnchor}`, "info");
|
|
2998
3534
|
} else {
|
|
2999
3535
|
const mission = currentMissionAnchor(snapshot);
|
|
3000
3536
|
pi.setSessionName(`completion: ${mission.slice(0, 60)}`);
|
|
3537
|
+
const resumePrompt = completionResumePrompt(
|
|
3538
|
+
currentTaskType(snapshot) ?? "(missing)",
|
|
3539
|
+
currentEvaluationProfile(snapshot) ?? "(missing)",
|
|
3540
|
+
);
|
|
3541
|
+
maybeWriteTestSnapshot(completionTestDriverPromptPath(), `${resumePrompt}\n`);
|
|
3001
3542
|
if (shouldSkipDriverKickoffForTests()) {
|
|
3002
3543
|
emitCommandText(ctx, "Skipped completion workflow resume kickoff (test mode)", "info");
|
|
3003
3544
|
return;
|
|
3004
3545
|
}
|
|
3005
|
-
pi.sendUserMessage(
|
|
3546
|
+
pi.sendUserMessage(resumePrompt);
|
|
3006
3547
|
emitCommandText(ctx, "Queued completion workflow resume", "info");
|
|
3007
3548
|
return;
|
|
3008
3549
|
}
|
|
@@ -3025,7 +3566,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
3025
3566
|
goal = decision.goalText;
|
|
3026
3567
|
kickoffIntent = "refocus";
|
|
3027
3568
|
kickoffMissionAnchor = decision.missionAnchor;
|
|
3028
|
-
await refocusCompletionMission(snapshot, decision.missionAnchor, decision.goalText);
|
|
3569
|
+
await refocusCompletionMission(snapshot, decision.missionAnchor, decision.goalText, decision.analysis);
|
|
3029
3570
|
snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
|
|
3030
3571
|
emitCommandText(ctx, `Started a new completion workflow round from explicit goal: ${decision.missionAnchor}`, "info");
|
|
3031
3572
|
} else {
|
|
@@ -3050,7 +3591,12 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
3050
3591
|
return;
|
|
3051
3592
|
}
|
|
3052
3593
|
goal = proposalDecision.goalText;
|
|
3053
|
-
await refocusCompletionMission(
|
|
3594
|
+
await refocusCompletionMission(
|
|
3595
|
+
snapshot,
|
|
3596
|
+
proposalDecision.missionAnchor,
|
|
3597
|
+
proposalDecision.goalText,
|
|
3598
|
+
proposalDecision.analysis,
|
|
3599
|
+
);
|
|
3054
3600
|
snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
|
|
3055
3601
|
kickoffMissionAnchor = proposalDecision.missionAnchor;
|
|
3056
3602
|
emitCommandText(ctx, `Refocused completion mission to: ${proposalDecision.missionAnchor}`, "info");
|
|
@@ -3060,11 +3606,19 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
3060
3606
|
}
|
|
3061
3607
|
}
|
|
3062
3608
|
pi.setSessionName(`completion: ${kickoffMissionAnchor.slice(0, 60)}`);
|
|
3609
|
+
const kickoffPrompt = completionKickoff(
|
|
3610
|
+
goal,
|
|
3611
|
+
currentTaskType(snapshot) ?? "(missing)",
|
|
3612
|
+
currentEvaluationProfile(snapshot) ?? "(missing)",
|
|
3613
|
+
kickoffIntent,
|
|
3614
|
+
kickoffMissionAnchor,
|
|
3615
|
+
);
|
|
3616
|
+
maybeWriteTestSnapshot(completionTestDriverPromptPath(), `${kickoffPrompt}\n`);
|
|
3063
3617
|
if (shouldSkipDriverKickoffForTests()) {
|
|
3064
3618
|
emitCommandText(ctx, "Skipped completion workflow kickoff (test mode)", "info");
|
|
3065
3619
|
return;
|
|
3066
3620
|
}
|
|
3067
|
-
pi.sendUserMessage(
|
|
3621
|
+
pi.sendUserMessage(kickoffPrompt);
|
|
3068
3622
|
emitCommandText(ctx, "Queued completion workflow kickoff", "info");
|
|
3069
3623
|
},
|
|
3070
3624
|
});
|