@linimin/pi-letscook 0.1.29 → 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.
@@ -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): "mission" | "scope" | "constraints" | "acceptance" | undefined {
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 acceptance details before /cook writes canonical workflow state.",
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) return { missionAnchor: editedProposal.mission, goalText: editedProposal.goalText };
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: "mission" | "scope" | "constraints" | "acceptance" | undefined;
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 { missionAnchor: proposal.mission, goalText: proposal.goalText };
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(ctx, projectName, editedText, false);
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
- ? { missionAnchor: proposal.mission, goalText: proposal.goalText }
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
- ? { missionAnchor: proposal.mission, goalText: proposal.goalText }
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(snapshot: CompletionStateSnapshot, missionAnchor: string, rawGoal: string): Promise<void> {
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 defaultState(missionAnchor: string): JsonRecord {
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(missionAnchor: string): JsonRecord {
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(missionAnchor: string): JsonRecord {
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\nset -euo pipefail\n\nfor file in \\
1474
- .agent/README.md \\
1475
- .agent/mission.md \\
1476
- .agent/profile.json \\
1477
- .agent/verify_completion_stop.sh \\
1478
- .agent/verify_completion_control_plane.sh \\
1479
- .agent/state.json \\
1480
- .agent/plan.json \\
1481
- .agent/active-slice.json; do\n [[ -e "$file" ]] || { echo "missing required file: $file"; exit 1; }\ndone\n\nnode <<'NODE'\nconst fs = require('node:fs');\n\nconst readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));\nconst assert = (condition, message) => {\n if (!condition) {\n console.error(message);\n process.exit(1);\n }\n};\nconst isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);\nconst isString = (value) => typeof value === 'string';\nconst isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === 'string');\nconst hasOnlyKeys = (object, allowed, label) => {\n const unknown = Object.keys(object).filter((key) => !allowed.includes(key));\n assert(unknown.length === 0, label + ': unknown keys: ' + unknown.join(', '));\n};\nconst requireKeys = (object, required, label) => {\n for (const key of required) {\n assert(Object.prototype.hasOwnProperty.call(object, key), label + ': missing required field: ' + key);\n }\n};\n\nfor (const file of ['.agent/profile.json', '.agent/state.json', '.agent/plan.json', '.agent/active-slice.json']) {\n readJson(file);\n}\n\nconst profile = readJson('.agent/profile.json');\nconst state = readJson('.agent/state.json');\nconst plan = readJson('.agent/plan.json');\nconst active = readJson('.agent/active-slice.json');\n\nassert(isObject(profile), '.agent/profile.json must be an object');\nassert(isObject(state), '.agent/state.json must be an object');\nassert(isObject(plan), '.agent/plan.json must be an object');\nassert(isObject(active), '.agent/active-slice.json must be an object');\n\nconst requiredProfile = ['schema_version', 'protocol_id', 'project_name', 'required_stop_judges', 'priority_policy_id', 'docs_surfaces'];\nrequireKeys(profile, requiredProfile, '.agent/profile.json');\nhasOnlyKeys(profile, requiredProfile, '.agent/profile.json');\nassert(profile.protocol_id === 'completion', '.agent/profile.json: protocol_id must be completion');\nassert(Array.isArray(profile.docs_surfaces), '.agent/profile.json: docs_surfaces must be an array');\n\nconst requiredState = [\n 'schema_version','mission_anchor','current_phase','continuation_policy','continuation_reason','project_done',\n 'requires_reground','slices_since_last_reground','remaining_release_blockers','remaining_high_value_gaps',\n 'unsatisfied_contract_ids','release_blocker_ids','next_mandatory_action','next_mandatory_role',\n 'remaining_stop_judges','last_reground_at','last_auditor_verdict','contract_status','latest_completed_slice','latest_verified_slice'\n];\nconst continuationPolicies = ['continue', 'await_user_input', 'blocked', 'paused', 'done'];\nconst workflowRoles = ['completion-bootstrapper', 'completion-regrounder', 'completion-implementer', 'completion-reviewer', 'completion-auditor', 'completion-stop-judge', null];\nconst workflowPhases = ['reground', 'implement', 'post_commit_review', 'post_commit_audit', 'post_commit_reconcile', 'stop_wave', 'awaiting_user', 'blocked', 'done'];\nrequireKeys(state, requiredState, '.agent/state.json');\nhasOnlyKeys(state, requiredState, '.agent/state.json');\nassert(continuationPolicies.includes(state.continuation_policy), '.agent/state.json: invalid continuation_policy');\nassert(workflowRoles.includes(state.next_mandatory_role), '.agent/state.json: invalid next_mandatory_role');\nassert(workflowPhases.includes(state.current_phase), '.agent/state.json: invalid current_phase');\nassert(isStringArray(state.unsatisfied_contract_ids), '.agent/state.json: unsatisfied_contract_ids must be an array of strings');\nassert(isStringArray(state.release_blocker_ids), '.agent/state.json: release_blocker_ids must be an array of strings');\n\nconst requiredPlan = ['schema_version', 'mission_anchor', 'last_reground_at', 'plan_basis', 'candidate_slices'];\nconst requiredSlice = ['slice_id', 'goal', 'acceptance_criteria', 'contract_ids', 'priority', 'status', 'why_now', 'blocked_on', 'evidence'];\nconst sliceStatuses = ['planned', 'selected', 'in_progress', 'blocked', 'done', 'cancelled'];\nrequireKeys(plan, requiredPlan, '.agent/plan.json');\nhasOnlyKeys(plan, requiredPlan, '.agent/plan.json');\nassert(Array.isArray(plan.candidate_slices), '.agent/plan.json: candidate_slices must be an array');\nfor (const [index, slice] of plan.candidate_slices.entries()) {\n const label = '.agent/plan.json candidate_slices[' + index + ']';\n assert(isObject(slice), label + ' must be an object');\n requireKeys(slice, requiredSlice, label);\n hasOnlyKeys(slice, requiredSlice, label);\n assert(isString(slice.slice_id) && slice.slice_id.length > 0, label + ': slice_id must be a non-empty string');\n assert(isString(slice.goal) && slice.goal.length > 0, label + ': goal must be a non-empty string');\n 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');\n assert(isStringArray(slice.contract_ids), label + ': contract_ids must be an array of strings');\n assert(typeof slice.priority === 'number' && Number.isFinite(slice.priority), label + ': priority must be a finite number');\n assert(sliceStatuses.includes(slice.status), label + ': invalid status');\n assert(isString(slice.why_now) && slice.why_now.length > 0, label + ': why_now must be a non-empty string');\n assert(isStringArray(slice.blocked_on), label + ': blocked_on must be an array of strings');\n assert(isStringArray(slice.evidence), label + ': evidence must be an array of strings');\n}\n\nconst requiredActiveBase = ['schema_version', 'mission_anchor', 'status', 'slice_id', 'goal', 'contract_ids', 'acceptance_criteria', 'blocked_on', 'locked_notes', 'must_fix_findings', 'basis_commit', 'remaining_contract_ids_before', 'release_blocker_count_before', 'high_value_gap_count_before'];\nconst allowedActive = [...requiredActiveBase, 'priority', 'why_now'];\nconst activeStatuses = ['idle', 'selected', 'in_progress', 'committed', 'done'];\nrequireKeys(active, requiredActiveBase, '.agent/active-slice.json');\nhasOnlyKeys(active, allowedActive, '.agent/active-slice.json');\nassert(activeStatuses.includes(active.status), '.agent/active-slice.json: invalid status');\nassert(isStringArray(active.contract_ids), '.agent/active-slice.json: contract_ids must be an array of strings');\nassert(Array.isArray(active.acceptance_criteria), '.agent/active-slice.json: acceptance_criteria must be an array');\nassert(isStringArray(active.blocked_on), '.agent/active-slice.json: blocked_on must be an array of strings');\nassert(isStringArray(active.locked_notes), '.agent/active-slice.json: locked_notes must be an array of strings');\nassert(isStringArray(active.must_fix_findings), '.agent/active-slice.json: must_fix_findings must be an array of strings');\nassert(isStringArray(active.remaining_contract_ids_before), '.agent/active-slice.json: remaining_contract_ids_before must be an array of strings');\n\nconst requiresExactHandoff = ['selected', 'in_progress', 'committed', 'done'].includes(active.status);\nif (requiresExactHandoff) {\n assert(Array.isArray(active.acceptance_criteria) && active.acceptance_criteria.length > 0 && active.acceptance_criteria.every((item) => typeof item === 'string' && item.length > 0), '.agent/active-slice.json: acceptance_criteria must be a non-empty array of strings when status carries an exact handoff');\n 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');\n 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');\n 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');\n 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');\n 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');\n} else {\n 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');\n 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');\n}\nNODE\n`;
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(root: string, missionAnchor: string): Promise<ScaffoldResult> {
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({ schema_version: 1, protocol_id: PROTOCOL_ID, project_name: projectName, required_stop_judges: 3, priority_policy_id: "completion-default", docs_surfaces: docsSurfaces }, null, 2)}\n`,
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
- { path: files.statePath, content: `${JSON.stringify(defaultState(missionAnchor), null, 2)}\n` },
1537
- { path: files.planPath, content: `${JSON.stringify(defaultPlan(missionAnchor), null, 2)}\n` },
1538
- { path: files.activePath, content: `${JSON.stringify(defaultActiveSlice(missionAnchor), null, 2)}\n` },
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) ? "present" : "missing_or_unclear";
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
- return [
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
- ].join(" ");
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
- return [
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
- ].join(" ");
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
- const fields: Record<string, string> = {};
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
- if (!value) return undefined;
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
- if (!value) return undefined;
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
- result.skipped.push("No canonical completion snapshot found.");
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
- result.errors.push("Could not resolve git HEAD for transcription.");
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
- if (role === "completion-reviewer" || role === "completion-auditor") {
2273
- if (!sliceId) {
2274
- result.errors.push(`Missing slice_id for ${role} transcription.`);
2275
- return result;
2276
- }
2277
- const type = role === "completion-reviewer" ? "reviewed" : "audited";
2278
- const history = await readJsonl(snapshot.files.sliceHistoryPath);
2279
- const duplicate = history.some((entry) => {
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(goal: string, intent: "auto" | "continue" | "refocus" = "auto", missionAnchor?: string): string {
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 created = await scaffoldCompletionFiles(root, kickoffMissionAnchor ?? projectName);
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(completionResumePrompt());
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(snapshot, proposalDecision.missionAnchor, proposalDecision.goalText);
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(completionKickoff(goal, kickoffIntent, kickoffMissionAnchor));
3621
+ pi.sendUserMessage(kickoffPrompt);
3068
3622
  emitCommandText(ctx, "Queued completion workflow kickoff", "info");
3069
3623
  },
3070
3624
  });