@linimin/pi-letscook 0.1.30 → 0.1.32

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