@mediadatafusion/pi-workflow-suite 0.0.20 → 0.0.21

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.
@@ -17,7 +17,7 @@ import { WORKFLOW_SHORTCUTS, workflowEntryShortcutLabel as workflowRegistryEntry
17
17
  import { cleanupOrphanProcesses, clearSubagentResultCache, runWorkflowSubagents, workflowSubagentResultOutput, type WorkflowSubagentResult, type WorkflowSubagentTask } from "./subagent/runner.js";
18
18
  import { parseScope, parseBool, parsePlanningDepth, parseClarificationMode, parseStandardTodoTriggerMode, parseStandardClarificationMode, parseStandardModelRole, parseWorkflowAgentScope, parseClarificationTiming, parseSubagentPolicy, parseSubagentPlanningPolicy, parseEditConcurrencyMode, parsePlanningOrchestrationPolicy, parseCompactionMode, parseWorkflowCompactionCheckMode, parseMissionAutonomy, parseValidationRetryMode, parsePositiveInt, updatedMessage, parseClarifyingQuestions, parseShorthandAnswers, formatAnswersForPlanner, planValidationStatusForVerdict } from "./workflow-parsers.js";
19
19
  import { classifyValidationFailure, normalizeValidationVerdict, validationReportHasRepairableIssue } from "./workflow-validation-classifier.js";
20
- import { type SubagentPhase, type SubagentPolicyValue, subagentPhaseSettingKeys, phasePolicy, phaseAutoUseAllowed, phaseParallelAllowed, workerCount, workerTargetForPolicy, activeWorkerTargetLabel, planningSubagentsAllowed, executionSubagentsAllowed, reviewSubagentsAllowed, validationSubagentsAllowed, repairPolicySource, forcedSubagentUnavailableReason, forcedSubagentMessage, fileWriteModeLabel, hasRequiredSubagentPreflight, requiredSubagentPreflightSection, forcedSubagentPolicySatisfiedGuidance, subagentSuitableForForcedPhase, subagentToolProfileLabel, subagentToolsAllowMutation } from "./workflow-subagent-policy.js";
20
+ import { type SubagentPhase, type SubagentPolicyValue, type SubagentPolicyDecision, subagentPhaseSettingKeys, phasePolicy, phaseAutoUseAllowed, phaseParallelAllowed, workerCount, workerTargetForPolicy, activeWorkerTargetLabel, planningSubagentsAllowed, executionSubagentsAllowed, reviewSubagentsAllowed, validationSubagentsAllowed, repairPolicySource, forcedSubagentUnavailableReason, forcedSubagentMessage, fileWriteModeLabel, hasRequiredSubagentPreflight, requiredSubagentPreflightSection, forcedSubagentPolicySatisfiedGuidance, subagentSuitableForForcedPhase, subagentToolProfileLabel, subagentToolsAllowMutation, subagentPolicyRequiresRequiredEvidence, subagentPolicyNeedsInternalDecision, formatSubagentPolicyDecision, forcedSubagentActionDecision, advisorySubagentPolicyDecision } from "./workflow-subagent-policy.js";
21
21
  import { renderWorkflowSettingsCapabilityMatrix, workflowSettingsCapabilitiesByStatus } from "./workflow-settings-capabilities.js";
22
22
  import { Box, Markdown, Spacer, Text, hyperlink, type Component } from "@earendil-works/pi-tui";
23
23
 
@@ -908,6 +908,17 @@ type WorkflowQueuedTurnOptions = {
908
908
  onFinalFailure?: (failure: WorkflowQueuedTurnFailure) => void;
909
909
  };
910
910
 
911
+ class WorkflowToolSurfaceAbort extends Error {
912
+ constructor(message: string) {
913
+ super(message);
914
+ this.name = "WorkflowToolSurfaceAbort";
915
+ }
916
+ }
917
+
918
+ function workflowToolSurfaceAbort(error: unknown): boolean {
919
+ return error instanceof WorkflowToolSurfaceAbort;
920
+ }
921
+
911
922
  const WORKFLOW_IDLE_RETRY_DELAYS_MS = [50, 100, 250, 500, 1000, 1500, 2000, 3000, 5000] as const;
912
923
 
913
924
  function queueGuardedAgentTurn(pi: ExtensionAPI, content: string, customType: string, attempt = 0, connectionAttempt = 0, idleAttempt = 0, options: WorkflowQueuedTurnOptions = {}): void {
@@ -929,6 +940,11 @@ function queueGuardedAgentTurn(pi: ExtensionAPI, content: string, customType: st
929
940
  pi.sendMessage({ customType, content, display: false }, { triggerTurn: true, deliverAs: "followUp" });
930
941
  workflowScheduledAgentTurns = Math.max(0, workflowScheduledAgentTurns - 1);
931
942
  } catch (error) {
943
+ if (workflowToolSurfaceAbort(error)) {
944
+ workflowScheduledAgentTurns = Math.max(0, workflowScheduledAgentTurns - 1);
945
+ recordWorkflowInternalEvent(undefined, `Workflow agent turn blocked by tool-surface verification: ${customType}: ${workflowTurnSendErrorMessage(error)}`);
946
+ return;
947
+ }
932
948
  if (workflowTurnSendErrorIsBusy(error) && attempt < WORKFLOW_AGENT_TURN_RETRY_DELAYS_MS.length - 1) {
933
949
  recordWorkflowInternalEvent(undefined, `Workflow agent turn delayed because Pi is still processing: ${customType}`);
934
950
  queueGuardedAgentTurn(pi, content, customType, attempt + 1, connectionAttempt, idleAttempt, options);
@@ -962,6 +978,10 @@ function sendAgentTurnNowOrQueue(pi: ExtensionAPI, content: string, customType:
962
978
  workflowScheduledAgentTurns = Math.max(0, workflowScheduledAgentTurns - 1);
963
979
  } catch (error) {
964
980
  workflowScheduledAgentTurns = Math.max(0, workflowScheduledAgentTurns - 1);
981
+ if (workflowToolSurfaceAbort(error)) {
982
+ recordWorkflowInternalEvent(undefined, `Workflow agent turn immediate send blocked by tool-surface verification: ${customType}: ${workflowTurnSendErrorMessage(error)}`);
983
+ return;
984
+ }
965
985
  queueAgentTurn(pi, content, customType, options);
966
986
  recordWorkflowInternalEvent(undefined, `Workflow agent turn immediate send fell back to queued delivery: ${customType}: ${workflowTurnSendErrorMessage(error)}`);
967
987
  }
@@ -992,7 +1012,7 @@ function workflowDeferredPhaseTimeoutMs(ctx: ExtensionContext, phase?: WorkflowP
992
1012
  try {
993
1013
  const settings = loadWorkflowSettings(ctx.cwd);
994
1014
  const policy = phasePolicy(settings, phase);
995
- if (policy !== "forced") return 30_000;
1015
+ if (!subagentPolicyRequiresRequiredEvidence(policy)) return 30_000;
996
1016
  const limits = settings.subagents as typeof settings.subagents & { subagentTimeoutMinutes?: number };
997
1017
  const timeoutMinutes = Math.max(1, Math.min(240, Number(limits.subagentTimeoutMinutes ?? 20)));
998
1018
  return timeoutMinutes * 60_000 + 60_000;
@@ -1099,6 +1119,16 @@ function executionToolsFor(settings: ReturnType<typeof loadWorkflowSettings>): s
1099
1119
  return executionSubagentsAllowed(settings) ? Array.from(new Set([...tools, "subagent"])) : tools;
1100
1120
  }
1101
1121
 
1122
+ function missionExecutionToolsFor(settings: ReturnType<typeof loadWorkflowSettings>): string[] {
1123
+ const tools = withRuntimeWebTools([...BASE_EXECUTE_TOOLS, MISSION_MILESTONE_RESULT_TOOL]);
1124
+ return executionSubagentsAllowed(settings) ? Array.from(new Set([...tools, "subagent"])) : tools;
1125
+ }
1126
+
1127
+ function missionRepairToolsFor(settings: ReturnType<typeof loadWorkflowSettings>): string[] {
1128
+ const tools = withRuntimeWebTools([...BASE_EXECUTE_TOOLS, WORKFLOW_REPAIR_RESULT_TOOL]);
1129
+ return executionSubagentsAllowed(settings) ? Array.from(new Set([...tools, "subagent"])) : tools;
1130
+ }
1131
+
1102
1132
  function requiredPlanExecutionTools(settings: ReturnType<typeof loadWorkflowSettings>): string[] {
1103
1133
  const tools = [...PLAN_TOOLS, "edit", "write", "bash", WORKFLOW_PROGRESS_TOOL, WORKFLOW_EXECUTION_RESULT_TOOL];
1104
1134
  if (executionSubagentsAllowed(settings)) tools.push("subagent");
@@ -1778,22 +1808,27 @@ function planPrompt(task: string, priorPlan?: string, feedback?: string, setting
1778
1808
  const subagentsBeforeClarification = useSubagentsBeforeClarification(settings, "plan");
1779
1809
  const planningOrchestrationPolicy = (subagents as typeof subagents & { planningOrchestrationPolicy?: string }).planningOrchestrationPolicy ?? "orchestrator_first";
1780
1810
  const preflightSatisfied = hasRequiredSubagentPreflight(options.preflightBlock);
1811
+ const forcedPlanningEvidenceRequired = subagentPolicyRequiresRequiredEvidence(subagentPolicy) && !preflightSatisfied;
1781
1812
  const orchestratorGuidance = preflightSatisfied
1782
1813
  ? "Planning orchestration required by forced policy has already been handled by Workflow Suite preflight. Do not call workflow-orchestrator again unless there is genuinely new targeted orchestration work beyond the preflight findings."
1814
+ : forcedPlanningEvidenceRequired && planningNeedsOrchestrator(settings, "plan")
1815
+ ? planningOrchestrationPolicy === "forced_orchestrated"
1816
+ ? "ORCHESTRATION REQUIRED: include workflow-orchestrator as one task inside the required forced Planning parallel subagent call. Do not call workflow-orchestrator separately before the worker batch."
1817
+ : "Planning orchestration is available for this turn. If it is useful, include workflow-orchestrator as one task inside the required forced Planning parallel subagent call; do not call workflow-orchestrator separately before the worker batch."
1783
1818
  : planningNeedsOrchestrator(settings, "plan")
1784
1819
  ? "ORCHESTRATION REQUIRED: call workflow-orchestrator before launching parallel planning/research workers or producing the final plan. The orchestrator must scope worker tasks, identify capability gaps, and decide whether live docs/API access is available before workers run."
1785
1820
  : "Orchestrator is optional for this planning turn. Use workflow-orchestrator if the task is broad, multi-pass, or needs coordinated workers.";
1786
1821
  const subagentGuidance = !planningSubagentsAllowed(settings)
1787
1822
  ? "Sub-agent use is disabled by settings. Do not call subagent."
1788
- : preflightSatisfied && subagentPolicy === "forced"
1823
+ : preflightSatisfied && subagentPolicyRequiresRequiredEvidence(subagentPolicy)
1789
1824
  ? forcedSubagentPolicySatisfiedGuidance("planning")
1790
1825
  : subagentPolicy === "forced"
1791
1826
  ? `FORCED SUB-AGENT POLICY: before producing the final plan, you MUST call the subagent tool with at least ${Math.max(1, subagents.minPlanningWorkersForMaximum ?? 2)} planning/research worker(s). Use read-only agents such as implementation-planning, codebase-research, quality-validation, workflow-orchestrator, or general-worker. If subagent execution fails, stop and report the exact blocker. Do not use or recommend parallel editing.`
1792
1827
  : subagentPolicy === "maximum"
1793
- ? `For non-trivial codebase work, use multiple read-only sub-agents before finalizing the plan. Strongly target at least ${subagents.minPlanningWorkersForMaximum ?? 2} workers, such as implementation-planning plus codebase-research or quality-validation. Skip only for truly trivial work or unavailable sub-agent execution, and explain the exact reason. Do not use or recommend parallel editing.`
1828
+ ? `For non-trivial codebase work, use multiple read-only sub-agents before finalizing the plan. Strongly target at least ${subagents.minPlanningWorkersForMaximum ?? 2} workers, such as implementation-planning plus codebase-research or quality-validation. Internally decide delegate or skip; skip only for truly trivial work or unavailable sub-agent execution, and give only the concise skip reason in Sub-Agent Usage Summary. Do not print internal policy deliberation. Do not use or recommend parallel editing.`
1794
1829
  : subagentPolicy === "deep"
1795
- ? `For non-trivial codebase work, use at least ${subagents.minPlanningWorkersForDeep ?? 1} read-only planning sub-agent before finalizing the plan. Prefer implementation-planning or codebase-research for safe read-only plan refinement. Explain any skip. Do not use or recommend parallel editing.`
1796
- : "Sub-agents are strongly encouraged for speed and quality. Use them for file discovery, project-rule audit, ambiguity reduction, risk checks, or validation planning; skip only for trivial one-file/doc tasks and explain why.";
1830
+ ? `For non-trivial codebase work, use at least ${subagents.minPlanningWorkersForDeep ?? 1} read-only planning sub-agent before finalizing the plan. Prefer implementation-planning or codebase-research for safe read-only plan refinement. Internally decide delegate or skip; give only the concise skip reason in Sub-Agent Usage Summary. Do not print internal policy deliberation. Do not use or recommend parallel editing.`
1831
+ : "Sub-agents are strongly encouraged for speed and quality. Internally decide delegate or skip. Use them for file discovery, project-rule audit, ambiguity reduction, risk checks, or validation planning; skip only for trivial one-file/doc tasks or no useful parallel work, and give only the concise skip reason in Sub-Agent Usage Summary. Do not print internal policy deliberation.";
1797
1832
  const feedbackKind = options.feedbackKind ?? (feedback ? "revision" : undefined);
1798
1833
  const feedbackBlock = feedbackKind === "clarification" && feedback
1799
1834
  ? `Clarification answers from user:\n${feedback}\n\nCLARIFICATION HAS BEEN RESOLVED. Produce the final approval-ready plan now. Do not ask more clarification questions. Do not execute. Do not call tools, inspect files, generate diagrams, or create artifacts after this point. If more facts are needed, list files to inspect in the plan instead of exhaustively investigating them now.\n\n`
@@ -1941,7 +1976,7 @@ Sub-agent planning policy:
1941
1976
  - When useSubagentsBeforeClarification=true and planning depth/policy is deep, maximum, or forced, use read-only codebase-research, implementation-planning, quality-validation, or general-worker before asking clarification if that analysis would make the questions more specific.
1942
1977
  - In Sub-Agent Usage Summary, list each sub-agent used and what it checked; if none ran, give the exact trivial/unavailable reason.
1943
1978
  - If sub-agents are not used when subagents.planningPolicy is auto, deep, or maximum, explain why in Sub-Agent Usage Summary.
1944
- - ${preflightSatisfied && subagentPolicy === "forced" ? "Forced planning policy was satisfied by Workflow Suite preflight; do not rerun required workers solely for policy compliance." : "If subagents.planningPolicy is forced, you must use the required sub-agents before the final plan or stop with: Sub-agent policy is forced, but sub-agent execution is unavailable because <reason>."}`;
1979
+ - ${preflightSatisfied && subagentPolicyRequiresRequiredEvidence(subagentPolicy) ? "Forced planning policy was satisfied by Workflow Suite preflight or an internal trivial-work exemption; do not rerun required workers solely for policy compliance." : "If subagents.planningPolicy is forced, you must use the required sub-agents before the final plan unless Workflow Suite supplied an internal trivial-work exemption, or stop with: Sub-agent policy is forced, but sub-agent execution is unavailable because <reason>."}`;
1945
1980
  }
1946
1981
 
1947
1982
  // SubagentPhase and SubagentPolicyValue imported from workflow-subagent-policy.ts
@@ -2019,22 +2054,39 @@ function repairArtifactDispositionOutput(): string {
2019
2054
  List movedFiles, preservedFiles, deletedFiles, rootArtifacts, possiblyUserOwnedFiles, and needsUserApproval. State "none" for empty categories. Set needsUserApproval only for a concrete disposition/action that should block automatic revalidation. Do not set it for advisory-only follow-up, credential rotation recommendations, preserved ambiguous files, manual QA still needed, or pre-existing debt. Deletion is acceptable only when explicitly approved or clearly current-task-generated, unrecoverable, and non-user-owned; otherwise preserve or request approval.`;
2020
2055
  }
2021
2056
 
2057
+ function forcedSubagentCallShape(phase: SubagentPhase, required: number): string {
2058
+ const workflowPhase = phase.toLowerCase();
2059
+ const preferred = phase === "Planning"
2060
+ ? ["implementation-planning", "codebase-research", "general-worker", "quality-validation"]
2061
+ : phase === "Execution"
2062
+ ? ["general-worker", "implementation-planning", "codebase-research", "quality-validation"]
2063
+ : phase === "Repair"
2064
+ ? ["general-worker", "implementation-planning", "quality-validation", "codebase-research"]
2065
+ : phase === "Review"
2066
+ ? ["quality-validation", "implementation-planning", "general-worker", "codebase-research"]
2067
+ : ["quality-validation", "general-worker", "codebase-research", "implementation-planning"];
2068
+ const agents = preferred.slice(0, Math.max(1, required));
2069
+ const tasks = agents.map((agent) => `{ agent: "${agent}", task: "<focused ${workflowPhase} support task>" }`).join(", ");
2070
+ return `Use one visible parallel call with this shape: subagent({ workflowPhase: "${workflowPhase}", tasks: [${tasks}] }). Provide at least ${Math.max(1, required)} worker task entr${Math.max(1, required) === 1 ? "y" : "ies"} in that single parallel tasks array. Worker count means task entries, not unique agent names. The agents shown are examples only; choose phase-suitable workers by task fit. Duplicate suitable agent roles are valid when useful, and distinct role names are not required. Do not add irrelevant filler roles. Do not use subagent chain for forced multi-worker evidence; chain is sequential and will be blocked. Every task must be separate, focused, useful, and phase-suitable.`;
2071
+ }
2072
+
2022
2073
  function phasePromptPolicyBlock(settings: ReturnType<typeof loadWorkflowSettings>, phase: SubagentPhase, label: string = phase, preflightBlock?: string): string {
2023
2074
  const policy = phasePolicy(settings, phase);
2024
2075
  const workers = workerCount(settings, phase);
2076
+ const required = Math.max(1, workers.maximum);
2025
2077
  const phaseName = phase === "Repair" ? "repair-mode sub-agent workers" : `${phase.toLowerCase()} sub-agent workers`;
2026
2078
  const preflightSatisfied = hasRequiredSubagentPreflight(preflightBlock);
2027
2079
  const guidance = !phaseAutoUseAllowed(settings, phase) || policy === "off"
2028
2080
  ? `${label} sub-agent use is disabled by settings. Do not call subagent.`
2029
- : preflightSatisfied && policy === "forced"
2081
+ : preflightSatisfied && subagentPolicyRequiresRequiredEvidence(policy)
2030
2082
  ? forcedSubagentPolicySatisfiedGuidance(label)
2031
2083
  : policy === "forced"
2032
- ? `FORCED SUB-AGENT POLICY: you MUST call the subagent tool with at least ${Math.max(1, workers.maximum)} ${phaseName}, or stop with: Sub-agent policy is forced, but sub-agent execution is unavailable because <reason>. Do not continue this phase or write files until the required workers have reported.`
2084
+ ? `FORCED SUB-AGENT POLICY: you MUST call the subagent tool with at least ${required} ${phaseName}, or stop with: Sub-agent policy is forced, but sub-agent execution is unavailable because <reason>. ${forcedSubagentCallShape(phase, required)} Do not continue this phase or write files until the required workers have reported.`
2033
2085
  : policy === "maximum"
2034
- ? `Strongly target ${Math.max(1, workers.maximum)} ${phaseName}; skip only when the work is truly trivial or sub-agents are unavailable, and give the exact skip reason.`
2086
+ ? `Strongly target ${Math.max(1, workers.maximum)} ${phaseName}; internally decide delegate or skip, skip only when the work is truly trivial or sub-agents are unavailable, and give only the concise skip reason. Do not print internal policy deliberation.`
2035
2087
  : policy === "deep"
2036
- ? `Use ${Math.max(1, workers.deep)} ${phaseName} for non-trivial work; give a concrete skip reason if no worker is useful or available.`
2037
- : `${label} sub-agent use is strongly encouraged; use workers for speed, file inspection, risk discovery, validation prep, or evidence-heavy work and give a concrete skip reason if none run.`;
2088
+ ? `Use ${Math.max(1, workers.deep)} ${phaseName} for non-trivial work; internally decide delegate or skip and give a concrete skip reason if no worker is useful or available. Do not print internal policy deliberation.`
2089
+ : `${label} sub-agent use is strongly encouraged; internally decide delegate or skip, strongly consider up to ${Math.max(1, workers.deep)} worker${Math.max(1, workers.deep) === 1 ? "" : "s"} for non-trivial work, use workers for speed, file inspection, risk discovery, validation prep, or evidence-heavy work, and give a concrete skip reason only when none run. Do not print internal policy deliberation.`;
2038
2090
  return `Sub-agent policy for ${label}:
2039
2091
  - policy: ${policy}
2040
2092
  - workers: ${activeWorkerTargetLabel(policy, workers)}
@@ -2121,15 +2173,15 @@ function executePrompt(state: WorkflowState, settings = loadWorkflowSettings(),
2121
2173
  const preflightSatisfied = hasRequiredSubagentPreflight(preflightBlock);
2122
2174
  const subagentGuidance = !executionSubagentsAllowed(settings)
2123
2175
  ? "Execution sub-agents are disabled by settings. Do not call subagent."
2124
- : preflightSatisfied && policy === "forced"
2176
+ : preflightSatisfied && subagentPolicyRequiresRequiredEvidence(policy)
2125
2177
  ? forcedSubagentPolicySatisfiedGuidance("execution")
2126
2178
  : policy === "forced"
2127
- ? `FORCED SUB-AGENT POLICY: first call workflow_progress({ step: N, status: "active" }) for the current approved Plan step. Then call the subagent tool with at least ${Math.max(1, workers.maximum)} execution/preparation worker(s) before any other executor work, file write, bash command, or final execution summary. Use them in parallel where safe for file inspection, implementation strategy, scoped implementation help, patch planning, regression search, and validation preparation. If subagent execution fails, stop and report the exact blocker. Do not apply simultaneous conflicting edits.`
2179
+ ? `FORCED SUB-AGENT POLICY: first call workflow_progress({ step: N, status: "active" }) for the current approved Plan step. Then call the subagent tool with at least ${Math.max(1, workers.maximum)} execution/preparation worker(s) before any other executor work, file write, bash command, or final execution summary. ${forcedSubagentCallShape("Execution", Math.max(1, workers.maximum))} Use them in parallel where safe for file inspection, implementation strategy, scoped implementation help, patch planning, regression search, and validation preparation. If subagent execution fails, stop and report the exact blocker. Do not apply simultaneous conflicting edits.`
2128
2180
  : policy === "maximum"
2129
- ? `For non-trivial implementation, use execution sub-agents before editing for analysis, implementation preparation, scoped implementation help, patch planning, regression search, and validation preparation. Strongly target at least ${workers.maximum} execution workers; skip only for trivial work or unavailable sub-agent execution and explain why. Do not apply simultaneous conflicting edits.`
2181
+ ? `For non-trivial implementation, use execution sub-agents before editing for analysis, implementation preparation, scoped implementation help, patch planning, regression search, and validation preparation. Strongly target at least ${workers.maximum} execution workers; internally decide delegate or skip, skip only for trivial work or unavailable sub-agent execution, and give only the concise skip reason. Do not print internal policy deliberation. Do not apply simultaneous conflicting edits.`
2130
2182
  : policy === "deep"
2131
- ? `For non-trivial implementation, use at least ${workers.deep} execution sub-agent before editing for analysis, implementation preparation, scoped implementation help, patch planning, or validation preparation. Explain any skip. Do not apply simultaneous conflicting edits.`
2132
- : "Execution sub-agents are strongly encouraged for speed and quality. Use them for file inspection, implementation prep, scoped implementation help, risk discovery, patch planning, or validation prep; skip only for trivial changes and explain why.";
2183
+ ? `For non-trivial implementation, use at least ${workers.deep} execution sub-agent before editing for analysis, implementation preparation, scoped implementation help, patch planning, or validation preparation. Internally decide delegate or skip and give only the concise skip reason. Do not print internal policy deliberation. Do not apply simultaneous conflicting edits.`
2184
+ : `Execution sub-agents are strongly encouraged for speed and quality. Internally decide delegate or skip. Strongly consider up to ${Math.max(1, workers.deep)} execution worker${Math.max(1, workers.deep) === 1 ? "" : "s"} for non-trivial work. Use them for file inspection, implementation prep, scoped implementation help, risk discovery, patch planning, or validation prep; skip only for trivial changes or no useful parallel work and give only the concise skip reason. Do not print internal policy deliberation.`;
2133
2185
  const automatableEvidenceGuidance = `Automatable evidence contract:
2134
2186
  - Before calling workflow_execution_result, you MUST gather all automatable validation evidence that the approved plan requires.
2135
2187
  - For web/app projects (React, Vite, Next.js, Vue, Svelte, etc.): run build and test commands; when the plan requires runtime/browser/localStorage behavior, start a bounded dev or preview server and verify the behavior with available commands (curl, node scripts, or the platform's built-in tools), then stop/cleanup the server.
@@ -2203,7 +2255,7 @@ ${requiredSubagentPreflightSection(preflightBlock)}
2203
2255
  - Execution agents may run in parallel for analysis, file inspection, implementation preparation, scoped implementation help, patch planning, regression search, and validation preparation.
2204
2256
  - Execution agents may perform work allowed by their configured tools and agent contract, but must stay inside the approved Plan scope and current per-step boundary when per-step gates are enabled.
2205
2257
  - Main executor owns final integration, validation-ready summary, and workflow_execution_result. Sub-agent file writes must follow editConcurrencyMode=${settings.subagents.editConcurrencyMode ?? "sequential"}; with sequential mode, serialize writes and avoid conflicting parallel edits.
2206
- - ${preflightSatisfied && policy === "forced" ? "Forced execution policy was satisfied by Workflow Suite preflight; do not rerun required workers solely for policy compliance." : "If executionPolicy is forced, activate the current step with workflow_progress first, then required sub-agents must run before read/grep/find/ls/edit/write/bash or final summary, or stop with: Sub-agent policy is forced, but sub-agent execution is unavailable because <reason>."}
2258
+ - ${preflightSatisfied && subagentPolicyRequiresRequiredEvidence(policy) ? "Forced execution policy was satisfied by Workflow Suite preflight; do not rerun required workers solely for policy compliance." : "If executionPolicy is forced, activate the current step with workflow_progress first, then required sub-agents must run before edit/write/meaningful bash/final summary. Local read/grep/find/ls probes may run only to orient the parent or form the required sub-agent call; they do not satisfy the worker requirement. If sub-agent execution is unavailable, stop with: Sub-agent policy is forced, but sub-agent execution is unavailable because <reason>."}
2207
2259
  - Final execution summary must include Sub-Agent Usage Summary with agents used and findings applied, or a forced-policy blocker.
2208
2260
 
2209
2261
  ${subagentCapabilityTable()}
@@ -2227,15 +2279,15 @@ function validatePrompt(state: WorkflowState, settings = loadWorkflowSettings(),
2227
2279
  const preflightSatisfied = hasRequiredSubagentPreflight(preflightBlock);
2228
2280
  const subagentGuidance = !validationSubagentsAllowed(settings)
2229
2281
  ? "Validation sub-agents are disabled by settings. Do not call subagent."
2230
- : preflightSatisfied && policy === "forced"
2282
+ : preflightSatisfied && subagentPolicyRequiresRequiredEvidence(policy)
2231
2283
  ? forcedSubagentPolicySatisfiedGuidance("validation")
2232
2284
  : policy === "forced"
2233
- ? `FORCED SUB-AGENT POLICY: you are the parent validator turn. Before final validation verdict, you MUST call the subagent tool with at least ${Math.max(1, workers.maximum)} validation worker(s), preferably quality-validation, general-worker, codebase-research, or implementation-planning. Do not call workflow-orchestrator for this forced Plan Validation requirement; the validator is already the parent controller. If subagent execution fails, stop and report the exact blocker.`
2285
+ ? `FORCED SUB-AGENT POLICY: you are the parent validator turn. Before final validation verdict or any substantive validation work, you MUST call the subagent tool with at least ${Math.max(1, workers.maximum)} validation worker(s), preferably quality-validation, general-worker, codebase-research, or implementation-planning. ${forcedSubagentCallShape("Validation", Math.max(1, workers.maximum))} Do not call workflow-orchestrator for this forced Plan Validation requirement; the validator is already the parent controller. If subagent execution fails, stop and report the exact blocker.`
2234
2286
  : policy === "maximum"
2235
- ? `For non-trivial validation, use validation sub-agents for independent checks, regression review, and risk analysis. Strongly target at least ${workers.maximum} validation workers, preferably quality-validation plus focused reviewers; skip only for trivial validation or unavailable sub-agent execution and explain why.`
2287
+ ? `For non-trivial validation, use validation sub-agents for independent checks, regression review, and risk analysis. Strongly target at least ${workers.maximum} validation workers, preferably quality-validation plus focused reviewers; internally decide delegate or skip, skip only for trivial validation or unavailable sub-agent execution, and give only the concise skip reason. Do not print internal policy deliberation.`
2236
2288
  : policy === "deep"
2237
- ? `For non-trivial validation, use at least ${workers.deep} validation sub-agent for independent checks, regression review, or risk analysis. Prefer quality-validation. Explain any skip.`
2238
- : "Validation sub-agents are strongly encouraged for independent review. Use them for plan compliance, diff review, regression risk, build/test review, or manual-QA classification; skip only for trivial validation and explain why.";
2289
+ ? `For non-trivial validation, use at least ${workers.deep} validation sub-agent for independent checks, regression review, or risk analysis. Prefer quality-validation. Internally decide delegate or skip and give only the concise skip reason. Do not print internal policy deliberation.`
2290
+ : `Validation sub-agents are strongly encouraged for independent review. Internally decide delegate or skip. Strongly consider up to ${Math.max(1, workers.deep)} validation worker${Math.max(1, workers.deep) === 1 ? "" : "s"} for non-trivial work. Use them for plan compliance, diff review, regression risk, build/test review, or manual-QA classification; skip only for trivial validation or no useful parallel work and give only the concise skip reason. Do not print internal policy deliberation.`;
2239
2291
  const automatableEvidenceVerifierGuidance = `Automatable evidence verification:
2240
2292
  - Before marking Manual Verification Required: yes, verify that the missing evidence is genuinely non-automatable.
2241
2293
  - If the plan required dev server, browser, localStorage, runtime, or endpoint checks that were not attempted by the executor, and those checks can be performed with safe read-only bash or parent runtime tools such as workflow_browser_check, mark Concrete Repairable Issue: yes and Evidence Gap: yes, then return FAIL rather than PARTIAL PASS.
@@ -2293,7 +2345,7 @@ ${requiredSubagentPreflightSection(preflightBlock)}
2293
2345
  ${subagentCapabilityTable()}
2294
2346
 
2295
2347
  - When validationPolicy is auto, deep, or maximum, validation sub-agents are expected for non-trivial work; prefer quality-validation for independent diff/risk/build-test review.
2296
- - ${preflightSatisfied && policy === "forced" ? "Forced validation policy was satisfied by Workflow Suite preflight; do not rerun required workers solely for policy compliance." : "If validationPolicy is forced, required sub-agents must run before verdict, or stop with: Sub-agent policy is forced, but sub-agent execution is unavailable because <reason>."}
2348
+ - ${preflightSatisfied && subagentPolicyRequiresRequiredEvidence(policy) ? "Forced validation policy was satisfied by Workflow Suite preflight; do not rerun required workers solely for policy compliance." : "If validationPolicy is forced, required sub-agents must run before verdict, or stop with: Sub-agent policy is forced, but sub-agent execution is unavailable because <reason>."}
2297
2349
  - PASS only when the approved plan is fully satisfied with no blocking unresolved risk.
2298
2350
  - FAIL when concrete missing requirements, unexpected changes, regressions, broken checks, unsafe/out-of-scope work, or concrete code/content/citation/source/file/metadata/artifact fixes remain.
2299
2351
  - FAIL when automatable runtime evidence (build, test, dev server, browser, localStorage, API response) was not gathered and the checks are performable with available tools. Missing automatable evidence is a concrete repairable issue, not a manual-only caveat.
@@ -2357,15 +2409,15 @@ function workflowRepairPrompt(state: WorkflowState, settings = loadWorkflowSetti
2357
2409
  const preflightSatisfied = hasRequiredSubagentPreflight(preflightBlock);
2358
2410
  const subagentGuidance = !phaseAutoUseAllowed(settings, "Repair") || policy === "off"
2359
2411
  ? "Repair sub-agents are disabled by settings. Do not call subagent."
2360
- : preflightSatisfied && policy === "forced"
2412
+ : preflightSatisfied && subagentPolicyRequiresRequiredEvidence(policy)
2361
2413
  ? forcedSubagentPolicySatisfiedGuidance("repair")
2362
2414
  : policy === "forced"
2363
- ? `FORCED SUB-AGENT POLICY: before any repair edit or final Workflow Repair Summary, you MUST call the subagent tool with at least ${Math.max(1, workers.maximum)} repair/execution worker(s). Use them for read-only repair verification, missing-file inspection, patch planning, and validation preparation. If subagent execution fails, stop and report the exact blocker.`
2415
+ ? `FORCED SUB-AGENT POLICY: before any repair edit or final Workflow Repair Summary, you MUST call the subagent tool with at least ${Math.max(1, workers.maximum)} repair/execution worker(s). ${forcedSubagentCallShape("Repair", Math.max(1, workers.maximum))} Use them for read-only repair verification, missing-file inspection, patch planning, and validation preparation. If subagent execution fails, stop and report the exact blocker.`
2364
2416
  : policy === "maximum"
2365
- ? `For non-trivial repair, strongly target ${Math.max(1, workers.maximum)} repair/execution worker(s) before edits; continue if skipped only with an explanation.`
2417
+ ? `For non-trivial repair, strongly target ${Math.max(1, workers.maximum)} repair/execution worker(s) before edits; internally decide delegate or skip, and continue if skipped only with a concise reason. Do not print internal policy deliberation.`
2366
2418
  : policy === "deep"
2367
- ? `For non-trivial repair, use ${Math.max(1, workers.deep)} repair/execution worker(s) before edits when useful and explain any skip.`
2368
- : "Repair sub-agents are strongly encouraged. Use them for failure triage, missing-file inspection, patch planning, and validation prep; skip only with a concrete reason.";
2419
+ ? `For non-trivial repair, use ${Math.max(1, workers.deep)} repair/execution worker(s) before edits when useful. Internally decide delegate or skip and give only the concise skip reason. Do not print internal policy deliberation.`
2420
+ : `Repair sub-agents are strongly encouraged. Internally decide delegate or skip. Strongly consider up to ${Math.max(1, workers.deep)} repair worker${Math.max(1, workers.deep) === 1 ? "" : "s"} for non-trivial work. Use them for failure triage, missing-file inspection, patch planning, and validation prep; skip only with a concrete reason. Do not print internal policy deliberation.`;
2369
2421
  return `${repairPrompt}
2370
2422
 
2371
2423
  You are in PI WORKFLOW REPAIR MODE.
@@ -2395,7 +2447,7 @@ Sub-agent repair policy:
2395
2447
  ${requiredSubagentPreflightSection(preflightBlock)}
2396
2448
  - Repair-mode sub-agent workers may inspect, triage, and plan safely. File writes must remain serialized through the Executor in repair mode unless scoped conflict protection is explicitly enabled.
2397
2449
  - You, the main repair executor, own all file writes, edits, and bash commands. Even when forced sub-agent policy is active and sub-agents report they cannot write files, you must proceed with your own file writes, edits, and bash commands after sub-agent inspection completes. Sub-agents are read-only by design; their inability to write does not mean you cannot write.
2398
- - ${preflightSatisfied && policy === "forced" ? "Forced repair policy was satisfied by Workflow Suite preflight; do not rerun required workers solely for policy compliance." : "If repairPolicy is forced, required sub-agents must run before any repair edit or final repair summary, or stop with: Sub-agent policy is forced, but sub-agent execution is unavailable because <reason>."}
2450
+ - ${preflightSatisfied && subagentPolicyRequiresRequiredEvidence(policy) ? "Forced repair policy was satisfied by Workflow Suite preflight; do not rerun required workers solely for policy compliance." : "If repairPolicy is forced, required sub-agents must run before any repair edit or final repair summary, or stop with: Sub-agent policy is forced, but sub-agent execution is unavailable because <reason>."}
2399
2451
  - Do not re-grade validation. Do not claim PASS/FAIL for repaired work; recommend revalidation.
2400
2452
  - If no concrete repairable issue exists, perform no-op repair summary and recommend manual verification or /plan revalidate.
2401
2453
 
@@ -2431,15 +2483,15 @@ function reviewerPrompt(state: WorkflowState, settings = loadWorkflowSettings(),
2431
2483
  const preflightSatisfied = hasRequiredSubagentPreflight(preflightBlock);
2432
2484
  const subagentGuidance = !reviewSubagentsAllowed(settings)
2433
2485
  ? "Reviewer sub-agents are disabled by settings. Do not call subagent."
2434
- : preflightSatisfied && policy === "forced"
2486
+ : preflightSatisfied && subagentPolicyRequiresRequiredEvidence(policy)
2435
2487
  ? forcedSubagentPolicySatisfiedGuidance("review")
2436
2488
  : policy === "forced"
2437
- ? `FORCED SUB-AGENT POLICY: before final reviewer report, you MUST call the subagent tool with at least ${Math.max(1, workers.maximum)} review worker(s) for independent read-only challenge review. If subagent execution fails, stop and report the exact blocker.`
2489
+ ? `FORCED SUB-AGENT POLICY: before final reviewer report or your own review analysis, you MUST call the subagent tool with at least ${Math.max(1, workers.maximum)} review worker(s) for independent read-only challenge review. ${forcedSubagentCallShape("Review", Math.max(1, workers.maximum))} If subagent execution fails, stop and report the exact blocker.`
2438
2490
  : policy === "maximum"
2439
- ? `For non-trivial review, use multiple reviewer sub-agents for independent risk review, regression review, implementation-scope review, and validation-plan review. Strongly target at least ${workers.maximum} review workers; skip only for trivial review or unavailable sub-agent execution and explain why.`
2491
+ ? `For non-trivial review, use multiple reviewer sub-agents for independent risk review, regression review, implementation-scope review, and validation-plan review. Strongly target at least ${workers.maximum} review workers; internally decide delegate or skip, skip only for trivial review or unavailable sub-agent execution, and give only the concise skip reason. Do not print internal policy deliberation.`
2440
2492
  : policy === "deep"
2441
- ? `For non-trivial review, use at least ${workers.deep} reviewer sub-agent for independent risk review, regression review, implementation-scope review, or validation-plan review. Explain any skip.`
2442
- : "Reviewer sub-agents are strongly encouraged. Use them to challenge scope, files, risk, and validation plan; skip only for trivial review and explain why.";
2493
+ ? `For non-trivial review, use at least ${workers.deep} reviewer sub-agent for independent risk review, regression review, implementation-scope review, or validation-plan review. Internally decide delegate or skip and give only the concise skip reason. Do not print internal policy deliberation.`
2494
+ : `Reviewer sub-agents are strongly encouraged. Internally decide delegate or skip. Strongly consider up to ${Math.max(1, workers.deep)} review worker${Math.max(1, workers.deep) === 1 ? "" : "s"} for non-trivial work. Use them to challenge scope, files, risk, and validation plan; skip only for trivial review or no useful parallel work and give only the concise skip reason. Do not print internal policy deliberation.`;
2443
2495
  return `You are in PI WORKFLOW REVIEWER MODE.
2444
2496
 
2445
2497
  CRITICAL: If forced review sub-agents are required by policy, call the subagent tool FIRST — before your own review analysis. Sub-agent findings must inform your review, not validate it after the fact. Once sub-agents complete (or if no sub-agents are required), perform your read-only review and call workflow_review_result with your verdict, issues, summary, and safety flags. After workflow_review_result returns its control-verdict tool result, STOP IMMEDIATELY. Do not call any more tools, do not call subagent again, do not create diagrams, and do not continue prose analysis. Workflow Suite owns the next handoff to execution or review retry. Do not do your own review analysis before dispatching required sub-agents.
@@ -2466,7 +2518,7 @@ ${subagentCapabilityTable()}
2466
2518
 
2467
2519
  - When reviewPolicy is auto, deep, or maximum, reviewer sub-agents are expected for non-trivial plans to challenge scope, files, risk, and validation plan.
2468
2520
  - Reviewer sub-agents must not perform direct file edits.
2469
- - ${preflightSatisfied && policy === "forced" ? "Forced review policy was satisfied by Workflow Suite preflight; do not rerun required workers solely for policy compliance." : "If reviewPolicy is forced, required sub-agents must run before the report, or stop with: Sub-agent policy is forced, but sub-agent execution is unavailable because <reason>."}
2521
+ - ${preflightSatisfied && subagentPolicyRequiresRequiredEvidence(policy) ? "Forced review policy was satisfied by Workflow Suite preflight; do not rerun required workers solely for policy compliance." : "If reviewPolicy is forced, required sub-agents must run before the report, or stop with: Sub-agent policy is forced, but sub-agent execution is unavailable because <reason>."}
2470
2522
  - The reviewer must not rubber-stamp execution; surface missing requirements before the executor starts.
2471
2523
  - Plan Review is notes-first for control flow. Use NOTES for nearly all actionable advice, including severe executor-correctable findings. Use NEEDS REPAIR only when the Plan text is structurally unusable for execution, such as having no executable implementation steps.
2472
2524
  - Validation command additions, rollback wording fixes, selector/test-hook refinements, off-limits/out-of-scope lists, instruction text updates, implementation parameter suggestions, game-rule details, impossible browser/test move sequences, missing draw/test data sequences, dev-server readiness, AI/settings/accessibility details, localStorage keys, icon choices, and executor cautions are executor notes, not repair blockers.
@@ -3901,6 +3953,20 @@ function planProgressHasOpenSteps(progress: PlanProgressState | undefined): bool
3901
3953
  return Boolean(progress?.steps.some((step) => step.status === "pending" || step.status === "active" || step.status === "failed" || step.status === "blocked"));
3902
3954
  }
3903
3955
 
3956
+ function planValidationBoundaryReached(state: WorkflowState, settings: ReturnType<typeof loadWorkflowSettings>): boolean {
3957
+ if (settings.workflow.validateAfterEachStep === true && typeof state.planStepValidationIndex === "number") return true;
3958
+ if (!state.approvedPlan?.trim() || !workflowPlanProgressEnabled(settings)) return true;
3959
+ const steps = state.planProgress?.steps ?? [];
3960
+ if (!steps.length) return true;
3961
+ return planProgressAllStepsCompleted(state);
3962
+ }
3963
+
3964
+ function planExecutionIncomplete(state: WorkflowState, settings: ReturnType<typeof loadWorkflowSettings>): boolean {
3965
+ if (!state.approvedPlan?.trim() || !workflowPlanProgressEnabled(settings)) return false;
3966
+ const steps = state.planProgress?.steps ?? [];
3967
+ return steps.length > 0 && planProgressHasOpenSteps(state.planProgress) && !planValidationBoundaryReached(state, settings);
3968
+ }
3969
+
3904
3970
  function planRuntimeLabel(state: WorkflowState): string {
3905
3971
  return `Runtime: ${formatDurationMs(planActiveRuntimeMs(state))} active`;
3906
3972
  }
@@ -4449,8 +4515,13 @@ function classifyStandardWork(task: string | undefined, answerSummary?: string):
4449
4515
  return { phase: "Planning", kind: "read_only" };
4450
4516
  }
4451
4517
 
4518
+ function trivialSubagentSkipReason(phase: SubagentPhase, task: string | undefined, kind?: WorkflowState["standardWorkKind"]): string | undefined {
4519
+ const decision = forcedSubagentActionDecision({ phase, policy: "forced", task, kind });
4520
+ return decision.outcome === "exempt_trivial" ? decision.reason : undefined;
4521
+ }
4522
+
4452
4523
  function standardForcedSubagentSafeBash(command: string): boolean {
4453
- return standardSafeReadOnlyBash(command);
4524
+ return forcedSubagentActionDecision({ phase: "Execution", policy: "forced", toolName: "bash", command }).allowBeforeEvidence;
4454
4525
  }
4455
4526
 
4456
4527
  function standardTodoUsageText(): string {
@@ -4851,6 +4922,9 @@ function standardPrompt(state: WorkflowState, settings: ReturnType<typeof loadWo
4851
4922
  const standardSubagentPolicyBlock = standardSubagentsAllowed(settings)
4852
4923
  ? `Standard sub-agent policy:\n- active phase: ${state.standardActivePhase ?? "Planning"}${state.standardWorkKind ? ` (${state.standardWorkKind})` : ""}\n- agentScope: ${settings.standard.subagentScope ?? "user"}\n- planning/research: ${activeWorkerTargetLabel(standardPhasePolicy(settings, "Planning"), standardWorkerCount(settings, "Planning"))}\n- execution: ${activeWorkerTargetLabel(standardPhasePolicy(settings, "Execution"), standardWorkerCount(settings, "Execution"))}\n- repair: ${activeWorkerTargetLabel(standardPhasePolicy(settings, "Repair"), standardWorkerCount(settings, "Repair"))}\n- review: ${activeWorkerTargetLabel(standardPhasePolicy(settings, "Review"), standardWorkerCount(settings, "Review"))}\n- validation: ${activeWorkerTargetLabel(standardPhasePolicy(settings, "Validation"), standardWorkerCount(settings, "Validation"))}\nUse any configured package, user, or project agent that fits the task. When calling subagent in Standard Mode, pass agentScope=${settings.standard.subagentScope ?? "user"} and workflowPhase=planning, execution, repair, review, or validation. Do not launch execution sub-agents merely for read-only summaries, status updates, action-item reports, or docs/repo inspection; use execution sub-agents only before actual mutation or execution-class bash. If the active Standard phase is execution or repair and that phase policy is forced, satisfy the required sub-agent usage before edit/write/unsafe bash.`
4853
4924
  : "Standard Mode sub-agent delegation is disabled by settings.";
4925
+ const standardSubagentDecisionInstruction = standardSubagentsAllowed(settings)
4926
+ ? "For Standard auto/deep/maximum sub-agent policies, internally decide delegate or skip. Delegate for non-trivial parallelizable research, validation, repair, or implementation prep. Skip for trivial read-only/status work or when no useful parallel worker exists. Do not print internal sub-agent policy deliberation; if no worker runs, give only a concise user-facing skip reason when a Sub-Agent Usage Summary is naturally relevant."
4927
+ : "";
4854
4928
  const autoCheckInstruction = standardAutoChecksRequired(state, settings)
4855
4929
  ? `Your first visible lines for every user-submitted Standard Mode response must be exactly this parser-safe Standard Auto Checks contract before any other text:\nStandard Auto Checks:\nCLARIFICATION_DECISION: ask|skip|disabled\nCLARIFICATION_REASON: <brief user-facing reason>\nTODO_DECISION: create|skip|on request|required|disabled|active\nTODO_REASON: <brief user-facing reason>\nUse CLARIFICATION_DECISION=ask only when you will ask clarification. Use skip when clarification is not needed. Use TODO_DECISION=create when you will initialize optional task-specific To Do tracking with standard_todo, required when To Do tracking is required by settings, active when a To Do list is already active, skip when visible tracking is not useful, on request when To Do tracking starts only on explicit request, and disabled when settings disable it. Keep reasons concise; do not reveal chain-of-thought.`
4856
4930
  : "Standard auto To Do and clarification checks are disabled; no Standard Auto Checks block is required.";
@@ -4888,6 +4962,7 @@ Rules:
4888
4962
  - ${clarificationLine}
4889
4963
  - ${autoCheckInstruction}
4890
4964
  - ${standardSubagentPolicyBlock}
4965
+ ${standardSubagentDecisionInstruction ? `- ${standardSubagentDecisionInstruction}` : ""}
4891
4966
 
4892
4967
  ${standardSubagentsAllowed(settings) ? subagentCapabilityTable() : ""}
4893
4968
 
@@ -7459,12 +7534,34 @@ function repairFailureApprovalReason(text: string, options: { retryMode?: string
7459
7534
  return undefined;
7460
7535
  }
7461
7536
 
7462
- function validationFailureRequiresApproval(text: string, settings: ReturnType<typeof loadWorkflowSettings>): string | undefined {
7537
+ function concreteRepairFailureHasExplicitApprovalRisk(text: string, options: { retryMode?: string; requireApprovalForDestructiveRepair: boolean; requireApprovalForOutOfScopeRepair: boolean }): boolean {
7538
+ const actionable = actionableSafetyText(text).replace(/\bworkflow_\w+\b/gi, "[tool]");
7539
+ const lines = actionable.split(/\n+/).map((line) => line.trim()).filter(Boolean);
7540
+ const highRiskAction = lines.some((line) =>
7541
+ /\b(delete|drop|destroy|reset|clean|force|overwrite|destructive|rm\s+-rf|migration|migrate|database|db\b|schema|sql|ddl|deploy|push|secret|token|credential|password|api key|auth|session|\.env|\.factory|\.cursor)\b/i.test(line)
7542
+ && /\b(repair|fix|change|modify|touch|read|print|expose|update|write|requires?|needs?|must|should|perform|run|execute|apply|approval|permission)\b/i.test(line)
7543
+ );
7544
+ if (options.requireApprovalForDestructiveRepair && highRiskAction) return true;
7545
+ if (options.requireApprovalForOutOfScopeRepair && lines.some((line) =>
7546
+ /\b(out[- ]of[- ]scope|outside scope|scope expansion|new requirement|unapproved)\b/i.test(line)
7547
+ && /\b(repair|fix|change|modify|update|write|requires?|needs?|must|should|perform|run|execute|apply)\b/i.test(line)
7548
+ )) return true;
7549
+ return (options.retryMode ?? "safe_only") === "safe_only" && lines.some((line) =>
7550
+ /\b(manual approval|needs approval|ask user|unsafe|permission)\b/i.test(line)
7551
+ && /\b(repair|fix|change|modify|update|write|requires?|needs?|must|should|perform|run|execute|apply)\b/i.test(line)
7552
+ );
7553
+ }
7554
+
7555
+ function validationFailureRequiresApproval(text: string, settings: ReturnType<typeof loadWorkflowSettings>, options: { concreteRepairableIssue?: boolean; evidenceGap?: boolean } = {}): string | undefined {
7463
7556
  if (settings.missions.validationRetryMode === "off") return "validationRetryMode=off.";
7464
- return repairFailureApprovalReason(text, {
7557
+ const gateOptions = {
7465
7558
  retryMode: settings.missions.validationRetryMode,
7466
7559
  requireApprovalForDestructiveRepair: settings.missions.requireApprovalForDestructiveRepair !== false,
7467
7560
  requireApprovalForOutOfScopeRepair: settings.missions.requireApprovalForOutOfScopeRepair !== false,
7561
+ };
7562
+ if (options.concreteRepairableIssue === true && options.evidenceGap !== true && !concreteRepairFailureHasExplicitApprovalRisk(text, gateOptions)) return undefined;
7563
+ return repairFailureApprovalReason(text, {
7564
+ ...gateOptions,
7468
7565
  outOfScopeReason: "repair appears outside the approved milestone scope.",
7469
7566
  safeOnlyReason: "safe_only repair mode requires approval for this validation failure.",
7470
7567
  });
@@ -7936,7 +8033,7 @@ function fallbackMissionMilestones(goal: string): MissionMilestone[] {
7936
8033
  ];
7937
8034
  }
7938
8035
 
7939
- function missionPlanPrompt(mission: MissionState, settings: ReturnType<typeof loadWorkflowSettings>, options: { forceClarification?: boolean; forceReason?: string; answerSummary?: string; qualityGateFeedback?: string; reviewFeedback?: string; preflightBlock?: string } = {}): string {
8036
+ function missionPlanPrompt(mission: MissionState, settings: ReturnType<typeof loadWorkflowSettings>, options: { forceClarification?: boolean; forceAdditionalClarification?: boolean; forceReason?: string; answerSummary?: string; qualityGateFeedback?: string; reviewFeedback?: string; preflightBlock?: string } = {}): string {
7940
8037
  const base = readPromptFile("mission-plan.md", "You are PI MISSION MODE PLANNER. Output MISSION_DECISION: plan and milestone headings. Do not execute.");
7941
8038
  const baseWithSubagentGuidance = `${base}\n\n${subagentCapabilityTable()}`;
7942
8039
  const sub = settings.subagents;
@@ -7949,12 +8046,24 @@ function missionPlanPrompt(mission: MissionState, settings: ReturnType<typeof lo
7949
8046
  const subagentsBeforeClarification = useSubagentsBeforeClarification(settings, "mission");
7950
8047
  const planningOrchestrationPolicy = (sub as typeof sub & { planningOrchestrationPolicy?: string }).planningOrchestrationPolicy ?? "orchestrator_first";
7951
8048
  const preflightSatisfied = hasRequiredSubagentPreflight(options.preflightBlock);
8049
+ const forcedPlanningEvidenceRequired = subagentPolicyRequiresRequiredEvidence(policy) && !preflightSatisfied;
7952
8050
  const orchestratorGuidance = preflightSatisfied
7953
8051
  ? "Mission planning orchestration required by forced policy has already been handled by Workflow Suite preflight. Do not call workflow-orchestrator again unless there is genuinely new targeted orchestration work beyond the preflight findings."
8052
+ : forcedPlanningEvidenceRequired && planningNeedsOrchestrator(settings, "mission")
8053
+ ? planningOrchestrationPolicy === "forced_orchestrated"
8054
+ ? "ORCHESTRATION REQUIRED: include workflow-orchestrator as one task inside the required forced Mission Planning parallel subagent call. Do not call workflow-orchestrator separately before the worker batch."
8055
+ : "Mission planning orchestration is available for this turn. If it is useful, include workflow-orchestrator as one task inside the required forced Mission Planning parallel subagent call; do not call workflow-orchestrator separately before the worker batch."
7954
8056
  : planningNeedsOrchestrator(settings, "mission")
7955
8057
  ? "ORCHESTRATION REQUIRED: call workflow-orchestrator before launching parallel mission planning/research workers or producing the milestone plan. The orchestrator must scope worker tasks, identify capability gaps, and decide whether live docs/API access is available before workers run."
7956
8058
  : "Orchestrator is optional for this mission planning turn. Use workflow-orchestrator if the mission is broad, multi-pass, or needs coordinated workers.";
7957
- return `${baseWithSubagentGuidance}\n\n${professionalOutputGuidance("Mission planning")}\n\nMission ID: ${mission.id}\nGoal: ${mission.goal}\nAutonomy: ${mission.autonomy}\nApproval Required: ${mission.approvalRequired}\nClarification recommended: ${missionNeedsClarification(mission.goal) ? "yes" : "no"}\nInitial analysis requirement: ${analysisInstruction}\n${options.answerSummary ? `\nMission clarification answers already supplied:\n${options.answerSummary}\nUse these answers as planning constraints. Do not ask the same clarification again. Produce MISSION_DECISION: plan with parser-safe milestones unless a genuinely new blocker exists; carry remaining uncertainty as assumptions/open questions inside the mission plan.\n` : ""}${options.reviewFeedback ? `\nMISSION REVIEW REPAIR REQUIRED BEFORE APPROVAL.\nReviewer feedback:\n${options.reviewFeedback}\nRevise the mission milestone plan only. Do not execute. Return MISSION_DECISION: plan with parser-safe milestones that address the reviewer feedback.\n` : ""}${options.forceClarification ? `\nMISSION CLARIFICATION IS REQUIRED BEFORE FINAL MILESTONE PLANNING. Reason: ${options.forceReason ?? "mission clarification policy requires it"}\nYour first line must be exactly MISSION_DECISION: clarify. Your job in this turn is to perform lightweight mission analysis, then generate only high-value mission-goal-specific A/B/C/D clarification questions. Do not produce the mission plan yet. ${subagentsBeforeClarification ? "If mission planning depth/policy calls for it and sub-agent use is available, use read-only planning/research sub-agents before asking clarification so the questions are context-aware." : "Do not call sub-agents in this clarification-generation turn unless forced by sub-agent policy."}\n` : ""}${options.qualityGateFeedback ? `\nThe previous mission clarification output failed the clarification quality gate: ${options.qualityGateFeedback}\nRegenerate better mission-specific clarification questions${options.forceClarification ? ". Do not produce a mission plan in this retry." : ", or use MISSION_DECISION: plan if no high-value question exists."}\n` : ""}\nMANDATORY STRUCTURED HANDOFF: call mission_plan_result with decision=clarify, plan, or blocked before final response. The typed tool payload is the primary handoff control plane. If the tool is unavailable for any reason, fall back to the parser-safe legacy format below:\n## Clarifying Questions\n\n## Q1. <mission-specific short question>\nA. <option specific to this mission>\nB. <option specific to this mission>\nC. <option specific to this mission>\nD. Other: type your own answer\nSkip this question\n\nRules for questions:\n- Generate questions from the actual mission goal after initial analysis.\n- Clarification is not a survey. Ask only when the answer genuinely changes the milestone plan.\n- Prefer one focused question when possible. Maximum ${missionMaxClarificationQuestions(settings)} questions.\n- Each question must resolve at least one real ambiguity, risk, scope decision, validation decision, target-environment choice, permission boundary, sub-agent need, diagnostic-vs-implementation choice, acceptance criterion, or forbidden-file concern.\n- Each question must be specific and actionable.\n- Each option must be a concrete choice, not "yes/no".\n- Always include D. Other as last option.\n- Do not ask product-roadmap or strategy questions unless the user explicitly requested product strategy planning.\n\nWorkflow settings:\n- mission.planningDepth: ${missionPlanningDepth(settings)}\n- mission.clarificationMode: ${missionClarificationMode(settings)}\n- mission.maxClarificationQuestions: ${missionMaxClarificationQuestions(settings)}\n- mission.interactiveClarificationEnabled: ${settings.missions.interactiveClarificationEnabled !== false}\n- mission.clarificationTiming: ${settings.missions.clarificationTiming ?? "after_initial_analysis"}\n- mission.clarificationQualityGate: ${settings.missions.clarificationQualityGate !== false}\n- mission.allowClarificationWithoutAnalysis: ${settings.missions.allowClarificationWithoutAnalysis === true}\n- mission.useSubagentsBeforeClarification: ${settings.missions.useSubagentsBeforeClarification !== false}\n- subagents.enabled: ${sub.enabled}\n- mission.subagentPolicy: ${policy}\n- Mission Planning Workers: ${activeWorkerTargetLabel(policy, { deep: deepWorkers, maximum: maxWorkers })}\n- subagents.requireApprovalBeforeRun: ${sub.requireApprovalBeforeRun === true}\n- planningOrchestrationPolicy: ${planningOrchestrationPolicy}\n- allowParallelReadOnly: ${sub.allowParallelReadOnly !== false}\n- allowParallelPlanning: ${sub.allowParallelPlanning !== false}\n- allowParallelEdits: ${sub.allowParallelEdits === true}${requiredSubagentPreflightSection(options.preflightBlock)}\n\nMission sub-agent policy:\n- off: do not use sub-agents.\n- auto: sub-agents are strongly encouraged; skip only with a concrete trivial/unavailable reason.\n- deep: use at least ${deepWorkers} worker(s) for non-trivial mission planning.\n- maximum: strongly target at least ${maxWorkers} worker(s); skip only for trivial or unavailable cases with exact reason.\n- forced: ${preflightSatisfied ? "already satisfied by Workflow Suite preflight; do not call the visible subagent tool solely for policy compliance." : `you MUST use at least ${Math.max(1, maxWorkers)} worker(s), or stop with: Sub-agent policy is forced, but sub-agent execution is unavailable because <reason>.`}\n\n${orchestratorGuidance}\n\nUse safe read-only/planning sub-agents when enabled. Do not execute or edit. Include mission Sub-Agent Usage Summary with workers used and findings, or the exact skip/blocker reason.
8059
+ const forceClarification = options.forceClarification === true || options.forceAdditionalClarification === true;
8060
+ const answerGuidance = options.forceAdditionalClarification === true
8061
+ ? "Use these prior answers as planning constraints. Do not ask duplicate or already-answered clarification questions. This turn is an explicit request for additional Mission clarification, so produce MISSION_DECISION: clarify with only new mission-specific questions that materially change the plan."
8062
+ : "Use these answers as planning constraints. Do not ask the same clarification again. Produce MISSION_DECISION: plan with parser-safe milestones unless a genuinely new blocker exists; carry remaining uncertainty as assumptions/open questions inside the mission plan.";
8063
+ const forceClarificationGuidance = options.forceAdditionalClarification === true
8064
+ ? "Your first line must be exactly MISSION_DECISION: clarify. Your job in this turn is to perform lightweight contextual mission analysis, including relevant codebase/project-rule inspection when available, then generate only new high-value mission-goal-specific A/B/C/D clarification questions. Do not reuse stale question indexes or duplicate prior answered questions. Do not produce the mission plan yet."
8065
+ : "Your first line must be exactly MISSION_DECISION: clarify. Your job in this turn is to perform lightweight mission analysis, then generate only high-value mission-goal-specific A/B/C/D clarification questions. Do not produce the mission plan yet.";
8066
+ return `${baseWithSubagentGuidance}\n\n${professionalOutputGuidance("Mission planning")}\n\nMission ID: ${mission.id}\nGoal: ${mission.goal}\nAutonomy: ${mission.autonomy}\nApproval Required: ${mission.approvalRequired}\nClarification recommended: ${missionNeedsClarification(mission.goal) ? "yes" : "no"}\nInitial analysis requirement: ${analysisInstruction}\n${options.answerSummary ? `\nMission clarification answers already supplied:\n${options.answerSummary}\n${answerGuidance}\n` : ""}${options.reviewFeedback ? `\nMISSION REVIEW REPAIR REQUIRED BEFORE APPROVAL.\nReviewer feedback:\n${options.reviewFeedback}\nRevise the mission milestone plan only. Do not execute. Return MISSION_DECISION: plan with parser-safe milestones that address the reviewer feedback.\n` : ""}${forceClarification ? `\nMISSION CLARIFICATION IS REQUIRED BEFORE FINAL MILESTONE PLANNING. Reason: ${options.forceReason ?? "mission clarification policy requires it"}\n${forceClarificationGuidance} ${subagentsBeforeClarification ? "If mission planning depth/policy calls for it and sub-agent use is available, use read-only planning/research sub-agents before asking clarification so the questions are context-aware." : "Do not call sub-agents in this clarification-generation turn unless forced by sub-agent policy."}\n` : ""}${options.qualityGateFeedback ? `\nThe previous mission clarification output failed the clarification quality gate: ${options.qualityGateFeedback}\nRegenerate better mission-specific clarification questions${forceClarification ? ". Do not produce a mission plan in this retry." : ", or use MISSION_DECISION: plan if no high-value question exists."}\n` : ""}\nMANDATORY STRUCTURED HANDOFF: call mission_plan_result with decision=clarify, plan, or blocked before final response. The typed tool payload is the primary handoff control plane. If the tool is unavailable for any reason, fall back to the parser-safe legacy format below:\n## Clarifying Questions\n\n## Q1. <mission-specific short question>\nA. <option specific to this mission>\nB. <option specific to this mission>\nC. <option specific to this mission>\nD. Other: type your own answer\nSkip this question\n\nRules for questions:\n- Generate questions from the actual mission goal after initial analysis.\n- Additional clarification turns must use fresh dynamic mission analysis and must not display or reuse stored prior questions.\n- Additional clarification turns must ask only new, non-duplicate, mission-specific questions; prior answers are constraints, not question templates.\n- Do not reuse stale question indexes from a prior clarification turn.\n- Clarification is not a survey. Ask only when the answer genuinely changes the milestone plan.\n- Prefer one focused question when possible. Maximum ${missionMaxClarificationQuestions(settings)} questions.\n- Each question must resolve at least one real ambiguity, risk, scope decision, validation decision, target-environment choice, permission boundary, sub-agent need, diagnostic-vs-implementation choice, acceptance criterion, or forbidden-file concern.\n- Each question must be specific and actionable.\n- Each option must be a concrete choice, not "yes/no".\n- Always include D. Other as last option.\n- Do not ask product-roadmap or strategy questions unless the user explicitly requested product strategy planning.\n\nWorkflow settings:\n- mission.planningDepth: ${missionPlanningDepth(settings)}\n- mission.clarificationMode: ${missionClarificationMode(settings)}\n- mission.maxClarificationQuestions: ${missionMaxClarificationQuestions(settings)}\n- mission.interactiveClarificationEnabled: ${settings.missions.interactiveClarificationEnabled !== false}\n- mission.clarificationTiming: ${settings.missions.clarificationTiming ?? "after_initial_analysis"}\n- mission.clarificationQualityGate: ${settings.missions.clarificationQualityGate !== false}\n- mission.allowClarificationWithoutAnalysis: ${settings.missions.allowClarificationWithoutAnalysis === true}\n- mission.useSubagentsBeforeClarification: ${settings.missions.useSubagentsBeforeClarification !== false}\n- subagents.enabled: ${sub.enabled}\n- mission.subagentPolicy: ${policy}\n- Mission Planning Workers: ${activeWorkerTargetLabel(policy, { deep: deepWorkers, maximum: maxWorkers })}\n- subagents.requireApprovalBeforeRun: ${sub.requireApprovalBeforeRun === true}\n- planningOrchestrationPolicy: ${planningOrchestrationPolicy}\n- allowParallelReadOnly: ${sub.allowParallelReadOnly !== false}\n- allowParallelPlanning: ${sub.allowParallelPlanning !== false}\n- allowParallelEdits: ${sub.allowParallelEdits === true}${requiredSubagentPreflightSection(options.preflightBlock)}\n\nMission sub-agent policy:\n- off: do not use sub-agents.\n- auto: sub-agents are strongly encouraged; skip only with a concrete trivial/unavailable reason.\n- deep: use at least ${deepWorkers} worker(s) for non-trivial mission planning.\n- maximum: strongly target at least ${maxWorkers} worker(s); skip only for trivial or unavailable cases with exact reason.\n- forced: ${preflightSatisfied ? "already satisfied by Workflow Suite preflight; do not call the visible subagent tool solely for policy compliance." : `you MUST use at least ${Math.max(1, maxWorkers)} worker(s), or stop with: Sub-agent policy is forced, but sub-agent execution is unavailable because <reason>.`}\n\n${orchestratorGuidance}\n\nUse safe read-only/planning sub-agents when enabled. Do not execute or edit. Include mission Sub-Agent Usage Summary with workers used and findings, or the exact skip/blocker reason.
7958
8067
 
7959
8068
  Diagram guidance:
7960
8069
  ${workflowMermaidGuidance()}
@@ -7973,15 +8082,15 @@ function missionReviewPrompt(mission: MissionState, settings = loadWorkflowSetti
7973
8082
  const preflightSatisfied = hasRequiredSubagentPreflight(preflightBlock);
7974
8083
  const subagentGuidance = !reviewSubagentsAllowed(settings)
7975
8084
  ? "Reviewer sub-agents are disabled by settings. Do not call subagent."
7976
- : preflightSatisfied && policy === "forced"
8085
+ : preflightSatisfied && subagentPolicyRequiresRequiredEvidence(policy)
7977
8086
  ? forcedSubagentPolicySatisfiedGuidance("review")
7978
8087
  : policy === "forced"
7979
8088
  ? `FORCED SUB-AGENT POLICY: before final reviewer report, you MUST call the subagent tool with at least ${Math.max(1, workers.maximum)} review worker(s) for independent read-only challenge review. If subagent execution fails, stop and report the exact blocker.`
7980
8089
  : policy === "maximum"
7981
- ? `For non-trivial review, use multiple reviewer sub-agents for independent risk review, regression review, implementation-scope review, and validation-plan review. Strongly target at least ${workers.maximum} review workers; skip only for trivial or unavailable cases with exact reason.`
8090
+ ? `For non-trivial review, use multiple reviewer sub-agents for independent risk review, regression review, implementation-scope review, and validation-plan review. Strongly target at least ${workers.maximum} review workers; internally decide delegate or skip, skip only for trivial/no-useful-parallel/unavailable cases, and give only the concise skip reason. Do not print internal policy deliberation.`
7982
8091
  : policy === "deep"
7983
- ? `For non-trivial review, use at least ${workers.deep} reviewer sub-agent for independent risk review, implementation-scope review, or validation-plan review. Explain any skip.`
7984
- : "Reviewer sub-agents are strongly encouraged. Use them to challenge scope, milestones, risks, and validation plan; skip only for trivial review and explain why.";
8092
+ ? `For non-trivial review, use at least ${workers.deep} reviewer sub-agent for independent risk review, implementation-scope review, or validation-plan review. Internally decide delegate or skip and give only the concise skip reason. Do not print internal policy deliberation.`
8093
+ : "Reviewer sub-agents are strongly encouraged. Internally decide delegate or skip. Use them to challenge scope, milestones, risks, and validation plan; skip only for trivial review or no useful parallel work and give only the concise skip reason. Do not print internal policy deliberation.";
7985
8094
  const milestoneLines = mission.milestones.map((milestone, index) => `- ${milestone.id || `M${index + 1}`}: ${milestone.title || "Untitled"}\n Objective: ${milestone.objective || "none recorded"}\n Steps: ${(milestone.steps ?? []).join("; ") || "none recorded"}\n Acceptance Criteria: ${(milestone.validation ?? []).join("; ") || "none recorded"}\n Risks: ${(milestone.risks ?? []).join("; ") || "none recorded"}`).join("\n") || "- none recorded";
7986
8095
  return `You are in PI MISSION MODE REVIEWER MODE.
7987
8096
 
@@ -8022,7 +8131,7 @@ ${subagentCapabilityTable()}
8022
8131
 
8023
8132
  - When reviewPolicy is auto, deep, or maximum, reviewer sub-agents are expected for non-trivial mission plans to challenge scope, milestones, risks, and validation plan.
8024
8133
  - Reviewer sub-agents must not perform direct file edits.
8025
- - ${preflightSatisfied && policy === "forced" ? "Forced review policy was satisfied by Workflow Suite preflight; do not rerun required workers solely for policy compliance." : "If reviewPolicy is forced, required sub-agents must run before the report, or stop with: Sub-agent policy is forced, but sub-agent execution is unavailable because <reason>."}
8134
+ - ${preflightSatisfied && subagentPolicyRequiresRequiredEvidence(policy) ? "Forced review policy was satisfied by Workflow Suite preflight; do not rerun required workers solely for policy compliance." : "If reviewPolicy is forced, required sub-agents must run before the report, or stop with: Sub-agent policy is forced, but sub-agent execution is unavailable because <reason>."}
8026
8135
  - The reviewer must not rubber-stamp execution; surface missing requirements before the mission is approved.
8027
8136
  - Mission Review is notes-first for control flow. Use NOTES for nearly all actionable advice, including severe executor-correctable milestone findings. Use NEEDS REPAIR only when the Mission plan is structurally unusable, such as having no parser-safe milestones.
8028
8137
  - Rule clarifications, game-rule pinning, AI contracts, settings-step details, accessibility criteria, implementation details, validation improvements, rollback wording, files-to-avoid lists, UI instruction notes, README scope decisions, icon choices, localStorage key naming, impossible-but-correctable milestone details, and executor cautions are NOTES.
@@ -8402,6 +8511,8 @@ export default function workflowModes(pi: ExtensionAPI): void {
8402
8511
  if (phase === "Planning") return planToolsFor(settings);
8403
8512
  if (phase === "Review") return reviewToolsFor(settings);
8404
8513
  if (phase === "Validation") return validationToolsFor(settings);
8514
+ if (phase === "Repair" && state.mode === "mission_repairing") return missionRepairToolsFor(settings);
8515
+ if (phase === "Execution" && state.mode === "mission_running") return missionExecutionToolsFor(settings);
8405
8516
  return executionToolsFor(settings);
8406
8517
  };
8407
8518
  const armWorkflowToolsForPhase = (ctx: ExtensionContext, phase: WorkflowPendingToolPhase, reason: string): void => {
@@ -8445,6 +8556,124 @@ export default function workflowModes(pi: ExtensionAPI): void {
8445
8556
  syncWorkflowRuntimeForActivity(ctx, `pending phase clear: ${phase}`);
8446
8557
  traceWorkflowTracking(ctx, "pending-phase-clear", { phase, reason });
8447
8558
  };
8559
+ const requiredToolsForWorkflowPhase = (settings: ReturnType<typeof loadWorkflowSettings>, phase: WorkflowPendingToolPhase, mode: WorkflowState["mode"] = state.mode): string[] => {
8560
+ let tools: string[];
8561
+ if (mode === "standard") {
8562
+ tools = [...BASE_EXECUTE_TOOLS, "standard_todo", STANDARD_HANDOFF_RESULT_TOOL];
8563
+ if (standardSubagentsAllowed(settings)) tools.push("subagent");
8564
+ return Array.from(new Set(tools));
8565
+ }
8566
+ if (phase === "Planning") {
8567
+ tools = [...PLAN_TOOLS, WORKFLOW_PLAN_RESULT_TOOL];
8568
+ if (planningSubagentsAllowed(settings)) tools.push("subagent");
8569
+ return Array.from(new Set(tools));
8570
+ }
8571
+ if (phase === "Review") {
8572
+ tools = ["read", "grep", "find", "ls", WORKFLOW_DIAGRAM_TOOL, WORKFLOW_REVIEW_RESULT_TOOL];
8573
+ if (reviewSubagentsAllowed(settings)) tools.push("subagent");
8574
+ return Array.from(new Set(tools));
8575
+ }
8576
+ if (phase === "Validation") {
8577
+ tools = ["read", "grep", "find", "ls", "bash", WORKFLOW_DIAGRAM_TOOL, WORKFLOW_VALIDATION_RESULT_TOOL];
8578
+ if (validationSubagentsAllowed(settings)) tools.push("subagent");
8579
+ return Array.from(new Set(tools));
8580
+ }
8581
+ if (phase === "Repair" && mode === "mission_repairing") {
8582
+ tools = [...PLAN_TOOLS, "edit", "write", "bash", WORKFLOW_DIAGRAM_TOOL, WORKFLOW_REPAIR_RESULT_TOOL];
8583
+ if (executionSubagentsAllowed(settings)) tools.push("subagent");
8584
+ return Array.from(new Set(tools));
8585
+ }
8586
+ if (phase === "Execution" && mode === "mission_running") {
8587
+ tools = [...PLAN_TOOLS, "edit", "write", "bash", WORKFLOW_DIAGRAM_TOOL, MISSION_MILESTONE_RESULT_TOOL];
8588
+ if (executionSubagentsAllowed(settings)) tools.push("subagent");
8589
+ return Array.from(new Set(tools));
8590
+ }
8591
+ if (phase === "Repair") {
8592
+ tools = [...PLAN_TOOLS, "edit", "write", "bash", WORKFLOW_PROGRESS_TOOL, WORKFLOW_REPAIR_RESULT_TOOL];
8593
+ if (executionSubagentsAllowed(settings)) tools.push("subagent");
8594
+ return Array.from(new Set(tools));
8595
+ }
8596
+ return requiredPlanExecutionTools(settings);
8597
+ };
8598
+ const missingToolsForWorkflowPhase = (activeTools: string[], settings: ReturnType<typeof loadWorkflowSettings>, phase: WorkflowPendingToolPhase, mode: WorkflowState["mode"] = state.mode): string[] => {
8599
+ const active = new Set(activeTools);
8600
+ return requiredToolsForWorkflowPhase(settings, phase, mode).filter((tool) => !active.has(tool));
8601
+ };
8602
+ const toolSurfaceBlockReason = (phase: WorkflowPendingToolPhase, missing: string[], source: string): string =>
8603
+ `Required ${phase} tools are unavailable after tool rearm at ${source}: ${missing.join(", ")}. Phase prompt was not delivered.`;
8604
+ const blockWorkflowToolSurface = (ctx: ExtensionContext, phase: WorkflowPendingToolPhase, missing: string[], source: string): string => {
8605
+ const settings = loadWorkflowSettings(ctx.cwd);
8606
+ const reason = toolSurfaceBlockReason(phase, missing, source);
8607
+ clearPendingWorkflowToolPhase(ctx, phase, `tool-surface block: ${source}`);
8608
+ recordWorkflowInternalEvent(ctx, `Workflow ${phase} tool surface blocked at ${source}: ${missing.join(", ")}.`);
8609
+ traceWorkflowTracking(ctx, "tool-surface-blocked", { phase, source, missingTools: missing });
8610
+ if (state.mode === "standard") {
8611
+ setStandardRuntimeActive(ctx, false);
8612
+ pi.setActiveTools(standardToolsFor(settings));
8613
+ show(pi, `# Standard Workflow Blocked\n\n${reason}\n\nRetry the request after the Standard tool surface is repaired, or run /standard status.`);
8614
+ return reason;
8615
+ }
8616
+ if (isMissionWorkflowMode(state)) {
8617
+ const mission = (state.activeMissionId ? loadMissionState(state.activeMissionId) : undefined) ?? activeMission ?? loadMissionState("latest");
8618
+ if (mission) {
8619
+ const nextAction = phase === "Validation"
8620
+ ? "Repair the validation tool surface, then run /mission revalidate or /mission resume."
8621
+ : phase === "Repair"
8622
+ ? "Repair the Mission repair tool surface, then run /mission repair or /mission resume."
8623
+ : phase === "Review"
8624
+ ? "Repair the Mission review tool surface, then run /mission review or /mission approve."
8625
+ : "Repair the Mission execution tool surface, then run /mission continue or /mission resume.";
8626
+ const paused = saveActiveMission({
8627
+ ...mission,
8628
+ status: "paused",
8629
+ lastStopReason: reason,
8630
+ lastBlockReason: reason,
8631
+ nextAction,
8632
+ lastSummary: `Mission ${phase.toLowerCase()} did not start because required tools were unavailable.`,
8633
+ });
8634
+ activeMission = paused;
8635
+ updateState({ mode: "mission_paused", activeMissionId: paused.id, task: paused.goal, originalTask: paused.goal, approvedPlan: paused.planText }, ctx);
8636
+ } else {
8637
+ updateState({ mode: "awaiting_mission_input", activeMissionId: undefined }, ctx);
8638
+ }
8639
+ pi.setActiveTools(planToolsFor(settings));
8640
+ show(pi, `# Mission Workflow Blocked\n\n${reason}\n\nRun /mission status before continuing.`);
8641
+ return reason;
8642
+ }
8643
+ const planText = state.approvedPlan ?? state.draftPlan;
8644
+ const targetMode: WorkflowState["mode"] = phase === "Validation"
8645
+ ? "executed"
8646
+ : phase === "Repair"
8647
+ ? "validated"
8648
+ : phase === "Review"
8649
+ ? "plan_approved"
8650
+ : state.reviewerReport ? "reviewed" : "plan_approved";
8651
+ updateState({
8652
+ mode: targetMode,
8653
+ lastReviewFailure: phase === "Review" ? reason : state.lastReviewFailure,
8654
+ lastValidationFailure: phase === "Validation" ? reason : state.lastValidationFailure,
8655
+ lastRepairStatus: state.lastRepairStatus === "running" ? "failed" : state.lastRepairStatus,
8656
+ lastRepairAttempt: phase === "Repair" ? reason : state.lastRepairAttempt,
8657
+ planProgress: workflowPlanProgressEnabled(settings) && planText?.trim()
8658
+ ? mergePlanProgress({ ...state, mode: targetMode }, settings, { lifecycleStatus: "blocked", nextAction: `repair ${phase.toLowerCase()} tool surface, then /plan continue` }, planText)
8659
+ : state.planProgress,
8660
+ lastPlanStopSummary: undefined,
8661
+ }, ctx);
8662
+ pi.setActiveTools(webSafePlanTools(PLAN_TOOLS));
8663
+ show(pi, `# Plan Workflow Blocked\n\n${reason}\n\nRun /plan continue after the tool surface is repaired.`);
8664
+ return reason;
8665
+ };
8666
+ const verifyWorkflowToolSurface = (ctx: ExtensionContext, phase: WorkflowPendingToolPhase, source: string, mode: WorkflowState["mode"] = state.mode): string | undefined => {
8667
+ const settings = loadWorkflowSettings(ctx.cwd);
8668
+ const missing = missingToolsForWorkflowPhase(pi.getActiveTools(), settings, phase, mode);
8669
+ traceWorkflowTracking(ctx, "verify-tool-surface", { phase, source, mode, activeTools: pi.getActiveTools(), missingTools: missing });
8670
+ return missing.length ? blockWorkflowToolSurface(ctx, phase, missing, source) : undefined;
8671
+ };
8672
+ const armAndVerifyWorkflowToolsForPhase = (ctx: ExtensionContext, phase: WorkflowPendingToolPhase, reason: string): void => {
8673
+ armWorkflowToolsForPhase(ctx, phase, reason);
8674
+ const blockReason = verifyWorkflowToolSurface(ctx, phase, reason);
8675
+ if (blockReason) throw new WorkflowToolSurfaceAbort(blockReason);
8676
+ };
8448
8677
  let workflowSubagentActivityTimer: ReturnType<typeof setInterval> | undefined;
8449
8678
  let workflowSubagentActivityRenderCtx: ExtensionContext | undefined;
8450
8679
  let workflowUiClockTimer: ReturnType<typeof setInterval> | undefined;
@@ -8634,6 +8863,17 @@ export default function workflowModes(pi: ExtensionAPI): void {
8634
8863
 
8635
8864
  const initialPlanParentSuppressed = (): boolean => state.mode === "plan_draft" && state.reviewHandoffSuppression?.kind === "plan_typed_initial_to_approval" && state.lastWorkflowHandoff?.type === WORKFLOW_PLAN_RESULT_TOOL;
8636
8865
  const planReviewParentSuppressed = (): boolean => state.mode === "executing" && state.reviewHandoffSuppression?.kind === "plan_typed_review_to_execution";
8866
+ const missionReviewParentSuppressed = (): boolean => {
8867
+ if (state.reviewHandoffSuppression?.kind !== "mission_typed_review_to_approval") return false;
8868
+ if (state.mode !== "mission_plan_ready" && state.mode !== "mission_approved" && state.mode !== "mission_running") return false;
8869
+ const suppressedMissionId = state.reviewHandoffSuppression.activeMissionId;
8870
+ if (suppressedMissionId && state.activeMissionId && suppressedMissionId !== state.activeMissionId) return false;
8871
+ const mission = (state.activeMissionId ? loadMissionState(state.activeMissionId) : undefined)
8872
+ ?? (suppressedMissionId ? loadMissionState(suppressedMissionId) : undefined)
8873
+ ?? activeMission
8874
+ ?? loadMissionState("latest");
8875
+ return mission?.currentStep !== "reviewer" && (mission?.reviewerVerdict === "PASS" || mission?.reviewerVerdict === "NOTES");
8876
+ };
8637
8877
 
8638
8878
  const markPlanValidationHandoffDidNotStart = (ctx: ExtensionContext, settings: ReturnType<typeof loadWorkflowSettings>, reason: string): void => {
8639
8879
  updateState({
@@ -8821,7 +9061,10 @@ export default function workflowModes(pi: ExtensionAPI): void {
8821
9061
  const executedStepIndex = typeof state.planExecutionStepIndex === "number" ? state.planExecutionStepIndex : state.planProgress?.currentStepIndex;
8822
9062
  const progressEnabled = workflowPlanProgressEnabled(settings);
8823
9063
  const progressedPlanProgress = progressEnabled ? planProgressWithCompletedSteps(state, settings, completedSteps) : state.planProgress;
8824
- const progressedState = progressedPlanProgress ? { ...state, planProgress: progressedPlanProgress } : state;
9064
+ const nextPlanStepValidationIndex = settings.workflow.validateAfterEachStep === true ? executedStepIndex : state.planStepValidationIndex;
9065
+ const progressedState = progressedPlanProgress
9066
+ ? { ...state, planProgress: progressedPlanProgress, planStepValidationIndex: nextPlanStepValidationIndex }
9067
+ : { ...state, planStepValidationIndex: nextPlanStepValidationIndex };
8825
9068
  // Fix C4: lifecycle matching mode for blocked/completion paths
8826
9069
  const blockedLifecycle = state.reviewerReport ? "reviewed" : "blocked";
8827
9070
  if (status !== "completed") {
@@ -8830,16 +9073,32 @@ export default function workflowModes(pi: ExtensionAPI): void {
8830
9073
  showBlockedPlanRecoveryMenu(ctx);
8831
9074
  return { ...typedToolAck(), details: { accepted: true, status, validationStarted: false } };
8832
9075
  }
8833
- const hasOpenSteps = progressEnabled && progressedPlanProgress?.steps.length && settings.workflow.validateAfterEachStep !== true && planProgressHasOpenSteps(progressedPlanProgress);
8834
- if (hasOpenSteps && !completedSteps.length) {
8835
- updateState({ mode: state.reviewerReport ? "reviewed" : "plan_approved", executionSummary: summary, planExecutionStepIndex: undefined, planProgress: progressEnabled ? mergePlanProgress(progressedState, settings, { lifecycleStatus: blockedLifecycle, validationStatus: "pending", nextAction: "continue execution" }) : state.planProgress }, ctx);
8836
- queuePlanTerminalSummary(ctx, "blocked", "Plan execution incomplete", { reason: "Executor reported completion, but step tracking shows incomplete steps with no completedSteps metadata. Resume execution to complete remaining steps." });
9076
+ const hasOpenSteps = progressEnabled && progressedPlanProgress?.steps.length && planProgressHasOpenSteps(progressedPlanProgress);
9077
+ const executionIncomplete = planExecutionIncomplete(progressedState, settings);
9078
+ const missingCompletedSteps = executionIncomplete && !completedSteps.length;
9079
+ if (executionIncomplete) {
9080
+ const reason = missingCompletedSteps
9081
+ ? "Executor reported completion, but step tracking shows incomplete steps with no completedSteps metadata. Resume execution to complete remaining steps before validation."
9082
+ : "Executor reported completion, but approved Plan progress still has incomplete steps. Resume execution to complete remaining steps before validation.";
9083
+ updateState({
9084
+ mode: "reviewed",
9085
+ executionSummary: summary,
9086
+ planExecutionStepIndex: undefined,
9087
+ planStepValidationIndex: undefined,
9088
+ validationReport: undefined,
9089
+ validationVerdict: undefined,
9090
+ lastValidationFailure: undefined,
9091
+ lastRepairStatus: "none",
9092
+ lastRepairAttempt: undefined,
9093
+ planProgress: progressEnabled ? mergePlanProgress({ ...progressedState, mode: "reviewed" }, settings, { lifecycleStatus: "reviewed", validationStatus: "pending", repairStatus: "none", nextAction: "continue execution" }) : state.planProgress,
9094
+ }, ctx);
9095
+ queuePlanTerminalSummary(ctx, "blocked", "Plan execution incomplete", { reason });
8837
9096
  showBlockedPlanRecoveryMenu(ctx);
8838
9097
  return { ...typedToolAck(), details: { accepted: true, status: "blocked", validationStarted: false } };
8839
9098
  }
8840
- const progressWarning = hasOpenSteps ? "Execution completed with incomplete Plan step metadata; validator must verify approved Plan coverage." : undefined;
9099
+ const progressWarning = hasOpenSteps && !planValidationBoundaryReached(progressedState, settings) ? "Execution completed with incomplete Plan step metadata; validator must verify approved Plan coverage." : undefined;
8841
9100
  const executionSummary = progressWarning ? `${summary}\n\nPlan Progress Warning: ${progressWarning}` : summary;
8842
- updateState({ mode: "executed", executionSummary, planStepValidationIndex: settings.workflow.validateAfterEachStep === true ? executedStepIndex : state.planStepValidationIndex, planExecutionStepIndex: undefined, planProgress: progressEnabled ? mergePlanProgress({ ...progressedState, mode: "executed" }, settings, { lifecycleStatus: "executing", validationStatus: progressWarning ? "unknown" : progressedState.planProgress?.validationStatus, nextAction: validationAvailable && validateAfterExecution ? "validator" : "finish workflow" }) : progressedPlanProgress }, ctx);
9101
+ updateState({ mode: "executed", executionSummary, planStepValidationIndex: nextPlanStepValidationIndex, planExecutionStepIndex: undefined, planProgress: progressEnabled ? mergePlanProgress({ ...progressedState, mode: "executed" }, settings, { lifecycleStatus: "executing", validationStatus: progressWarning ? "unknown" : progressedState.planProgress?.validationStatus, nextAction: validationAvailable && validateAfterExecution ? "validator" : "finish workflow" }) : progressedPlanProgress }, ctx);
8843
9102
  if (validationAvailable && validateAfterExecution) {
8844
9103
  setPendingWorkflowToolPhase(ctx, "Validation", "typed execution accepted");
8845
9104
  }
@@ -8905,6 +9164,21 @@ export default function workflowModes(pi: ExtensionAPI): void {
8905
9164
  return { ...typedToolAck(false), details: { accepted: false, verdict, reason: `workflow_validation_result is only accepted during validation phases; current mode is ${state.mode}.` }, isError: true };
8906
9165
  }
8907
9166
  if (planValidationMode) {
9167
+ const settings = loadWorkflowSettings(ctx.cwd);
9168
+ if (!planValidationBoundaryReached(state, settings)) {
9169
+ const reason = "Approved Plan execution is incomplete. Validation results are not accepted until the configured validation boundary is reached.";
9170
+ updateState({
9171
+ mode: "reviewed",
9172
+ validationReport: undefined,
9173
+ validationVerdict: undefined,
9174
+ lastValidationFailure: undefined,
9175
+ lastRepairStatus: "none",
9176
+ lastRepairAttempt: undefined,
9177
+ planStepValidationIndex: undefined,
9178
+ planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: "reviewed" }, settings, { lifecycleStatus: "reviewed", validationStatus: "pending", repairStatus: "none", nextAction: "continue execution" }, state.approvedPlan) : state.planProgress,
9179
+ }, ctx);
9180
+ return { ...typedToolAck(false), details: { accepted: false, verdict, reason }, isError: true };
9181
+ }
8908
9182
  planForcedSubagentPreflightReconcile(ctx, "Validation");
8909
9183
  const forcedValidationBlock = forcedSubagentUsageSatisfied(ctx, "Validation");
8910
9184
  if (forcedValidationBlock) {
@@ -8934,6 +9208,22 @@ export default function workflowModes(pi: ExtensionAPI): void {
8934
9208
  showBlockedPlanRecoveryMenu(ctx);
8935
9209
  return { ...typedToolAck(), details: { accepted: true, status } };
8936
9210
  }
9211
+ if (!planValidationBoundaryReached(state, settings)) {
9212
+ updateState({
9213
+ mode: "reviewed",
9214
+ executionSummary: `${state.executionSummary ?? ""}\n\nRepair summary received before validation boundary (not revalidated):\n${summary}`.trim(),
9215
+ lastWorkflowHandoff: undefined,
9216
+ lastRepairStatus: "none",
9217
+ lastRepairAttempt: undefined,
9218
+ validationReport: undefined,
9219
+ validationVerdict: undefined,
9220
+ lastValidationFailure: undefined,
9221
+ planStepValidationIndex: undefined,
9222
+ planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: "reviewed" }, settings, { lifecycleStatus: "reviewed", validationStatus: "pending", repairStatus: "none", nextAction: "continue execution" }, state.approvedPlan) : state.planProgress,
9223
+ }, ctx);
9224
+ show(pi, "# Plan Repair Ignored\n\nApproved Plan execution is incomplete. Repair/revalidation is not available until the configured validation boundary is reached.\n\nUse /plan continue.");
9225
+ return { ...typedToolAck(), details: { accepted: true, status, ignored: true } };
9226
+ }
8937
9227
  updateState({ mode: "revalidating", executionSummary: `${state.executionSummary ?? ""}\n\nRepair summary:\n${summary}`.trim(), lastRepairStatus: "completed", lastRepairAttempt: compact(summary, 1200), planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: "revalidating", lastRepairStatus: "completed" }, settings, { lifecycleStatus: "revalidating", validationStatus: "running", repairStatus: "completed", nextAction: "validation result" }) : state.planProgress }, ctx);
8938
9228
  setPendingWorkflowToolPhase(ctx, "Validation", "typed repair completed");
8939
9229
  deferWorkflowAction(pi, "begin typed revalidation after repair", async () => { await beginValidation(ctx, true, true); }, { timeoutMs: workflowDeferredPhaseTimeoutMs(ctx, "Validation"), onFailure: (error) => recoverPlanTransientHandoffFailure(ctx, "repair_revalidation", transientFailureReason("Typed repair completed but revalidation handoff failed", error instanceof Error ? error.message : String(error)), { phase: "Validation", completedRepair: true }) });
@@ -9012,6 +9302,7 @@ export default function workflowModes(pi: ExtensionAPI): void {
9012
9302
  if (!passed) {
9013
9303
  await handleMissionReviewFailure(ctx, reviewed, missionReview.verdict, missionReview.report);
9014
9304
  } else {
9305
+ setReviewHandoffSuppression(ctx, "mission_typed_review_to_approval");
9015
9306
  deferWorkflowAction(pi, "resume mission approval after reviewer pass", async () => {
9016
9307
  const latest = loadMissionState(reviewed.id) ?? activeMission ?? reviewed;
9017
9308
  const block = missionReviewContinuationBlock(latest);
@@ -10347,6 +10638,29 @@ ${reportExcerpt(validation, 2400)}
10347
10638
  : kind === "run" ? "Run /mission resume or /mission continue."
10348
10639
  : kind === "planning" ? "Run /mission resume or rerun /mission plan."
10349
10640
  : "Run /mission review, /mission approve, or /mission resume.";
10641
+ if (kind === "run") {
10642
+ const activeIndex = mission.currentMilestoneIndex ?? 0;
10643
+ const runningMilestones = mission.milestones.map((milestone, index) => index === activeIndex ? { ...milestone, status: "active" as const } : milestone);
10644
+ const runNextAction = "Wait for mission_milestone_result from the active executor, or run /mission continue to re-arm execution if no result arrives.";
10645
+ const running = saveActiveMission({
10646
+ ...mission,
10647
+ status: "running",
10648
+ milestones: runningMilestones,
10649
+ currentValidationRetry: retry,
10650
+ missionValidationRetryCount: missionRetry,
10651
+ lastRepairStatus: repairStatus,
10652
+ lastStopReason: retryableInterruptionText(reason),
10653
+ lastBlockReason: "",
10654
+ nextAction: runNextAction,
10655
+ lastSummary: "Mission run handoff interrupted by a transient failure; execution boundary preserved for typed milestone handoff.",
10656
+ });
10657
+ checkpointMission(running, `Mission ${kind} interrupted by transient handoff failure; execution boundary preserved. ${compact(reason, 500)}`, runNextAction, running.milestones[activeIndex]?.id, { validationResult: running.lastValidationResult });
10658
+ updateState({ mode: "mission_running", activeMissionId: running.id, task: running.goal, originalTask: running.goal, approvedPlan: running.planText }, ctx);
10659
+ activeMission = running;
10660
+ armWorkflowToolsForPhase(ctx, "Execution", "recover mission run transient handoff failure");
10661
+ show(pi, `# Mission Workflow Interrupted\n\nA transient connection or model handoff failure interrupted Mission execution handoff. This is not a semantic mission block; the active milestone execution boundary was preserved so a valid mission_milestone_result can still be accepted.\n\nPhase: ${workflowDisplayText(kind)}\nReason: ${workflowDisplayText(reason)}\n\nNext: ${workflowDisplayText(runNextAction)}.`);
10662
+ return;
10663
+ }
10350
10664
  const paused = saveActiveMission({
10351
10665
  ...mission,
10352
10666
  status: "paused",
@@ -10377,7 +10691,7 @@ ${reportExcerpt(validation, 2400)}
10377
10691
  initialDelayMs: 2000,
10378
10692
  requireIdle: true,
10379
10693
  isIdle: () => ctx.isIdle(),
10380
- onBeforeSend: (customType) => armWorkflowToolsForPhase(ctx, phase, `queued turn before send: ${customType}`),
10694
+ onBeforeSend: (customType) => armAndVerifyWorkflowToolsForPhase(ctx, phase, `queued turn before send: ${customType}`),
10381
10695
  });
10382
10696
 
10383
10697
  const buildQueuedPlanningRecovery = (ctx: ExtensionContext, recover: (reason: string) => void): WorkflowQueuedTurnOptions =>
@@ -10562,7 +10876,15 @@ ${renderMissionProgress(mission, settings)}
10562
10876
  return true;
10563
10877
  }
10564
10878
 
10565
- function resolveActiveMissionForResume(): { mission?: MissionState; candidates: MissionState[]; reason?: string } {
10879
+ function missionMatchesCurrentProject(ctx: ExtensionContext, mission?: MissionState): boolean {
10880
+ return Boolean(mission?.cwd && mission.cwd === ctx.cwd);
10881
+ }
10882
+
10883
+ function sortMissionResumeCandidates(a: MissionState, b: MissionState): number {
10884
+ return missionResumePriority(a.status) - missionResumePriority(b.status) || b.updatedAt.localeCompare(a.updatedAt);
10885
+ }
10886
+
10887
+ function resolveActiveMissionForResume(ctx: ExtensionContext): { mission?: MissionState; candidates: MissionState[]; reason?: string; fallbackProject?: boolean } {
10566
10888
  const byState = state.activeMissionId ? loadMissionState(state.activeMissionId) : undefined;
10567
10889
  const loadedActive = activeMission && (!state.activeMissionId || activeMission.id === state.activeMissionId) ? activeMission : undefined;
10568
10890
  const latest = loadMissionState();
@@ -10576,18 +10898,24 @@ ${renderMissionProgress(mission, settings)}
10576
10898
  add(byState);
10577
10899
  [loadedActive, latest, ...all]
10578
10900
  .filter((mission): mission is MissionState => missionIsResumeCandidate(mission) && mission.id !== byState.id)
10579
- .sort((a, b) => missionResumePriority(a.status) - missionResumePriority(b.status) || b.updatedAt.localeCompare(a.updatedAt))
10901
+ .sort((a, b) => Number(missionMatchesCurrentProject(ctx, b)) - Number(missionMatchesCurrentProject(ctx, a)) || sortMissionResumeCandidates(a, b))
10580
10902
  .forEach(add);
10581
10903
  const inactiveReason = missionIsResumeCandidate(byState) ? undefined : `Current mission is ${(byState as MissionState).status}.`;
10582
10904
  return { mission: byState, candidates: ordered, reason: inactiveReason };
10583
10905
  }
10584
10906
 
10585
- [loadedActive, latest, ...all]
10586
- .filter(missionIsResumeCandidate)
10587
- .sort((a, b) => missionResumePriority(a.status) - missionResumePriority(b.status) || b.updatedAt.localeCompare(a.updatedAt))
10588
- .forEach(add);
10907
+ const currentProject = [loadedActive, latest, ...all]
10908
+ .filter((mission): mission is MissionState => missionIsResumeCandidate(mission) && missionMatchesCurrentProject(ctx, mission))
10909
+ .sort(sortMissionResumeCandidates);
10910
+ const otherProject = [loadedActive, latest, ...all]
10911
+ .filter((mission): mission is MissionState => missionIsResumeCandidate(mission) && !missionMatchesCurrentProject(ctx, mission))
10912
+ .sort(sortMissionResumeCandidates);
10913
+
10914
+ currentProject.forEach(add);
10915
+ otherProject.forEach(add);
10589
10916
 
10590
- if (ordered.length > 0) return { mission: ordered[0], candidates: ordered };
10917
+ if (currentProject.length > 0) return { mission: currentProject[0], candidates: ordered };
10918
+ if (otherProject.length > 0) return { mission: otherProject[0], candidates: ordered, fallbackProject: true, reason: `No resumable mission found for current project ${basename(ctx.cwd)}; showing saved missions from other projects.` };
10591
10919
 
10592
10920
  add(loadedActive);
10593
10921
  add(latest);
@@ -10600,7 +10928,7 @@ ${renderMissionProgress(mission, settings)}
10600
10928
  const done = mission.milestones.filter((m) => m.status === "completed" || m.status === "skipped").length;
10601
10929
  const percent = total ? Math.round((done / total) * 100) : 0;
10602
10930
  const milestone = total ? `M${Math.min(mission.currentMilestoneIndex + 1, total)}/${total}` : "M0/0";
10603
- return `${mission.id} | ${mission.status} | ${milestone} | ${percent}% | ${compact(mission.goal, 80).replace(/\n/g, " ")}`;
10931
+ return `${mission.id} | ${mission.projectLabel ?? mission.cwd ?? "unknown project"} | ${mission.status} | ${milestone} | ${percent}% | ${compact(mission.goal, 80).replace(/\n/g, " ")}`;
10604
10932
  }
10605
10933
 
10606
10934
  async function chooseMissionFromHistory(ctx: ExtensionContext, missions: MissionState[], title = "Choose a mission"): Promise<MissionState | undefined> {
@@ -10614,12 +10942,15 @@ ${renderMissionProgress(mission, settings)}
10614
10942
  return missions.find((mission) => mission.id === id);
10615
10943
  }
10616
10944
 
10617
- async function chooseResumeMission(ctx: ExtensionContext, resolved: { mission?: MissionState; candidates: MissionState[]; reason?: string }, action: "resume" | "continue" | "next" | "retry" | "repair" | "revalidate" = "resume"): Promise<MissionState | undefined> {
10618
- if (resolved.candidates.length <= 1 || !ctx.hasUI) return resolved.mission;
10945
+ async function chooseResumeMission(ctx: ExtensionContext, resolved: { mission?: MissionState; candidates: MissionState[]; reason?: string; fallbackProject?: boolean }, action: "resume" | "continue" | "next" | "retry" | "repair" | "revalidate" = "resume"): Promise<MissionState | undefined> {
10946
+ if (resolved.fallbackProject && !ctx.hasUI) return undefined;
10947
+ if (!resolved.fallbackProject && (resolved.candidates.length <= 1 || !ctx.hasUI)) return resolved.mission;
10619
10948
  const title = action === "resume" ? "Mission Resume" : `Mission ${action[0].toUpperCase()}${action.slice(1)}`;
10620
10949
  const verb = action === "resume" ? "use" : action;
10621
10950
  const choices = resolved.candidates.map(missionResumeChoiceLabel);
10622
- const intro = action === "resume"
10951
+ const intro = resolved.fallbackProject
10952
+ ? (resolved.reason ?? "Only saved missions from other projects were found. Choose explicitly before continuing.")
10953
+ : action === "resume"
10623
10954
  ? "Choose which mission to use."
10624
10955
  : `I found more than one mission that can ${action}. Choose which one to use.`;
10625
10956
  show(pi, `# ${title}\n\n${intro}\n\n${choices.map((choice, i) => `${i + 1}. ${choice}`).join("\n")}`);
@@ -10670,6 +11001,8 @@ ${renderMissionProgress(mission, settings)}
10670
11001
  if (!route) return { systemPrompt: `${systemPrompt}\n\nPI WORKFLOW MISSION VALIDATION BLOCKED: validator model is disabled, unavailable, or not configured. Do not execute Mission work. Report that Mission validation must be re-entered after validator routing is fixed.` };
10671
11002
  pi.setActiveTools(validationToolsFor(settings));
10672
11003
  updateState({ mode, activeMissionId: mission.id, modelsUsed: { ...(state.modelsUsed ?? {}), validator: modelLabel(route) } }, ctx);
11004
+ const surfaceBlock = verifyWorkflowToolSurface(ctx, "Validation", "mission active validation gate", mode);
11005
+ if (surfaceBlock) return { systemPrompt: `${systemPrompt}\n\nPI WORKFLOW MISSION VALIDATION BLOCKED: ${surfaceBlock}\n\nDo not validate the mission. Run /mission revalidate after the tool surface is repaired.` };
10673
11006
  return { systemPrompt: `${systemPrompt}\n\n${missionValidationPrompt(mission, settings, state.executionSummary, phasePreflightBlocks.Validation)}` };
10674
11007
  }
10675
11008
  if (mode === "mission_final_validating") {
@@ -10677,13 +11010,17 @@ ${renderMissionProgress(mission, settings)}
10677
11010
  if (!route) return { systemPrompt: `${systemPrompt}\n\nPI WORKFLOW MISSION FINAL VALIDATION BLOCKED: validator model is disabled, unavailable, or not configured. Do not execute Mission work. Report that final validation must be re-entered after validator routing is fixed.` };
10678
11011
  pi.setActiveTools(validationToolsFor(settings));
10679
11012
  updateState({ mode, activeMissionId: mission.id, modelsUsed: { ...(state.modelsUsed ?? {}), validator: modelLabel(route) } }, ctx);
11013
+ const surfaceBlock = verifyWorkflowToolSurface(ctx, "Validation", "mission active final validation gate", mode);
11014
+ if (surfaceBlock) return { systemPrompt: `${systemPrompt}\n\nPI WORKFLOW MISSION FINAL VALIDATION BLOCKED: ${surfaceBlock}\n\nDo not validate the mission. Run /mission revalidate after the tool surface is repaired.` };
10680
11015
  return { systemPrompt: `${systemPrompt}\n\n${missionFinalValidationPrompt(mission, settings, state.executionSummary, phasePreflightBlocks.Validation)}` };
10681
11016
  }
10682
11017
  if (mode === "mission_repairing") {
10683
11018
  const route = await applyMissionModelForRole(pi, ctx, "executor", { cwd: ctx.cwd });
10684
11019
  if (!route) return { systemPrompt: `${systemPrompt}\n\nPI WORKFLOW MISSION REPAIR BLOCKED: executor model is disabled, unavailable, or not configured. Do not validate or continue Mission execution. Report that Mission repair must be re-entered after executor routing is fixed.` };
10685
- pi.setActiveTools(executionToolsFor(settings));
11020
+ pi.setActiveTools(missionRepairToolsFor(settings));
10686
11021
  updateState({ mode, activeMissionId: mission.id, modelsUsed: { ...(state.modelsUsed ?? {}), executor: modelLabel(route) } }, ctx);
11022
+ const surfaceBlock = verifyWorkflowToolSurface(ctx, "Repair", "mission active repair gate", mode);
11023
+ if (surfaceBlock) return { systemPrompt: `${systemPrompt}\n\nPI WORKFLOW MISSION REPAIR BLOCKED: ${surfaceBlock}\n\nDo not repair the mission. Run /mission repair or /mission resume after the tool surface is repaired.` };
10687
11024
  return { systemPrompt: `${systemPrompt}\n\n${missionRepairPrompt(mission, settings, phasePreflightBlocks.Repair)}` };
10688
11025
  }
10689
11026
  return undefined;
@@ -10865,9 +11202,9 @@ ${renderMissionProgress(mission, settings)}
10865
11202
  }
10866
11203
 
10867
11204
  async function handleMissionResume(ctx: ExtensionContext) {
10868
- const resolved = resolveActiveMissionForResume();
11205
+ const resolved = resolveActiveMissionForResume(ctx);
10869
11206
  let selected = resolved.mission;
10870
- const allMissions = listMissionStates();
11207
+ const allMissions = listMissionStates().sort((a, b) => Number(missionMatchesCurrentProject(ctx, b)) - Number(missionMatchesCurrentProject(ctx, a)) || b.updatedAt.localeCompare(a.updatedAt));
10871
11208
  if (!selected && allMissions.length > 0) selected = allMissions[0];
10872
11209
  if (!selected) return show(pi, `# Mission Resume
10873
11210
 
@@ -10897,6 +11234,7 @@ ${renderMissionStatus(mission)}`);
10897
11234
  return show(pi, `# ${selectedFromHistory ? "Selected Mission" : "Mission Resume"}
10898
11235
 
10899
11236
  Mission: ${mission.id}
11237
+ Project: ${mission.projectLabel ?? mission.cwd ?? "unknown project"}
10900
11238
  Status: ${missionPhaseFromStatus(mission.status)}
10901
11239
  Current Milestone: ${mission.milestones.length ? `M${Math.min(mission.currentMilestoneIndex + 1, mission.milestones.length)}/${mission.milestones.length}` : "M0/0"}
10902
11240
  Resume Available: ${resume.available ? "yes" : "no"}
@@ -10986,7 +11324,7 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
10986
11324
  await beginMissionPlanning(ctx, activeMission);
10987
11325
  }
10988
11326
 
10989
- async function beginMissionPlanning(ctx: ExtensionContext, mission: MissionState, options: { forceClarification?: boolean; forceReason?: string; planningPreflightSatisfied?: boolean; reviewFeedback?: string } = {}) {
11327
+ async function beginMissionPlanning(ctx: ExtensionContext, mission: MissionState, options: { forceClarification?: boolean; forceAdditionalClarification?: boolean; forceReason?: string; planningPreflightSatisfied?: boolean; reviewFeedback?: string } = {}) {
10990
11328
  stopStartupVisual(ctx);
10991
11329
  const settings = loadWorkflowSettings(ctx.cwd);
10992
11330
  clearTypedHandoff(ctx, "Mission planning");
@@ -11001,7 +11339,7 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
11001
11339
  approvedPlan: undefined,
11002
11340
  lastCompletedMissionSummary: undefined,
11003
11341
  }, ctx);
11004
- const planningOverride = { policy: missionSubagentPolicy(settings), workers: missionPlanningWorkerCount(settings), label: "Mission Planning" };
11342
+ const planningOverride = { policy: missionSubagentPolicy(settings), workers: missionPlanningWorkerCount(settings), label: "Mission Planning", task: mission.goal };
11005
11343
  if (!options.planningPreflightSatisfied && !beginForcedSubagentPhase(ctx, "Planning", settings, planningOverride)) {
11006
11344
  const blocked = saveActiveMission({ ...mission, status: "blocked", lastBlockReason: "Mission planning blocked by forced sub-agent policy availability gate.", nextAction: "Fix mission planning sub-agent policy blocker, then run /mission resume or /mission plan.", lastSummary: "Mission planning blocked before planner handoff." });
11007
11345
  checkpointMission(blocked, "Mission planning blocked before planner handoff.", "Fix mission planning sub-agent policy blocker, then run /mission resume or /mission plan.");
@@ -11021,7 +11359,9 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
11021
11359
  const pending = saveActiveMission({ ...mission, status: "planning", modelsUsed: { ...(mission.modelsUsed ?? {}), planner: modelLabel(route) }, lastSummary: "Mission planning requested from planner model.", nextAction: "Mission planner is active; wait for the milestone plan or stop the turn." });
11022
11360
  const hasAnswers = Boolean(pending.clarificationAnswers?.length);
11023
11361
  const answerSummary = hasAnswers ? formatAnswersForPlanner(pending.clarificationQuestions ?? [], pending.clarificationAnswers ?? []) : undefined;
11024
- const forceClarification = !hasAnswers && options.forceClarification === true;
11362
+ const forceClarification = options.forceAdditionalClarification === true
11363
+ ? options.forceClarification === true
11364
+ : !hasAnswers && options.forceClarification === true;
11025
11365
  updateState({
11026
11366
  mode: "mission_planning",
11027
11367
  activeMissionId: pending.id,
@@ -11035,7 +11375,7 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
11035
11375
  }, ctx);
11036
11376
  queueWorkflowPrompt(
11037
11377
  pi,
11038
- missionPlanPrompt(pending, settings, { forceClarification, forceReason: forceClarification ? options.forceReason : undefined, answerSummary, reviewFeedback: options.reviewFeedback }),
11378
+ missionPlanPrompt(pending, settings, { forceClarification, forceAdditionalClarification: options.forceAdditionalClarification, forceReason: forceClarification ? options.forceReason : undefined, answerSummary, reviewFeedback: options.reviewFeedback }),
11039
11379
  buildQueuedFailureRecovery(ctx, (reason) => recoverMissionTransientHandoffFailure(ctx, "planning", reason)),
11040
11380
  );
11041
11381
  }
@@ -11355,7 +11695,7 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
11355
11695
  lastSummary: `Running approved milestone ${milestone.id}.`,
11356
11696
  });
11357
11697
  checkpointMission(running, `Starting approved milestone ${milestone.id}: ${milestone.title}.`, "Execute current mission milestone, then run mission validation.", milestone.id);
11358
- pi.setActiveTools(executionToolsFor(settings));
11698
+ pi.setActiveTools(missionExecutionToolsFor(settings));
11359
11699
  updateState({ mode: "mission_running", activeMissionId: running.id, task: running.goal, originalTask: running.goal, approvedPlan: missionRunPlan(running), draftPlan: undefined, executionSummary: undefined, validationReport: undefined, validationVerdict: undefined, reviewerReport: undefined, modelsUsed: { ...(state.modelsUsed ?? {}), planner: running.modelsUsed?.planner }, missionTokensUsed: 0 }, ctx);
11360
11700
  if (!auto) show(pi, `# MISSION MODE ACTIVE\n\n# Mission Run Started\n\nMission ID: ${running.id}\nStatus: executing\nMilestone: ${milestone.id} — ${milestone.title}\nAutonomy: ${workflowDisplayValue(running.autonomy)}\n\n${renderMissionProgress(running, settings)}`);
11361
11701
  if (!beginForcedSubagentPhase(ctx, "Execution", settings)) {
@@ -11485,8 +11825,9 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
11485
11825
 
11486
11826
  const syncSubagentPhaseUsage = (phase: SubagentPhase): void => {
11487
11827
  const evidence = successfulSubagentEvidenceByPhase[phase];
11828
+ const distinctNames = new Set(Array.from(evidence.values()));
11488
11829
  subagentUsageByPhase[phase] = evidence.size;
11489
- subagentNamesByPhase[phase] = new Set(Array.from(evidence.values()));
11830
+ subagentNamesByPhase[phase] = distinctNames;
11490
11831
  };
11491
11832
 
11492
11833
  const recordSuccessfulSubagentEvidence = (phase: SubagentPhase, observations: SubagentNameObservation[]): void => {
@@ -11513,6 +11854,17 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
11513
11854
  return undefined;
11514
11855
  };
11515
11856
 
11857
+ const forcedSubagentGuardLabel = (phase: SubagentPhase): string => {
11858
+ if (state.mode === "standard") return `Standard ${phase}`;
11859
+ if (state.mode === "mission_planning" && phase === "Planning") return "Mission Planning";
11860
+ if (state.mode === "mission_running" && phase === "Execution") return "Mission Execution";
11861
+ if (state.mode === "mission_repairing" && phase === "Repair") return "Mission Repair";
11862
+ if (state.mode === "mission_plan_ready" && phase === "Review") return "Mission Review";
11863
+ if ((state.mode === "mission_validating" || state.mode === "mission_revalidating") && phase === "Validation") return "Mission Validation";
11864
+ if (state.mode === "mission_final_validating" && phase === "Validation") return "Mission Final Validation";
11865
+ return phase;
11866
+ };
11867
+
11516
11868
  const resetSubagentPhaseUsage = (phase: SubagentPhase) => {
11517
11869
  successfulSubagentEvidenceByPhase[phase].clear();
11518
11870
  syncSubagentPhaseUsage(phase);
@@ -11533,34 +11885,128 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
11533
11885
  syncSubagentPhaseUsage(phase);
11534
11886
  };
11535
11887
 
11536
- const priorSubagentUsageSatisfiesForced = (phase: SubagentPhase, snapshot: { count: number; names: Set<string> }, settings: ReturnType<typeof loadWorkflowSettings>, override?: { policy?: SubagentPolicyValue; workers?: { deep: number; maximum: number } }): boolean => {
11888
+ type SubagentPhasePolicyOverride = { policy?: SubagentPolicyValue; workers?: { deep: number; maximum: number }; label?: string; task?: string; kind?: WorkflowState["standardWorkKind"] };
11889
+
11890
+ const recordSubagentPolicyDecision = (ctx: ExtensionContext, decision: Omit<SubagentPolicyDecision, "createdAt">): SubagentPolicyDecision => {
11891
+ const fullDecision: SubagentPolicyDecision = { ...decision, createdAt: new Date().toISOString() };
11892
+ updateState({
11893
+ subagentPolicyDecisions: {
11894
+ ...(state.subagentPolicyDecisions ?? {}),
11895
+ [decision.phase]: fullDecision,
11896
+ },
11897
+ }, ctx);
11898
+ return fullDecision;
11899
+ };
11900
+
11901
+ const phasePolicyForCurrentMode = (settings: ReturnType<typeof loadWorkflowSettings>, phase: SubagentPhase): SubagentPolicyValue => {
11902
+ return state.mode === "standard" ? standardPhasePolicy(settings, phase) : phasePolicy(settings, phase);
11903
+ };
11904
+
11905
+ const workersForCurrentMode = (settings: ReturnType<typeof loadWorkflowSettings>, phase: SubagentPhase): { deep: number; maximum: number } => {
11906
+ return state.mode === "standard" ? standardWorkerCount(settings, phase) : workerCount(settings, phase);
11907
+ };
11908
+
11909
+ const forcedTrivialExemptionDecision = (ctx: ExtensionContext, phase: SubagentPhase, policy: SubagentPolicyValue, workers: { deep: number; maximum: number }, task?: string, kind?: WorkflowState["standardWorkKind"]): SubagentPolicyDecision | undefined => {
11910
+ if (!subagentPolicyRequiresRequiredEvidence(policy)) return undefined;
11911
+ const reason = trivialSubagentSkipReason(phase, task, kind);
11912
+ if (!reason) return undefined;
11913
+ return recordSubagentPolicyDecision(ctx, {
11914
+ phase,
11915
+ policy,
11916
+ outcome: "exempt_trivial",
11917
+ reason,
11918
+ task,
11919
+ required: workerTargetForPolicy(policy, workers),
11920
+ observed: 0,
11921
+ background: false,
11922
+ });
11923
+ };
11924
+
11925
+ const rememberAdvisorySubagentDecision = (ctx: ExtensionContext, phase: SubagentPhase, policy: SubagentPolicyValue, outcome: "auto_delegate" | "auto_skip_trivial" | "auto_skip_no_useful_parallel_work" | "unavailable", reason: string, task?: string, workers = workerCount(loadWorkflowSettings(ctx.cwd), phase)): void => {
11926
+ if (!subagentPolicyNeedsInternalDecision(policy)) return;
11927
+ recordSubagentPolicyDecision(ctx, {
11928
+ phase,
11929
+ policy,
11930
+ outcome,
11931
+ reason,
11932
+ task,
11933
+ required: workerTargetForPolicy(policy, workers),
11934
+ observed: subagentUsageByPhase[phase] ?? 0,
11935
+ background: false,
11936
+ });
11937
+ };
11938
+
11939
+ const advisoryWorkerTargetForPolicy = (policy: SubagentPolicyValue, workers: { deep: number; maximum: number }): number => {
11940
+ if (policy === "auto") return Math.max(1, workers.deep);
11941
+ return Math.max(1, workerTargetForPolicy(policy, workers));
11942
+ };
11943
+
11944
+ const advisorySubagentUnavailableReason = (ctx: ExtensionContext, phase: SubagentPhase, policy: SubagentPolicyValue, workers: { deep: number; maximum: number }, label: string): string | undefined => {
11945
+ if (!subagentPolicyNeedsInternalDecision(policy)) return undefined;
11946
+ const target = advisoryWorkerTargetForPolicy(policy, workers);
11947
+ if (target <= 0) return undefined;
11948
+ const settings = loadWorkflowSettings(ctx.cwd);
11949
+ if (settings.subagents.enabled === false) return "subagents.enabled=false";
11950
+ if (!phaseAutoUseAllowed(settings, phase)) return `subagents.autoUseDuring${phase}=false`;
11951
+ if (!phaseParallelAllowed(settings, phase)) return `subagents.allowParallel${phase}=false`;
11952
+ const candidates = chooseForcedSubagents(phase, Math.min(target, 8), label, listEffectiveAgents(ctx.cwd));
11953
+ if (candidates.length === 0) return `no suitable ${phase.toLowerCase()} sub-agent workers are available`;
11954
+ return undefined;
11955
+ };
11956
+
11957
+ const priorSubagentUsageSatisfiesForced = (phase: SubagentPhase, snapshot: { count: number; names: Set<string> }, settings: ReturnType<typeof loadWorkflowSettings>, override?: SubagentPhasePolicyOverride): boolean => {
11537
11958
  const policy = override?.policy ?? phasePolicy(settings, phase);
11538
- if (policy !== "forced") return false;
11539
- const required = workerTargetForPolicy("forced", override?.workers ?? workerCount(settings, phase));
11959
+ if (!subagentPolicyRequiresRequiredEvidence(policy)) return false;
11960
+ const required = workerTargetForPolicy(policy, override?.workers ?? workerCount(settings, phase));
11540
11961
  return snapshot.count >= required;
11541
11962
  };
11542
11963
 
11543
- const beginForcedSubagentPhase = (ctx: ExtensionContext, phase: SubagentPhase, settings: ReturnType<typeof loadWorkflowSettings>, override?: { policy?: SubagentPolicyValue; workers?: { deep: number; maximum: number }; label?: string }): boolean => {
11964
+ const beginForcedSubagentPhase = (ctx: ExtensionContext, phase: SubagentPhase, settings: ReturnType<typeof loadWorkflowSettings>, override?: SubagentPhasePolicyOverride): boolean => {
11544
11965
  resetSubagentPhaseUsage(phase);
11545
11966
  phasePreflightBlocks[phase] = undefined;
11546
- const reason = forcedSubagentUnavailableReason(settings, phase, ctx.cwd, override?.policy ?? phasePolicy(settings, phase), override?.workers ?? workerCount(settings, phase));
11967
+ const policy = override?.policy ?? phasePolicy(settings, phase);
11968
+ const workers = override?.workers ?? workerCount(settings, phase);
11969
+ const task = override?.task ?? state.task ?? state.originalTask;
11970
+ if (subagentPolicyNeedsInternalDecision(policy)) {
11971
+ const advisory = advisorySubagentPolicyDecision(phase, policy, task, override?.kind);
11972
+ rememberAdvisorySubagentDecision(ctx, phase, policy, advisory.outcome, advisory.reason, task, workers);
11973
+ }
11974
+ const exemption = forcedTrivialExemptionDecision(ctx, phase, policy, workers, task, override?.kind);
11975
+ if (exemption) {
11976
+ phasePreflightBlocks[phase] = formatSubagentPolicyDecision(exemption);
11977
+ return true;
11978
+ }
11979
+ const reason = forcedSubagentUnavailableReason(settings, phase, ctx.cwd, policy, workers);
11547
11980
  if (!reason) return true;
11981
+ if (subagentPolicyRequiresRequiredEvidence(policy)) {
11982
+ recordSubagentPolicyDecision(ctx, {
11983
+ phase,
11984
+ policy,
11985
+ outcome: "unavailable",
11986
+ reason,
11987
+ task,
11988
+ required: workerTargetForPolicy(policy, workers),
11989
+ observed: subagentUsageByPhase[phase] ?? 0,
11990
+ background: false,
11991
+ });
11992
+ }
11548
11993
  showInternal(pi, `# Sub-Agent Policy Blocked\n\n${forcedSubagentMessage(phase, reason, override?.label)}`);
11549
11994
  return false;
11550
11995
  };
11551
11996
 
11552
- const forcedSubagentUsageSatisfied = (ctx: ExtensionContext, phase: SubagentPhase, override?: { policy?: SubagentPolicyValue; workers?: { deep: number; maximum: number }; label?: string }): string | undefined => {
11997
+ const forcedSubagentUsageSatisfied = (ctx: ExtensionContext, phase: SubagentPhase, override?: SubagentPhasePolicyOverride): string | undefined => {
11553
11998
  const settings = loadWorkflowSettings(ctx.cwd);
11554
11999
  const policy = override?.policy ?? phasePolicy(settings, phase);
11555
- if (policy !== "forced") return undefined;
11556
- const required = workerTargetForPolicy("forced", override?.workers ?? workerCount(settings, phase));
12000
+ if (!subagentPolicyRequiresRequiredEvidence(policy)) return undefined;
12001
+ if (forcedTrivialExemptionDecision(ctx, phase, policy, override?.workers ?? workerCount(settings, phase), override?.task ?? state.task ?? state.originalTask, override?.kind)) return undefined;
12002
+ const required = workerTargetForPolicy(policy, override?.workers ?? workerCount(settings, phase));
11557
12003
  const observed = subagentUsageByPhase[phase] ?? 0;
11558
12004
  if (observed >= required) return undefined;
11559
12005
  const label = override?.label ?? phase;
11560
12006
  return `${label} stopped because ${label} Policy is forced. Required ${label.toLowerCase()} sub-agent workers: ${required}. Observed: ${observed}. Validation or the next workflow phase will not start until the forced worker requirement is satisfied. Change this in Workflow Sub-agents / Workers, or rerun with the required workers.`;
11561
12007
  };
11562
12008
 
11563
- const blockIfForcedSubagentsMissing = (ctx: ExtensionContext, phase: SubagentPhase, override?: { policy?: SubagentPolicyValue; workers?: { deep: number; maximum: number }; label?: string }): boolean => {
12009
+ const blockIfForcedSubagentsMissing = (ctx: ExtensionContext, phase: SubagentPhase, override?: SubagentPhasePolicyOverride): boolean => {
11564
12010
  const reason = forcedSubagentUsageSatisfied(ctx, phase, override);
11565
12011
  if (!reason) return false;
11566
12012
  showInternal(pi, `# Sub-Agent Policy Blocked\n\n${forcedSubagentMessage(phase, reason, override?.label)}`);
@@ -11638,19 +12084,36 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
11638
12084
  return [`Phase: ${label}`, `Observed workers: ${succeeded.length}`, ...lines, ...(failed.length ? ["Failed workers:", ...failed.map((result) => `- ${result.agent} (${result.agentSource}): ${compact(workflowSubagentResultOutput(result), 500)}`)] : [])].join("\n");
11639
12085
  };
11640
12086
 
11641
- const runForcedSubagentPreflight = async (ctx: ExtensionContext, phase: SubagentPhase, settings: ReturnType<typeof loadWorkflowSettings>, context: ForcedSubagentPreflightContext = {}, override?: { policy?: SubagentPolicyValue; workers?: { deep: number; maximum: number }; label?: string }): Promise<{ ok: boolean; block?: string }> => {
12087
+ const runForcedSubagentPreflight = async (ctx: ExtensionContext, phase: SubagentPhase, settings: ReturnType<typeof loadWorkflowSettings>, context: ForcedSubagentPreflightContext = {}, override?: SubagentPhasePolicyOverride): Promise<{ ok: boolean; block?: string }> => {
11642
12088
  resetSubagentPhaseUsage(phase);
11643
12089
  phasePreflightBlocks[phase] = undefined;
11644
12090
  const policy = override?.policy ?? phasePolicy(settings, phase);
11645
12091
  const workers = override?.workers ?? workerCount(settings, phase);
11646
12092
  const label = override?.label ?? context.label ?? phase;
11647
- if (policy !== "forced") return { ok: true };
12093
+ const task = override?.task ?? context.task ?? state.task ?? state.originalTask;
12094
+ if (!subagentPolicyRequiresRequiredEvidence(policy)) return { ok: true };
12095
+ const exemption = forcedTrivialExemptionDecision(ctx, phase, policy, workers, task, override?.kind);
12096
+ if (exemption) {
12097
+ const block = formatSubagentPolicyDecision(exemption);
12098
+ phasePreflightBlocks[phase] = block;
12099
+ return { ok: true, block };
12100
+ }
11648
12101
  const reason = forcedSubagentUnavailableReason(settings, phase, ctx.cwd, policy, workers);
11649
12102
  if (reason) {
12103
+ recordSubagentPolicyDecision(ctx, {
12104
+ phase,
12105
+ policy,
12106
+ outcome: "unavailable",
12107
+ reason,
12108
+ task,
12109
+ required: workerTargetForPolicy(policy, workers),
12110
+ observed: subagentUsageByPhase[phase] ?? 0,
12111
+ background: false,
12112
+ });
11650
12113
  showInternal(pi, `# Sub-Agent Policy Blocked\n\n${forcedSubagentMessage(phase, reason, label)}`);
11651
12114
  return { ok: false };
11652
12115
  }
11653
- const required = workerTargetForPolicy("forced", workers);
12116
+ const required = workerTargetForPolicy(policy, workers);
11654
12117
  const effectiveAgents = listEffectiveAgents(ctx.cwd);
11655
12118
  const agents = chooseForcedSubagents(phase, required, label, effectiveAgents);
11656
12119
  const names = agents.map((agent) => agent.name);
@@ -11670,7 +12133,7 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
11670
12133
  return { ok: false };
11671
12134
  }
11672
12135
  }
11673
- const useBackground = settings.subagents.allowBackgroundSubagents === true && phase !== "Validation";
12136
+ const useBackground = settings.subagents.allowBackgroundSubagents === true && (phase === "Planning" || phase === "Review");
11674
12137
  const tasks: WorkflowSubagentTask[] = names.map((agent, index) => ({ agent, task: forcedSubagentTaskText(phase, agent, index, required, context), cwd: ctx.cwd, background: useBackground, workflowPhase: phase }));
11675
12138
  stopStartupVisual(ctx);
11676
12139
  const startedAt = new Date().toISOString();
@@ -11727,6 +12190,16 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
11727
12190
  }
11728
12191
  if (isBackground) {
11729
12192
  // Background execution: agents run independently, phase proceeds immediately
12193
+ recordSubagentPolicyDecision(ctx, {
12194
+ phase,
12195
+ policy,
12196
+ outcome: "required",
12197
+ reason: "required workers launched as advisory background preflight",
12198
+ task,
12199
+ required,
12200
+ observed: 0,
12201
+ background: true,
12202
+ });
11730
12203
  return { ok: true };
11731
12204
  }
11732
12205
  const succeeded = run.results.filter((result) => result.exitCode === 0);
@@ -11742,6 +12215,16 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
11742
12215
  showInternal(pi, `# Sub-Agent Policy Blocked\n\n${label} forced preflight did not satisfy required workers. Required: ${required}; succeeded: ${succeeded.length}.\n\n${block}`);
11743
12216
  return { ok: false, block };
11744
12217
  }
12218
+ recordSubagentPolicyDecision(ctx, {
12219
+ phase,
12220
+ policy,
12221
+ outcome: "required",
12222
+ reason: "required workers completed before phase continued",
12223
+ task,
12224
+ required,
12225
+ observed: succeeded.length,
12226
+ background: false,
12227
+ });
11745
12228
  return { ok: true, block };
11746
12229
  };
11747
12230
 
@@ -11749,12 +12232,15 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
11749
12232
  policy: standardPhasePolicy(settings, phase),
11750
12233
  workers: standardWorkerCount(settings, phase),
11751
12234
  label: `Standard ${phase}`,
12235
+ task: state.task ?? state.originalTask ?? "Standard Mode task",
12236
+ kind: state.standardWorkKind,
11752
12237
  });
11753
12238
 
11754
12239
  const standardForcedSubagentSatisfied = (phase: SubagentPhase, settings: ReturnType<typeof loadWorkflowSettings>, task: string): boolean => {
11755
12240
  const override = standardForcedSubagentOverride(settings, phase);
11756
- if (override.policy !== "forced") return true;
11757
- const required = workerTargetForPolicy("forced", override.workers);
12241
+ if (!subagentPolicyRequiresRequiredEvidence(override.policy)) return true;
12242
+ if (trivialSubagentSkipReason(phase, task, override.kind)) return true;
12243
+ const required = workerTargetForPolicy(override.policy, override.workers);
11758
12244
  const record = state.standardSubagentPreflight?.[phase];
11759
12245
  if (record?.task === task && record.observed >= required) {
11760
12246
  restoreSuccessfulSubagentEvidence(phase, record.observed, new Set(record.agents), `standard:${phase}:${task}`);
@@ -11763,12 +12249,48 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
11763
12249
  return false;
11764
12250
  };
11765
12251
 
12252
+ const forcedSubagentVisibleCallBlock = (ctx: ExtensionContext, phase: SubagentPhase, required: number, observed: number, input: unknown, label: string = phase): string | undefined => {
12253
+ const remaining = Math.max(0, required - observed);
12254
+ if (remaining <= 0) return undefined;
12255
+ const callShape = `Use one parallel tasks call shaped like subagent({ workflowPhase: "${phase.toLowerCase()}", tasks: [...] }). Worker count means task entries, not unique agent names; duplicate suitable roles are valid when useful, and distinct role names are not required. Choose phase-suitable workers by task fit. Do not add filler roles. Do not use subagent chain for forced multi-worker evidence.`;
12256
+ const countText = (requested: number, suffix = "") => {
12257
+ const shortBy = Math.max(0, remaining - requested);
12258
+ const shortage = shortBy > 0 ? `; this call is short by ${shortBy}` : "";
12259
+ return `subagent blocked — forced ${label} requires ${required} total worker task${required === 1 ? "" : "s"} before substantive ${phase.toLowerCase()} work; observed ${observed}, requested ${requested}${shortage}.${suffix}`;
12260
+ };
12261
+ if (!isRecord(input)) return countText(0, " The subagent request could not be inspected.");
12262
+ const requestedPhase = parseSubagentWorkflowPhase(input.workflowPhase);
12263
+ if (requestedPhase && requestedPhase !== phase) {
12264
+ return `subagent blocked — forced ${label} requires ${phase.toLowerCase()} workers, but the call requested workflowPhase=${String(input.workflowPhase)}.`;
12265
+ }
12266
+ const observations = extractSubagentObservations(input);
12267
+ const requested = observations.length;
12268
+ const distinctNames = Array.from(new Set(observations.map((observation) => observation.name).filter(Boolean)));
12269
+ if (remaining > 1 && !Array.isArray(input.tasks)) {
12270
+ const shape = Array.isArray(input.chain) && requested >= remaining
12271
+ ? `subagent blocked — forced ${label} requires ${required} total worker task${required === 1 ? "" : "s"} before substantive ${phase.toLowerCase()} work; observed ${observed}, requested ${requested} via chain. The requested count is enough, but chain is sequential and does not satisfy forced multi-worker evidence. ${callShape}`
12272
+ : `${countText(requested)} ${callShape}`;
12273
+ return shape;
12274
+ }
12275
+ if (requested < remaining) {
12276
+ return countText(requested, "");
12277
+ }
12278
+ const availableAgents = listEffectiveAgents(ctx.cwd);
12279
+ const suitableNames = new Set(availableAgents.filter((agent) => subagentSuitableForForcedPhase(forcedAgentProfile(agent), phase, label)).map((agent) => agent.name));
12280
+ const unsuitable = distinctNames.filter((name) => !suitableNames.has(name));
12281
+ if (unsuitable.length > 0) {
12282
+ return `subagent blocked — forced ${label} requested unsuitable or unavailable worker${unsuitable.length === 1 ? "" : "s"} for ${phase.toLowerCase()}: ${unsuitable.join(", ")}.`;
12283
+ }
12284
+ return undefined;
12285
+ };
12286
+
11766
12287
  const planForcedSubagentPreflightReconcile = (ctx: ExtensionContext, phase: SubagentPhase): void => {
11767
12288
  const block = phasePreflightBlocks[phase];
11768
12289
  if (!block) return;
11769
12290
  const settings = loadWorkflowSettings(ctx.cwd);
11770
12291
  const policy = phasePolicy(settings, phase);
11771
- if (policy !== "forced") return;
12292
+ if (!subagentPolicyRequiresRequiredEvidence(policy)) return;
12293
+ if (/^Policy decision:\s*exempt_trivial\b/im.test(block)) return;
11772
12294
  const observedMatch = block.match(/Observed workers: (\d+)/);
11773
12295
  if (!observedMatch) return;
11774
12296
  const observed = parseInt(observedMatch[1], 10);
@@ -11780,9 +12302,11 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
11780
12302
  };
11781
12303
 
11782
12304
  const planForcedSubagentPreflightSatisfied = (ctx: ExtensionContext, phase: SubagentPhase, settings: ReturnType<typeof loadWorkflowSettings>): boolean => {
11783
- if (phasePolicy(settings, phase) !== "forced") return false;
12305
+ const policy = phasePolicy(settings, phase);
12306
+ if (!subagentPolicyRequiresRequiredEvidence(policy)) return false;
12307
+ if (forcedTrivialExemptionDecision(ctx, phase, policy, workerCount(settings, phase), state.task ?? state.originalTask, state.standardWorkKind)) return true;
11784
12308
  planForcedSubagentPreflightReconcile(ctx, phase);
11785
- const required = workerTargetForPolicy("forced", workerCount(settings, phase));
12309
+ const required = workerTargetForPolicy(policy, workerCount(settings, phase));
11786
12310
  return (subagentUsageByPhase[phase] ?? 0) >= required;
11787
12311
  };
11788
12312
 
@@ -12047,6 +12571,12 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
12047
12571
  standardPhaseByToolCall.delete(toolCallId);
12048
12572
  const phaseOverride = parseSubagentWorkflowPhase(args.workflowPhase);
12049
12573
  if (phaseOverride) standardPhaseByToolCall.set(toolCallId, phaseOverride);
12574
+ const phase = phaseOverride ?? phaseForWorkflowMode(state.mode);
12575
+ if (phase) {
12576
+ const settings = loadWorkflowSettings(ctx.cwd);
12577
+ const policy = phasePolicyForCurrentMode(settings, phase);
12578
+ rememberAdvisorySubagentDecision(ctx, phase, policy, "auto_delegate", "visible parent subagent call started", state.task ?? state.originalTask, workersForCurrentMode(settings, phase));
12579
+ }
12050
12580
  const observations = extractSubagentObservations(args, `top:${toolCallId}`);
12051
12581
  registerSubagentObservations(toolCallId, observations, ctx, observations.length === 0);
12052
12582
  if (observations.length === 0) renderWorkflowSubagentActivity(ctx);
@@ -12273,7 +12803,7 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
12273
12803
  repairRetryState: reviewerRepair ? state.repairRetryState : undefined,
12274
12804
  planProgress: initialPlanProgress,
12275
12805
  }, ctx);
12276
- if (!options.planningPreflightSatisfied && !beginForcedSubagentPhase(ctx, "Planning", settings)) {
12806
+ if (!options.planningPreflightSatisfied && !beginForcedSubagentPhase(ctx, "Planning", settings, { task })) {
12277
12807
  pi.setActiveTools(planToolsFor(settings));
12278
12808
  updateState({ mode: "plan_draft", activePlanId, draftPlan: "Planning blocked before the planner could run: forced planning sub-agent requirements are unavailable.", approvedPlan: undefined, lastReviewFailure: "Forced planning sub-agent requirements are unavailable.", executionSummary: undefined, validationReport: undefined, validationVerdict: undefined, reviewerReport: undefined, reviewerVerdict: undefined, currentReviewRetry: 0, workflowReviewRetryCount: 0, maxReviewRetriesPerPlan: undefined, maxReviewRetriesPerWorkflow: undefined, lastReviewAttempt: undefined, lastReviewRepairStatus: "none", reviewHistory: undefined, reviewRepairInProgress: undefined, repairRetryState: undefined, currentValidationRetry: 0, workflowValidationRetryCount: 0, lastRepairStatus: "none", planStepValidationIndex: undefined, planExecutionStepIndex: undefined, planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: "validated", draftPlan: undefined, approvedPlan: undefined }, settings, { lifecycleStatus: "blocked", nextAction: "fix planning sub-agent policy or revise planning", steps: [] }, undefined) : undefined, lastCompletedPlanSummary: undefined }, ctx);
12279
12809
  return;
@@ -12382,19 +12912,8 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
12382
12912
  }
12383
12913
  updateState({ modelsUsed: { ...(state.modelsUsed ?? {}), executor: modelLabel(route) } }, ctx);
12384
12914
  armWorkflowToolsForPhase(ctx, "Execution", "beginExecution prompt queued");
12385
- let missingExecutionTools = missingRequiredPlanExecutionTools(pi.getActiveTools(), settings);
12386
- if (missingExecutionTools.length) {
12387
- pi.setActiveTools(executionToolsFor(settings));
12388
- missingExecutionTools = missingRequiredPlanExecutionTools(pi.getActiveTools(), settings);
12389
- traceWorkflowTracking(ctx, "defensive-tool-arm", { reason: "required execution tools missing before executor queue", missingExecutionTools });
12390
- }
12391
- if (missingExecutionTools.length) {
12392
- const reason = `Required execution tools are unavailable after execution tool rearm: ${missingExecutionTools.join(", ")}. Execution was not queued.`;
12393
- const revertMode = previousMode === "reviewed" ? "reviewed" as const : "plan_approved" as const;
12394
- updateState({ mode: revertMode, planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: revertMode }, settings, { lifecycleStatus: revertMode === "reviewed" ? "reviewed" : "approved", nextAction: "repair execution tool surface" }, state.approvedPlan) : state.planProgress, lastReviewFailure: reason }, ctx);
12395
- pi.setActiveTools(webSafePlanTools(PLAN_TOOLS));
12396
- recordWorkflowInternalEvent(ctx, `Plan execution blocked because required execution tools were unavailable after rearm: ${missingExecutionTools.join(", ")}.`);
12397
- show(pi, `# Execution Blocked\n\n${reason}\n\nRun /plan continue after the execution tool surface is repaired.`);
12915
+ const executionSurfaceBlock = verifyWorkflowToolSurface(ctx, "Execution", "beginExecution prompt queued");
12916
+ if (executionSurfaceBlock) {
12398
12917
  return false;
12399
12918
  }
12400
12919
  queueAgentTurn(pi, executePrompt(executionState, settings, phasePreflightBlocks.Execution), auto ? "workflow-execute-trigger" : "workflow-execute-manual-trigger", buildQueuedPhaseRecovery(ctx, "Execution", (reason) => recoverPlanTransientHandoffFailure(ctx, "execution", reason, { phase: "Execution" })));
@@ -12476,6 +12995,21 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
12476
12995
  return true;
12477
12996
  }
12478
12997
  const settings = loadWorkflowSettings(ctx.cwd);
12998
+ if (!planValidationBoundaryReached(state, settings)) {
12999
+ const reason = "Approved Plan execution is incomplete. Continue execution and complete the remaining Plan steps before validation.";
13000
+ updateState({
13001
+ mode: "reviewed",
13002
+ validationReport: undefined,
13003
+ validationVerdict: undefined,
13004
+ lastValidationFailure: undefined,
13005
+ lastRepairStatus: "none",
13006
+ lastRepairAttempt: undefined,
13007
+ planStepValidationIndex: undefined,
13008
+ planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: "reviewed" }, settings, { lifecycleStatus: "reviewed", validationStatus: "pending", repairStatus: "none", nextAction: "continue execution" }, state.approvedPlan) : state.planProgress,
13009
+ }, ctx);
13010
+ show(pi, `# Plan Validation Refused\n\n${reason}\n\nUse /plan continue.`);
13011
+ return false;
13012
+ }
12479
13013
  const route = await applyModelForRole(pi, ctx, "validator", { requireEnabled: false, cwd: ctx.cwd });
12480
13014
  if (!route) {
12481
13015
  const reason = "Plan validator model is disabled, unavailable, or not configured.";
@@ -12656,6 +13190,20 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
12656
13190
  async function startWorkflowRepair(ctx: ExtensionContext, source: "auto" | "user" = "auto") {
12657
13191
  if (!state.approvedPlan) return show(pi, "# Plan Repair Refused\n\nNo approved plan exists.");
12658
13192
  const settings = loadWorkflowSettings(ctx.cwd);
13193
+ if (!planValidationBoundaryReached(state, settings)) {
13194
+ const reason = "Approved Plan execution is incomplete. Repair is only available after a valid validation failure.";
13195
+ updateState({
13196
+ mode: "reviewed",
13197
+ validationReport: undefined,
13198
+ validationVerdict: undefined,
13199
+ lastValidationFailure: undefined,
13200
+ lastRepairStatus: "none",
13201
+ lastRepairAttempt: undefined,
13202
+ planStepValidationIndex: undefined,
13203
+ planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: "reviewed" }, settings, { lifecycleStatus: "reviewed", validationStatus: "pending", repairStatus: "none", nextAction: "continue execution" }, state.approvedPlan) : state.planProgress,
13204
+ }, ctx);
13205
+ return show(pi, `# Plan Repair Refused\n\n${reason}\n\nUse /plan continue.`);
13206
+ }
12659
13207
  if (state.concreteRepairableIssue === false) {
12660
13208
  const reason = "No concrete repairable issue — manual verification only.";
12661
13209
  updateState({ mode: "validated", lastRepairStatus: "blocked", lastRepairAttempt: reason, lastValidationFailure: state.lastValidationFailure || state.validationReport, planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: "validated", lastRepairStatus: "blocked" }, settings, { lifecycleStatus: "blocked", repairStatus: "blocked", nextAction: "manual verification or revalidate" }) : state.planProgress }, ctx);
@@ -12749,14 +13297,43 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
12749
13297
  }
12750
13298
 
12751
13299
  async function handleWorkflowRetryCommand(ctx: ExtensionContext, action: "retry" | "repair" | "revalidate") {
13300
+ const settings = loadWorkflowSettings(ctx.cwd);
12752
13301
  if (action === "revalidate") {
12753
13302
  if (!state.approvedPlan && (state.mode === "idle" || state.mode === "cancelled" || state.mode === "awaiting_plan_input")) {
12754
13303
  const latest = latestRecoverablePlan();
12755
13304
  if (latest) recoverPlanFromHistory(ctx, latest);
12756
13305
  }
13306
+ if (state.approvedPlan && !planValidationBoundaryReached(state, settings)) {
13307
+ const reason = "Approved Plan execution is incomplete. Continue execution and complete the remaining Plan steps before validation.";
13308
+ updateState({
13309
+ mode: "reviewed",
13310
+ validationReport: undefined,
13311
+ validationVerdict: undefined,
13312
+ lastValidationFailure: undefined,
13313
+ lastRepairStatus: "none",
13314
+ lastRepairAttempt: undefined,
13315
+ planStepValidationIndex: undefined,
13316
+ planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: "reviewed" }, settings, { lifecycleStatus: "reviewed", validationStatus: "pending", repairStatus: "none", nextAction: "continue execution" }, state.approvedPlan) : state.planProgress,
13317
+ }, ctx);
13318
+ return show(pi, `# Plan Revalidate Refused\n\n${reason}\n\nUse /plan continue.`);
13319
+ }
12757
13320
  return beginValidation(ctx, true, true);
12758
13321
  }
12759
13322
  if (await handlePlanReviewRepairRecovery(ctx, action)) return;
13323
+ if (state.approvedPlan && !planValidationBoundaryReached(state, settings)) {
13324
+ const reason = "Approved Plan execution is incomplete. Repair is only available after a valid validation failure.";
13325
+ updateState({
13326
+ mode: "reviewed",
13327
+ validationReport: undefined,
13328
+ validationVerdict: undefined,
13329
+ lastValidationFailure: undefined,
13330
+ lastRepairStatus: "none",
13331
+ lastRepairAttempt: undefined,
13332
+ planStepValidationIndex: undefined,
13333
+ planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: "reviewed" }, settings, { lifecycleStatus: "reviewed", validationStatus: "pending", repairStatus: "none", nextAction: "continue execution" }, state.approvedPlan) : state.planProgress,
13334
+ }, ctx);
13335
+ return show(pi, `# Plan ${action}\n\n${reason}\n\nUse /plan continue.`);
13336
+ }
12760
13337
  if (state.mode === "executing" || state.mode === "validating" || state.mode === "repairing" || state.mode === "revalidating") return show(pi, `# Plan ${action}\n\nCannot ${action}: workflow is currently ${state.mode}. Wait for the active gate first.`);
12761
13338
  const hasValidationFailure = Boolean(state.lastValidationFailure || state.validationReport || state.validationVerdict === "FAIL" || state.validationVerdict === "PARTIAL PASS");
12762
13339
  if (!hasValidationFailure) return show(pi, `# Plan ${action}\n\nCannot ${action}: no validation failure is recorded.`);
@@ -12970,6 +13547,21 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
12970
13547
  async function resumeInterruptedPlanValidation(ctx: ExtensionContext, title: string): Promise<boolean> {
12971
13548
  const settings = loadWorkflowSettings(ctx.cwd);
12972
13549
  const revalidate = state.mode === "revalidating";
13550
+ if (!planValidationBoundaryReached(state, settings)) {
13551
+ show(pi, `# ${title}\n\nRecovered interrupted Plan validation runtime, but approved Plan execution is incomplete. Re-arming executor for the current incomplete step.`);
13552
+ updateState({
13553
+ mode: "reviewed",
13554
+ validationReport: undefined,
13555
+ validationVerdict: undefined,
13556
+ lastValidationFailure: undefined,
13557
+ lastRepairStatus: "none",
13558
+ lastRepairAttempt: undefined,
13559
+ planStepValidationIndex: undefined,
13560
+ planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: "reviewed" }, settings, { lifecycleStatus: "reviewed", validationStatus: "pending", repairStatus: "none", nextAction: "executor" }, state.approvedPlan) : state.planProgress,
13561
+ }, ctx);
13562
+ await beginExecution(ctx, true);
13563
+ return true;
13564
+ }
12973
13565
  if (!planValidationGateActive(settings)) {
12974
13566
  show(pi, `# ${title}\n\nRecovered interrupted Plan validation runtime. Validation is disabled or unavailable for this workflow.`);
12975
13567
  await completePlanWithoutValidation(ctx, "Validation skipped: validation gate is disabled for this workflow.");
@@ -13027,6 +13619,21 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
13027
13619
  restorePlanRuntimeSnapshot(ctx, snapshot);
13028
13620
  const settings = loadWorkflowSettings(ctx.cwd);
13029
13621
  const validationAvailable = planValidationGateActive(settings);
13622
+ if ((state.mode === "executed" || state.mode === "validated" || state.mode === "validating" || state.mode === "revalidating") && planExecutionIncomplete(state, settings)) {
13623
+ show(pi, `# ${title}\n\nRecovered Plan runtime from session history, but approved Plan execution is incomplete. Continuing execution from the current incomplete step.`);
13624
+ updateState({
13625
+ mode: "reviewed",
13626
+ validationReport: undefined,
13627
+ validationVerdict: undefined,
13628
+ lastValidationFailure: undefined,
13629
+ lastRepairStatus: "none",
13630
+ lastRepairAttempt: undefined,
13631
+ planStepValidationIndex: undefined,
13632
+ planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: "reviewed" }, settings, { lifecycleStatus: "reviewed", validationStatus: "pending", repairStatus: "none", nextAction: "executor" }, state.approvedPlan) : state.planProgress,
13633
+ }, ctx);
13634
+ await beginExecution(ctx, true);
13635
+ return true;
13636
+ }
13030
13637
  if (state.mode === "executed" && validationAvailable) {
13031
13638
  show(pi, `# ${title}\n\nRecovered executed Plan runtime from session history. Continuing to validation.`);
13032
13639
  await beginValidation(ctx, true, false, "immediate");
@@ -13105,7 +13712,10 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
13105
13712
  }, ctx);
13106
13713
  }
13107
13714
 
13108
- function planRecoveryGuidance(current: WorkflowState, validationAvailable: boolean, activeRuntimeStale = false): string {
13715
+ function planRecoveryGuidance(current: WorkflowState, validationAvailable: boolean, activeRuntimeStale = false, settings = loadWorkflowSettings()): string {
13716
+ if ((current.mode === "executed" || current.mode === "validated" || current.mode === "validating" || current.mode === "revalidating") && planExecutionIncomplete(current, settings)) {
13717
+ return "Approved Plan execution is incomplete. Next action: /plan continue to complete the remaining steps before validation or repair.";
13718
+ }
13109
13719
  if (current.mode === "idle" || current.mode === "cancelled" || current.mode === "awaiting_plan_input") {
13110
13720
  const latest = latestRecoverablePlan();
13111
13721
  if (latest) return `No active Plan Mode workflow is loaded. Recoverable approved plan found.\n\n${recoverablePlanDetails(latest)}\n\nNext action: /plan resume to restore it, /plan continue to run it, or /p <task> to start a new plan.`;
@@ -13128,7 +13738,11 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
13128
13738
  return `No safe automatic continuation is available for state: ${current.mode}.`;
13129
13739
  }
13130
13740
 
13131
- function resolveActivePlanForResume(): { plan?: SavedWorkflowPlan; candidates: SavedWorkflowPlan[]; source: "active" | "history" | "none"; reason?: string } {
13741
+ function planMatchesCurrentProject(ctx: ExtensionContext, plan?: SavedWorkflowPlan): boolean {
13742
+ return Boolean(plan?.projectPath && plan.projectPath === ctx.cwd);
13743
+ }
13744
+
13745
+ function resolveActivePlanForResume(ctx: ExtensionContext): { plan?: SavedWorkflowPlan; candidates: SavedWorkflowPlan[]; source: "active" | "history" | "fallback" | "none"; reason?: string } {
13132
13746
  const candidates = listWorkflowPlans()
13133
13747
  .filter((plan) => plan.finalPlan?.trim() && (plan.approvalStatus === "approved" || plan.approvalStatus === "revised"))
13134
13748
  .sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? ""));
@@ -13137,10 +13751,17 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
13137
13751
  const add = (plan?: SavedWorkflowPlan) => {
13138
13752
  if (plan?.finalPlan?.trim() && (plan.approvalStatus === "approved" || plan.approvalStatus === "revised") && !ordered.some((p) => p.id === plan.id)) ordered.push(plan);
13139
13753
  };
13140
- add(latest);
13141
- candidates.forEach(add);
13754
+ const currentProject = [latest, ...candidates]
13755
+ .filter((plan): plan is SavedWorkflowPlan => Boolean(plan?.finalPlan?.trim()) && (plan.approvalStatus === "approved" || plan.approvalStatus === "revised") && planMatchesCurrentProject(ctx, plan))
13756
+ .sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? ""));
13757
+ const otherProject = [latest, ...candidates]
13758
+ .filter((plan): plan is SavedWorkflowPlan => Boolean(plan?.finalPlan?.trim()) && (plan.approvalStatus === "approved" || plan.approvalStatus === "revised") && !planMatchesCurrentProject(ctx, plan))
13759
+ .sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? ""));
13760
+ currentProject.forEach(add);
13761
+ otherProject.forEach(add);
13142
13762
  if (state.approvedPlan?.trim() && !isMissionWorkflowMode(state)) return { candidates: ordered, source: "active", reason: "Active approved plan is already loaded." };
13143
- if (ordered.length > 0) return { plan: ordered[0], candidates: ordered, source: "history" };
13763
+ if (currentProject.length > 0) return { plan: currentProject[0], candidates: ordered, source: "history" };
13764
+ if (otherProject.length > 0) return { plan: otherProject[0], candidates: ordered, source: "fallback", reason: `No recoverable Plan Mode workflow found for current project ${basename(ctx.cwd)}; showing saved plans from other projects.` };
13144
13765
  return { candidates: [], source: "none", reason: "No active or recoverable approved Plan Mode workflow found." };
13145
13766
  }
13146
13767
 
@@ -13194,7 +13815,7 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
13194
13815
  }
13195
13816
 
13196
13817
  function planResumeChoiceLabel(plan: SavedWorkflowPlan): string {
13197
- return `${plan.id} | ${plan.approvalStatus} | ${plan.planningMode} | ${compact(plan.originalTask || "Saved plan", 80).replace(/\n/g, " ")}`;
13818
+ return `${plan.id} | ${plan.projectLabel ?? plan.projectPath ?? "unknown project"} | ${plan.approvalStatus} | ${plan.planningMode} | ${compact(plan.originalTask || "Saved plan", 80).replace(/\n/g, " ")}`;
13198
13819
  }
13199
13820
 
13200
13821
  function planResumeCanContinueCurrent(current: WorkflowState): boolean {
@@ -13222,12 +13843,15 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
13222
13843
  return selectable.find((plan) => plan.id === id);
13223
13844
  }
13224
13845
 
13225
- async function choosePlanForContinueWhenNoCurrentPlan(ctx: ExtensionContext, resolved: { plan?: SavedWorkflowPlan; candidates: SavedWorkflowPlan[]; source: "active" | "history" | "none"; reason?: string }, action: "resume" | "continue" = "resume"): Promise<SavedWorkflowPlan | undefined> {
13846
+ async function choosePlanForContinueWhenNoCurrentPlan(ctx: ExtensionContext, resolved: { plan?: SavedWorkflowPlan; candidates: SavedWorkflowPlan[]; source: "active" | "history" | "fallback" | "none"; reason?: string }, action: "resume" | "continue" = "resume"): Promise<SavedWorkflowPlan | undefined> {
13226
13847
  if (resolved.source === "active") return undefined;
13227
- if (resolved.candidates.length <= 1 || !ctx.hasUI) return resolved.plan;
13848
+ if (resolved.source === "fallback" && !ctx.hasUI) return undefined;
13849
+ if (resolved.source !== "fallback" && (resolved.candidates.length <= 1 || !ctx.hasUI)) return resolved.plan;
13228
13850
  const title = action === "resume" ? "Plan Resume" : "Plan Continue";
13229
13851
  const choices = resolved.candidates.map(planResumeChoiceLabel);
13230
- const intro = action === "resume"
13852
+ const intro = resolved.source === "fallback"
13853
+ ? (resolved.reason ?? "Only saved plans from other projects were found. Choose explicitly before continuing.")
13854
+ : action === "resume"
13231
13855
  ? "Choose which plan to use."
13232
13856
  : "I found more than one saved plan. Choose which one to continue.";
13233
13857
  show(pi, `# ${title}\n\n${intro}\n\n${choices.map((choice, i) => `${i + 1}. ${choice}`).join("\n")}`);
@@ -13242,10 +13866,10 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
13242
13866
  const validationAvailable = planValidationGateActive(settings);
13243
13867
  const currentAdvancedSnapshot = findAdvancedPlanRuntimeSnapshotForCurrentState(ctx);
13244
13868
  if (currentAdvancedSnapshot) restorePlanRuntimeSnapshotForResume(ctx, currentAdvancedSnapshot);
13245
- const resolved = resolveActivePlanForResume();
13246
- const allPlans = listWorkflowPlans().filter((plan) => plan.finalPlan?.trim());
13869
+ const resolved = resolveActivePlanForResume(ctx);
13870
+ const allPlans = listWorkflowPlans().filter((plan) => plan.finalPlan?.trim()).sort((a, b) => Number(planMatchesCurrentProject(ctx, b)) - Number(planMatchesCurrentProject(ctx, a)) || (b.timestamp ?? "").localeCompare(a.timestamp ?? ""));
13247
13871
  const activeRuntimeStale = planActiveRuntimeIsRecoverablyStale(ctx);
13248
- if (resolved.source === "none" && allPlans.length === 0) return show(pi, `# Plan Resume\n\n${resolved.reason ?? planRecoveryGuidance(state, validationAvailable, activeRuntimeStale)}\n\nUse /workflow plans list to see saved plans.\n\n${renderWorkflowStatus(state, pi.getActiveTools(), ctx.cwd)}`);
13872
+ if (resolved.source === "none" && allPlans.length === 0) return show(pi, `# Plan Resume\n\n${resolved.reason ?? planRecoveryGuidance(state, validationAvailable, activeRuntimeStale, settings)}\n\nUse /workflow plans list to see saved plans.\n\n${renderWorkflowStatus(state, pi.getActiveTools(), ctx.cwd)}`);
13249
13873
  let selected = resolved.plan;
13250
13874
  if (resolved.source === "history" && selected) {
13251
13875
  const advancedSnapshot = findAdvancedPlanRuntimeSnapshot(ctx, selected);
@@ -13265,10 +13889,16 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
13265
13889
  // Secondary surface for a saved plan chosen through "Choose Another Plan".
13266
13890
  const showSelectedSavedPlanResumeMenu = async (plan: SavedWorkflowPlan): Promise<void> => {
13267
13891
  const canContinue = plan.approvalStatus === "approved" || plan.approvalStatus === "revised";
13892
+ const canRevalidatePlan = canContinue && planValidationBoundaryReached({
13893
+ ...state,
13894
+ approvedPlan: plan.finalPlan,
13895
+ planProgress: plan.planProgress,
13896
+ planStepValidationIndex: plan.planStepValidationIndex,
13897
+ }, settings);
13268
13898
  const summary = `# Selected Plan\n\n${recoverablePlanDetails(plan)}\n\nStatus: ${plan.approvalStatus}`;
13269
13899
  show(pi, `${summary}\n\nChoose what to do.`);
13270
13900
  if (!ctx.hasUI) return;
13271
- const choices = [...(canContinue ? ["Continue Plan", "Revalidate Plan"] : []), "Reuse Plan", "Amend Plan", "List Plan Status", "List Plans", "Cancel"];
13901
+ const choices = [...(canContinue ? ["Continue Plan"] : []), ...(canRevalidatePlan ? ["Revalidate Plan"] : []), "Reuse Plan", "Amend Plan", "List Plan Status", "List Plans", "Cancel"];
13272
13902
  const choice = await ctx.ui.select("Selected Plan", choices);
13273
13903
  if (choice === "Continue Plan") {
13274
13904
  const advancedSnapshot = findAdvancedPlanRuntimeSnapshot(ctx, plan);
@@ -13291,17 +13921,18 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
13291
13921
  // Primary /plan resume surface: restore state, then let the user choose.
13292
13922
  const showActivePlanResumeActionMenu = async (currentPlan?: SavedWorkflowPlan): Promise<void> => {
13293
13923
  const details = currentPlan ? `\n\n${recoverablePlanDetails(currentPlan)}` : "";
13294
- const summary = `# Plan Resume\n\n${currentPlan ? "Plan loaded from saved history." : state.approvedPlan ? "Current plan is loaded." : "No current plan is loaded."}${details}\n\n${planRecoveryGuidance(state, validationAvailable, planActiveRuntimeIsRecoverablyStale(ctx))}\n\n${renderWorkflowStatus(state, pi.getActiveTools(), ctx.cwd)}`;
13924
+ const summary = `# Plan Resume\n\n${currentPlan ? state.approvedPlan ? "Plan loaded from saved history." : "Saved plan is available." : state.approvedPlan ? "Current plan is loaded." : "No current plan is loaded."}${details}\n\n${resolved.source === "fallback" ? `${resolved.reason}\n\n` : ""}${planRecoveryGuidance(state, validationAvailable, planActiveRuntimeIsRecoverablyStale(ctx), settings)}\n\n${renderWorkflowStatus(state, pi.getActiveTools(), ctx.cwd)}`;
13295
13925
  show(pi, `${summary}\n\nChoose what to do.`);
13296
13926
  if (!ctx.hasUI) return;
13297
13927
  const hasAlternatives = allPlans.some((plan) => plan.id !== currentPlan?.id);
13298
13928
  const canContinueCurrent = planResumeCanContinueCurrent(state);
13299
13929
  const reviewRepairAvailable = planReviewRepairRecoveryAvailable(state);
13300
- const validationRepairAvailable = Boolean(state.lastValidationFailure || state.validationReport || state.validationVerdict === "FAIL" || state.validationVerdict === "PARTIAL PASS");
13930
+ const validationBoundaryReached = planValidationBoundaryReached(state, settings);
13931
+ const validationRepairAvailable = validationBoundaryReached && Boolean(state.lastValidationFailure || state.validationReport || state.validationVerdict === "FAIL" || state.validationVerdict === "PARTIAL PASS");
13301
13932
  const choice = await ctx.ui.select("Plan Resume", [
13302
13933
  ...(reviewRepairAvailable ? ["Review Repair / Recover Notes"] : []),
13303
13934
  ...(state.approvedPlan && validationRepairAvailable ? ["Repair / Retry"] : []),
13304
- ...(state.approvedPlan ? ["Revalidate"] : []),
13935
+ ...(state.approvedPlan && validationBoundaryReached ? ["Revalidate"] : []),
13305
13936
  ...(canContinueCurrent ? ["Continue Current Plan"] : []),
13306
13937
  ...(hasAlternatives ? ["Choose Another Plan"] : []),
13307
13938
  "List Plan Status", "List Plans", "Cancel",
@@ -13360,7 +13991,7 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
13360
13991
  const status = renderWorkflowStatus(state, pi.getActiveTools(), ctx.cwd);
13361
13992
 
13362
13993
  if (!state.approvedPlan && (state.mode === "idle" || state.mode === "cancelled" || state.mode === "awaiting_plan_input")) {
13363
- const resolved = resolveActivePlanForResume();
13994
+ const resolved = resolveActivePlanForResume(ctx);
13364
13995
  const selected = await choosePlanForContinueWhenNoCurrentPlan(ctx, resolved, "continue");
13365
13996
  if (!selected) return show(pi, `# ${title}\n\n${resolved.reason ?? "No plan selected. Continue cancelled."}\n\nUse /workflow plans list to see saved plans.\n\n${status}`);
13366
13997
  const advancedSnapshot = findAdvancedPlanRuntimeSnapshot(ctx, selected);
@@ -13431,6 +14062,21 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
13431
14062
  await beginExecution(ctx, true);
13432
14063
  return;
13433
14064
  }
14065
+ if ((state.mode === "executed" || state.mode === "validated") && planExecutionIncomplete(state, settings)) {
14066
+ show(pi, `# ${title}\n\nApproved Plan execution is incomplete. Continuing execution from the current incomplete step.`);
14067
+ updateState({
14068
+ mode: "reviewed",
14069
+ validationReport: undefined,
14070
+ validationVerdict: undefined,
14071
+ lastValidationFailure: undefined,
14072
+ lastRepairStatus: "none",
14073
+ lastRepairAttempt: undefined,
14074
+ planStepValidationIndex: undefined,
14075
+ planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: "reviewed" }, settings, { lifecycleStatus: "reviewed", validationStatus: "pending", repairStatus: "none", nextAction: "executor" }, state.approvedPlan) : state.planProgress,
14076
+ }, ctx);
14077
+ await beginExecution(ctx, true);
14078
+ return;
14079
+ }
13434
14080
  if (state.mode === "executed") {
13435
14081
  if (validationAvailable) {
13436
14082
  show(pi, `# ${title}\n\nExecution is complete. Continuing to validation.`);
@@ -13607,7 +14253,10 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
13607
14253
  const settings = loadWorkflowSettings(ctx.cwd);
13608
14254
  clearTypedHandoff(ctx, "Mission final repair");
13609
14255
  const failure = mission.lastFinalValidationFailure || mission.lastValidationFailure || mission.lastBlockReason || "No final validation failure details recorded.";
13610
- const unsafe = validationFailureRequiresApproval(failure, settings);
14256
+ const unsafe = validationFailureRequiresApproval(failure, settings, {
14257
+ concreteRepairableIssue: mission.concreteRepairableIssue,
14258
+ evidenceGap: mission.evidenceGap,
14259
+ });
13611
14260
  if (unsafe) {
13612
14261
  const blocked = saveActiveMission({ ...mission, status: "blocked", lastRepairStatus: "blocked", lastBlockReason: `Final repair requires approval: ${unsafe}`, nextAction: "Inspect final validation failure or approve broader repair before retrying.", lastSummary: `Mission final repair blocked: ${unsafe}` });
13613
14262
  checkpointMission(blocked, `Mission final repair blocked before attempt. Reason: ${unsafe}`, "Inspect final validation failure or approve broader repair before retrying.", undefined, { validationResult: blocked.lastFinalValidationResult ?? blocked.lastValidationResult });
@@ -13646,7 +14295,7 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
13646
14295
  repairHistory: appendRepairHistory(mission, { timestamp: new Date().toISOString(), retry, status: "running", validationFailure: compact(failure, 800), nextAction: "Repair final validation failure, then revalidate." }),
13647
14296
  });
13648
14297
  checkpointMission(repairing, `Final validation repair started. Retry ${retry}/${maxRetries}. Failure: ${compact(failure, 500)}`, "Repair final validation failure, then revalidate.", undefined, { validationResult: repairing.lastFinalValidationResult ?? repairing.lastValidationResult });
13649
- pi.setActiveTools(executionToolsFor(settings));
14298
+ pi.setActiveTools(missionRepairToolsFor(settings));
13650
14299
  updateState({ mode: "mission_repairing", activeMissionId: repairing.id, modelsUsed: { ...(state.modelsUsed ?? {}), executor: modelLabel(route) } }, ctx);
13651
14300
  saveActiveMission({ ...repairing, modelsUsed: { ...(repairing.modelsUsed ?? {}), executor: modelLabel(route) } });
13652
14301
  if (source !== "auto") show(pi, `# Mission Final Repair Started\n\nRetry: ${retry} of ${maxRetries}\n\n${renderMissionProgress(repairing, settings)}`);
@@ -13671,7 +14320,10 @@ Use /mission status, /mission resume, /mission continue, or /mission retry.`);
13671
14320
  return show(pi, `# Mission Runtime Budget Reached\n\n${runtimeBlocked}\n\n${renderMissionStatus(activeMission ?? paused)}`);
13672
14321
  }
13673
14322
  const failure = mission.lastValidationFailure || mission.lastBlockReason || "No validation failure details recorded.";
13674
- const unsafe = validationFailureRequiresApproval(failure, settings);
14323
+ const unsafe = validationFailureRequiresApproval(failure, settings, {
14324
+ concreteRepairableIssue: mission.concreteRepairableIssue,
14325
+ evidenceGap: mission.evidenceGap,
14326
+ });
13675
14327
  if (unsafe) {
13676
14328
  const milestone = mission.milestones[mission.currentMilestoneIndex];
13677
14329
  const blocked = saveActiveMission({ ...mission, status: "blocked", lastRepairStatus: "blocked", lastBlockReason: `Repair requires approval: ${unsafe}`, nextAction: "Inspect validation failure or approve broader repair before retrying.", lastSummary: `Mission repair blocked: ${unsafe}` });
@@ -13736,7 +14388,7 @@ ${renderMissionStatus(activeMission ?? paused)}`);
13736
14388
  repairHistory: appendRepairHistory(mission, { timestamp: new Date().toISOString(), milestoneId: milestone?.id, retry, status: "running", validationFailure: compact(failure, 800), nextAction: "Repair current validation failure, then revalidate." }),
13737
14389
  });
13738
14390
  checkpointMission(repairing, `${manualRetryOverride ? "Manual repair retry override" : "Repair started"} for ${milestone?.id ?? "current milestone"}. Retry ${retry}/${maxRetries} per milestone; mission retry ${missionRetry}/${maxMissionRetries} total. Failure: ${compact(failure, 500)}`, "Repair current validation failure, then revalidate.", milestone?.id, { validationResult: repairing.lastValidationResult });
13739
- pi.setActiveTools(executionToolsFor(settings));
14391
+ pi.setActiveTools(missionRepairToolsFor(settings));
13740
14392
  updateState({ mode: "mission_repairing", activeMissionId: repairing.id, modelsUsed: { ...(state.modelsUsed ?? {}), executor: modelLabel(route) } }, ctx);
13741
14393
  saveActiveMission({ ...repairing, modelsUsed: { ...(repairing.modelsUsed ?? {}), executor: modelLabel(route) } });
13742
14394
  if (source !== "auto") show(pi, `# Mission Repair Started\n\nRetry: ${retry} of ${maxRetries} per milestone\nMission Retry: ${missionRetry} of ${maxMissionRetries} total\nMilestone: ${milestone?.id ?? "current"} — ${milestone?.title ?? "unknown"}\n\n${renderMissionProgress(repairing, settings)}`);
@@ -13834,7 +14486,10 @@ ${renderMissionStatus(activeMission ?? paused)}`);
13834
14486
  const retry = missionFinalValidationRetryCount(mission);
13835
14487
  const maxRetries = missionMaxFinalValidationRetries(mission, settings);
13836
14488
  const failure = `Final mission validation ${verdict}. ${compact(validationText, 1200)}`;
13837
- const unsafe = validationFailureRequiresApproval(failure, settings);
14489
+ const unsafe = validationFailureRequiresApproval(failure, settings, {
14490
+ concreteRepairableIssue: mission.concreteRepairableIssue,
14491
+ evidenceGap: mission.evidenceGap,
14492
+ });
13838
14493
  const nextRetry = retry + 1;
13839
14494
  const failed = saveActiveMission({ ...mission, status: "validating", finalValidationRetryCount: nextRetry, maxFinalValidationRetries: maxRetries, lastFinalValidationResult: verdict ?? "UNKNOWN", lastFinalValidationFailure: failure, lastValidationFailure: failure, lastBlockReason: "", nextAction: "Final mission validation failed; evaluating safe repair retry.", lastSummary: `Final mission validation ${verdict}.` });
13840
14495
  checkpointMission(failed, `Final mission validation failed. Retry ${retry}/${maxRetries}. ${compact(validationText, 500)}`, "Evaluate safe final repair retry before completion.", undefined, { validationResult: verdict });
@@ -13917,7 +14572,10 @@ ${renderMissionStatus(activeMission ?? paused)}`);
13917
14572
  lastSummary: `Validation ${verdict} for mission milestone ${milestone?.id ?? "current"}.`,
13918
14573
  });
13919
14574
  checkpointMission(failed, `Validation failed for ${milestone?.id ?? "current milestone"}. Retry ${retry}/${maxRetries} per milestone; mission retry ${missionRetry}/${maxMissionRetries} total. ${compact(validationText, 500)}`, "Evaluate safe repair retry before blocking or advancing.", milestone?.id, { validationResult: verdict });
13920
- const unsafe = validationFailureRequiresApproval(failure, settings);
14575
+ const unsafe = validationFailureRequiresApproval(failure, settings, {
14576
+ concreteRepairableIssue: mission.concreteRepairableIssue,
14577
+ evidenceGap: mission.evidenceGap,
14578
+ });
13921
14579
  const shouldBlock = mission.autonomy === "manual"
13922
14580
  || settings.missions.autoRepairValidationFailures === false
13923
14581
  || settings.missions.pauseAfterValidationFailure === true
@@ -13993,7 +14651,7 @@ ${renderMissionStatus(activeMission ?? paused)}`);
13993
14651
  const answerSummary = formatAnswersForPlanner(questions, answers);
13994
14652
  const settings = loadWorkflowSettings(ctx.cwd);
13995
14653
  const priorPlanning = snapshotSubagentPhaseUsage("Planning");
13996
- if (!beginForcedSubagentPhase(ctx, "Planning", settings)) {
14654
+ if (!beginForcedSubagentPhase(ctx, "Planning", settings, { task: state.task ?? "Plan with clarification answers" })) {
13997
14655
  updateState({ mode: "planning", clarifyingAnswers: answers, draftPlan: state.draftPlan, approvedPlan: undefined, planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: "planning", draftPlan: state.draftPlan, approvedPlan: undefined }, settings, { lifecycleStatus: "blocked", nextAction: "fix forced planning sub-agent policy or rerun planning", steps: [] }, undefined) : state.planProgress }, ctx);
13998
14656
  return;
13999
14657
  }
@@ -14066,7 +14724,7 @@ ${renderMissionStatus(activeMission ?? paused)}`);
14066
14724
  }
14067
14725
  const questions = mission.clarificationQuestions?.length ? mission.clarificationQuestions : (state.clarifyingQuestions ?? []);
14068
14726
  const settings = loadWorkflowSettings(ctx.cwd);
14069
- const planningOverride = { policy: missionSubagentPolicy(settings), workers: missionPlanningWorkerCount(settings), label: "Mission Planning" };
14727
+ const planningOverride = { policy: missionSubagentPolicy(settings), workers: missionPlanningWorkerCount(settings), label: "Mission Planning", task: mission.goal };
14070
14728
  const priorPlanning = snapshotSubagentPhaseUsage("Planning");
14071
14729
  if (!beginForcedSubagentPhase(ctx, "Planning", settings, planningOverride)) {
14072
14730
  const blocked = saveActiveMission({ ...mission, status: "blocked", clarificationQuestions: questions, clarificationAnswers: answers, lastBlockReason: "Mission clarification resume blocked by forced Mission Planning sub-agent policy.", nextAction: "Fix Mission Planning sub-agent workers, then run /mission resume or /mission plan.", lastSummary: "Mission clarification answers recorded, but forced Mission Planning availability gate did not pass." });
@@ -14173,15 +14831,10 @@ ${renderMissionStatus(activeMission ?? paused)}`);
14173
14831
  } else if (choice === "Revise Mission Plan") {
14174
14832
  await beginMissionPlanning(ctx, { ...mission, clarificationAnswers: mission.clarificationAnswers ?? [] });
14175
14833
  } else if (choice === "Answer More Clarifications") {
14176
- if (mission.clarificationQuestions?.length) {
14177
- updateState({ mode: "mission_awaiting_clarification", activeMissionId: mission.id, task: mission.goal, originalTask: mission.goal, clarifyingQuestions: mission.clarificationQuestions, clarifyingAnswers: mission.clarificationAnswers }, ctx);
14178
- show(pi, `# Mission Clarification\n\n${renderClarificationQuestionsForUser(mission.clarificationQuestions)}`);
14179
- } else {
14180
- const next = saveActiveMission({ ...mission, status: "draft", lastSummary: "Additional mission clarification requested before approval.", nextAction: "Generate dynamic mission clarification questions before approval." });
14181
- checkpointMission(next, "Additional mission clarification requested before approval.", "Generate dynamic mission-specific clarification questions.");
14182
- recordWorkflowInternalEvent(ctx, "Internal workflow lifecycle event suppressed.");
14183
- await beginMissionPlanning(ctx, next, { forceClarification: true, forceReason: "user requested additional mission clarification before approval" });
14184
- }
14834
+ const next = saveActiveMission({ ...mission, status: "draft", lastSummary: "Additional mission clarification requested before approval.", nextAction: "Generate dynamic mission clarification questions before approval." });
14835
+ checkpointMission(next, "Additional mission clarification requested before approval.", "Generate dynamic mission-specific clarification questions.");
14836
+ recordWorkflowInternalEvent(ctx, "Internal workflow lifecycle event suppressed.");
14837
+ await beginMissionPlanning(ctx, next, { forceClarification: true, forceAdditionalClarification: true, forceReason: "user requested additional mission clarification before approval" });
14185
14838
  } else if (choice === "Cancel Mission") {
14186
14839
  const stopped = saveActiveMission({ ...mission, status: "stopped", lastSummary: "Mission cancelled before approval." });
14187
14840
  checkpointMission(stopped, "Mission cancelled before approval.", "No further mission action queued.");
@@ -14801,7 +15454,7 @@ ${renderMissionStatus(activeMission ?? paused)}`);
14801
15454
  else if (choice === "Parallel Agent Settings") await showParallelismSettingsMenu(ctx);
14802
15455
  else if (choice === "Background Sub-agents") {
14803
15456
  const enabled = await chooseBool(ctx, "subagents.allowBackgroundSubagents?");
14804
- if (enabled !== undefined) { const r = updateSettings(ctx.cwd, undefined, (s) => { s.subagents.allowBackgroundSubagents = enabled; }); workflowUiNotify(ctx, `subagents.allowBackgroundSubagents set to ${enabled} in ${r.file}. Background sub-agents run in Planning/Review/Validation phases without blocking the parent.`, "info"); }
15457
+ if (enabled !== undefined) { const r = updateSettings(ctx.cwd, undefined, (s) => { s.subagents.allowBackgroundSubagents = enabled; }); workflowUiNotify(ctx, `subagents.allowBackgroundSubagents set to ${enabled} in ${r.file}. Background sub-agents are advisory and may run in Planning/Review preflight without blocking the parent.`, "info"); }
14805
15458
  } else if (choice === "Activity Indicator") {
14806
15459
  const enabled = await chooseBool(ctx, "subagents.activityIndicatorEnabled?");
14807
15460
  if (enabled !== undefined) { const r = updateSettings(ctx.cwd, undefined, (s) => { s.subagents.activityIndicatorEnabled = enabled; }); workflowUiNotify(ctx, `subagents.activityIndicatorEnabled set to ${enabled} in ${r.file}`, "info"); renderWorkflowSubagentActivity(ctx); }
@@ -16145,30 +16798,96 @@ Pi Version: v${VERSION}
16145
16798
  if (process.env.PI_SUBAGENT_WORKER === "1") return;
16146
16799
  const settings = loadWorkflowSettings(ctx.cwd);
16147
16800
  let phase = phaseForWorkflowMode(state.mode);
16801
+ const command = event.toolName === "bash" ? String((event.input as { command?: unknown }).command ?? "") : undefined;
16148
16802
  if (state.mode === "standard") {
16149
16803
  if (event.toolName === "bash") {
16150
- const command = String((event.input as { command?: unknown }).command ?? "");
16151
16804
  if (standardForcedSubagentSafeBash(command)) return;
16152
16805
  }
16153
16806
  if (event.toolName === "edit" || event.toolName === "write" || event.toolName === "bash") phase = "Execution";
16154
16807
  }
16155
- const planValidationPhase = phase === "Validation" && (state.mode === "validating" || state.mode === "revalidating");
16156
- if (phase !== "Execution" && phase !== "Repair" && !planValidationPhase) return;
16808
+ if (event.toolName === WORKFLOW_EXECUTION_RESULT_TOOL || event.toolName === MISSION_MILESTONE_RESULT_TOOL) phase = "Execution";
16809
+ if (event.toolName === WORKFLOW_REPAIR_RESULT_TOOL) phase = "Repair";
16810
+ if (event.toolName === WORKFLOW_VALIDATION_RESULT_TOOL) phase = "Validation";
16811
+ if (event.toolName === WORKFLOW_REVIEW_RESULT_TOOL) phase = "Review";
16812
+ if (!phase) return;
16157
16813
  const policy = state.mode === "standard" ? standardPhasePolicy(settings, phase) : phasePolicy(settings, phase);
16158
- if (policy !== "forced") return;
16814
+ if (subagentPolicyNeedsInternalDecision(policy)) {
16815
+ if (event.toolName === "subagent") {
16816
+ const workers = state.mode === "standard" ? standardWorkerCount(settings, phase) : workerCount(settings, phase);
16817
+ const target = advisoryWorkerTargetForPolicy(policy, workers);
16818
+ const observed = subagentUsageByPhase[phase] ?? 0;
16819
+ const task = state.task ?? state.originalTask;
16820
+ recordSubagentPolicyDecision(ctx, {
16821
+ phase,
16822
+ policy,
16823
+ outcome: "auto_delegate",
16824
+ reason: "visible parent sub-agent call started after advisory consideration",
16825
+ task,
16826
+ required: target,
16827
+ observed,
16828
+ background: false,
16829
+ });
16830
+ }
16831
+ return;
16832
+ }
16833
+ if (!subagentPolicyRequiresRequiredEvidence(policy)) return;
16159
16834
  if (state.mode === "standard" && standardForcedSubagentSatisfied(phase, settings, state.task ?? state.originalTask ?? "Standard Mode task")) return;
16160
- const required = workerTargetForPolicy("forced", state.mode === "standard" ? standardWorkerCount(settings, phase) : workerCount(settings, phase));
16835
+ const required = workerTargetForPolicy(policy, state.mode === "standard" ? standardWorkerCount(settings, phase) : workerCount(settings, phase));
16161
16836
  const observed = subagentUsageByPhase[phase] ?? 0;
16162
16837
  if (observed >= required) return;
16163
- if (planValidationPhase) {
16164
- if (event.toolName === "subagent") return;
16165
- return { block: true, reason: `${event.toolName} blocked — validation workers required (${observed}/${required})` };
16838
+ if (phase === "Validation" && event.toolName === WORKFLOW_VALIDATION_RESULT_TOOL) {
16839
+ recordSubagentPolicyDecision(ctx, {
16840
+ phase,
16841
+ policy,
16842
+ outcome: "allow_user_explicit_finalization",
16843
+ reason: "typed validation verdict is finalization; forced validation workers must be enforced before validation work, not after verdict",
16844
+ task: state.task ?? state.originalTask,
16845
+ required,
16846
+ observed,
16847
+ background: false,
16848
+ });
16849
+ return;
16166
16850
  }
16167
- if (event.toolName === "edit" || event.toolName === "write" || event.toolName === "bash") {
16168
- if (phase === "Repair") return;
16169
- const label = state.mode === "standard" ? "Standard Mode" : phase;
16170
- return { block: true, reason: `${event.toolName} blocked — execution workers required (${observed}/${required})` };
16851
+ if (event.toolName === "subagent") {
16852
+ const label = forcedSubagentGuardLabel(phase);
16853
+ const block = forcedSubagentVisibleCallBlock(ctx, phase, required, observed, event.input, label);
16854
+ if (block) return { block: true, reason: block };
16855
+ recordSubagentPolicyDecision(ctx, {
16856
+ phase,
16857
+ policy,
16858
+ outcome: "required",
16859
+ reason: "visible sub-agent call requested enough forced worker task entries for the remaining requirement",
16860
+ task: state.task ?? state.originalTask,
16861
+ required,
16862
+ observed,
16863
+ background: false,
16864
+ });
16865
+ return;
16866
+ }
16867
+ const actionDecision = forcedSubagentActionDecision({
16868
+ phase,
16869
+ policy,
16870
+ task: state.task ?? state.originalTask,
16871
+ kind: state.standardWorkKind,
16872
+ toolName: event.toolName,
16873
+ command,
16874
+ });
16875
+ if (actionDecision.allowBeforeEvidence) {
16876
+ recordSubagentPolicyDecision(ctx, {
16877
+ phase,
16878
+ policy,
16879
+ outcome: actionDecision.outcome,
16880
+ reason: actionDecision.reason,
16881
+ task: state.task ?? state.originalTask,
16882
+ required,
16883
+ observed,
16884
+ background: false,
16885
+ });
16886
+ return;
16171
16887
  }
16888
+ const label = forcedSubagentGuardLabel(phase);
16889
+ const noun = phase === "Planning" ? "planning" : phase === "Repair" ? "repair" : phase === "Review" ? "review" : phase === "Validation" ? "validation" : "execution";
16890
+ return { block: true, reason: `${event.toolName} blocked — forced ${noun} workers required (${observed}/${required}) for ${label}` };
16172
16891
  });
16173
16892
 
16174
16893
  function workflowAutoCompactionModeEligible(mode: string): boolean {
@@ -16377,7 +17096,7 @@ Pi Version: v${VERSION}
16377
17096
  const settings = loadWorkflowSettings(ctx.cwd);
16378
17097
  const override = standardForcedSubagentOverride(settings, phase);
16379
17098
  const task = state.task ?? state.originalTask ?? "Standard Mode task";
16380
- if (override.policy === "forced") rememberStandardSubagentPreflight(ctx, phase, task, workerTargetForPolicy("forced", override.workers));
17099
+ if (subagentPolicyRequiresRequiredEvidence(override.policy)) rememberStandardSubagentPreflight(ctx, phase, task, workerTargetForPolicy(override.policy, override.workers));
16381
17100
  }
16382
17101
  }
16383
17102
  finishSubagentActivity(event.toolCallId, Boolean(event.isError), ctx);
@@ -16635,7 +17354,7 @@ Pi Version: v${VERSION}
16635
17354
  const shouldRespectDraftPlanningRail = (action === "continue" || action === "next") && currentMission?.status === "draft" && currentMission.milestones.length === 0;
16636
17355
  const needsMissionResolution = action === "retry" || action === "repair" || action === "revalidate" || ((action === "continue" || action === "next") && !shouldRespectDraftPlanningRail);
16637
17356
  const resolvedMission = needsMissionResolution
16638
- ? await chooseResumeMission(ctx, resolveActiveMissionForResume(), action as "continue" | "next" | "retry" | "repair" | "revalidate")
17357
+ ? await chooseResumeMission(ctx, resolveActiveMissionForResume(ctx), action as "continue" | "next" | "retry" | "repair" | "revalidate")
16639
17358
  : undefined;
16640
17359
  const mission = resolvedMission ?? currentMission;
16641
17360
  if (!mission) return show(pi, `# Mission ${action}\n\nNo mission selected. ${action} cancelled.`);
@@ -17822,6 +18541,10 @@ Public workflow commands:
17822
18541
  if (route) updateState({ modelsUsed: { ...(state.modelsUsed ?? {}), [standardRole]: modelLabel(route) } }, ctx);
17823
18542
  }
17824
18543
  pi.setActiveTools(standardToolsFor(settings));
18544
+ const standardSurfaceBlock = verifyWorkflowToolSurface(ctx, "Execution", "standard before_agent_start", "standard");
18545
+ if (standardSurfaceBlock) {
18546
+ return { systemPrompt: `${event.systemPrompt}\n\nPI WORKFLOW STANDARD MODE BLOCKED: ${standardSurfaceBlock}\n\nDo not execute the user request. Retry after the Standard tool surface is repaired.` };
18547
+ }
17825
18548
  standardTodoCreatedThisTurn = false;
17826
18549
  const rawPrompt = event.prompt?.trim() ?? "";
17827
18550
  const answeredClarification = Boolean(state.standardClarificationAnswer?.trim() && state.standardClarificationTask?.trim() && !state.standardClarificationPending);
@@ -17840,7 +18563,8 @@ Public workflow commands:
17840
18563
  return { systemPrompt: `${event.systemPrompt}\n\n${standardClarificationRequestPrompt(task, settings, gate.reason)}` };
17841
18564
  }
17842
18565
  }
17843
- if (task && (standardWork.phase === "Execution" || standardWork.phase === "Repair")) {
18566
+ const standardTodoMustInitialize = standardRequiredTodoMissing(state, settings, task);
18567
+ if (task && !standardTodoMustInitialize && (standardWork.phase === "Execution" || standardWork.phase === "Repair")) {
17844
18568
  const override = standardForcedSubagentOverride(settings, standardWork.phase);
17845
18569
  if (!beginForcedSubagentPhase(ctx, standardWork.phase, settings, override)) {
17846
18570
  pi.setActiveTools(clarificationToolsFor(settings, false));
@@ -17859,7 +18583,7 @@ Public workflow commands:
17859
18583
  if (!route) return { systemPrompt: `${event.systemPrompt}\n\nPI WORKFLOW PLAN MODE ROUTING FAILED: the configured planner model is unavailable. Do not execute the user request; explain the routing error and ask the user to check /workflow-settings list.` };
17860
18584
  const task = event.prompt || "Create an implementation plan for the user's requested task.";
17861
18585
  const gate = clarificationGate(task, settings, false, false);
17862
- if (!beginForcedSubagentPhase(ctx, "Planning", settings)) return { systemPrompt: `${event.systemPrompt}\n\nPI WORKFLOW PLAN MODE BLOCKED: forced planning sub-agent requirements are unavailable. Do not execute the user request; report the sub-agent policy blocker and wait for settings or worker availability to be fixed.` };
18586
+ if (!beginForcedSubagentPhase(ctx, "Planning", settings, { task })) return { systemPrompt: `${event.systemPrompt}\n\nPI WORKFLOW PLAN MODE BLOCKED: forced planning sub-agent requirements are unavailable. Do not execute the user request; report the sub-agent policy blocker and wait for settings or worker availability to be fixed.` };
17863
18587
  activeMission = undefined;
17864
18588
  const activePlanId = createWorkflowPlanId(ctx.cwd);
17865
18589
  updateState({ mode: "planning", activePlanId, activeMissionId: undefined, task, originalTask: task, draftPlan: undefined, approvedPlan: undefined, clarifyingQuestions: undefined, clarifyingAnswers: undefined, clarificationAlreadyAsked: gate.force ? true : undefined, clarificationRequiredBeforePlan: gate.force || undefined, clarificationRequirementReason: gate.forceReason, clarificationSkipReason: gate.skipReason, clarificationQualityRetryCount: undefined, planningDepth: settings.planning.depth, clarificationMode: settings.planning.clarificationMode, executionSummary: undefined, validationReport: undefined, validationVerdict: undefined, currentValidationRetry: 0, workflowValidationRetryCount: 0, maxValidationRetriesPerPlan: undefined, maxValidationRetriesPerWorkflow: undefined, lastValidationFailure: undefined, lastRepairAttempt: undefined, repairHistory: undefined, lastRepairStatus: "none", planRuntime: undefined, planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: "planning", task, draftPlan: undefined, approvedPlan: undefined }, settings, { lifecycleStatus: "planning", nextAction: "planner" }, undefined) : undefined, modelsUsed: { planner: modelLabel(route) }, planHistoryId: undefined, approvedPlanHistoryId: undefined }, ctx);
@@ -17907,6 +18631,10 @@ Public workflow commands:
17907
18631
  if (mission?.currentStep === "reviewer") {
17908
18632
  const settings = loadWorkflowSettings(ctx.cwd);
17909
18633
  armWorkflowToolsForPhase(ctx, "Review", "mission review before_agent_start");
18634
+ const reviewSurfaceBlock = verifyWorkflowToolSurface(ctx, "Review", "mission review before_agent_start");
18635
+ if (reviewSurfaceBlock) {
18636
+ return { systemPrompt: `${event.systemPrompt}\n\nPI MISSION REVIEW BLOCKED: ${reviewSurfaceBlock}\n\nDo not approve, execute, validate, repair, call workflow_plan_result, call mission_plan_result, or call workflow_progress. Report that Mission review stayed pending because required Review tools were unavailable, then stop.` };
18637
+ }
17910
18638
  const surfaceProblem = missionReviewRequiredToolSurfaceProblem(pi.getActiveTools());
17911
18639
  if (surfaceProblem) {
17912
18640
  blockMissionReviewToolSurface(ctx, mission, surfaceProblem, "mission review before_agent_start", false);
@@ -17934,7 +18662,14 @@ Public workflow commands:
17934
18662
  const prompt = await missionActiveGatePrompt(ctx, mission, persistedGateMode, event.systemPrompt);
17935
18663
  if (prompt) return prompt;
17936
18664
  }
17937
- pi.setActiveTools(executionToolsFor(loadWorkflowSettings(ctx.cwd)));
18665
+ if (state.reviewHandoffSuppression?.kind === "mission_typed_review_to_approval") {
18666
+ clearReviewHandoffSuppression(ctx, "mission execution turn started after typed Mission review handoff");
18667
+ }
18668
+ pi.setActiveTools(missionExecutionToolsFor(loadWorkflowSettings(ctx.cwd)));
18669
+ const missionExecutionSurfaceBlock = verifyWorkflowToolSurface(ctx, "Execution", "mission execution before_agent_start");
18670
+ if (missionExecutionSurfaceBlock) {
18671
+ return { systemPrompt: `${event.systemPrompt}\n\nPI MISSION EXECUTION BLOCKED: ${missionExecutionSurfaceBlock}\n\nDo not execute the milestone. Run /mission continue after the tool surface is repaired.` };
18672
+ }
17938
18673
  return { systemPrompt: `${event.systemPrompt}\n\n${missionRuntimePrompt(mission, loadWorkflowSettings(ctx.cwd), phasePreflightBlocks.Execution)}` };
17939
18674
  }
17940
18675
 
@@ -17942,7 +18677,11 @@ Public workflow commands:
17942
18677
  if (state.lastWorkflowHandoff?.type === WORKFLOW_REPAIR_RESULT_TOOL) return;
17943
18678
  const mission = (state.activeMissionId ? loadMissionState(state.activeMissionId) : undefined) ?? activeMission ?? loadMissionState("latest");
17944
18679
  if (!mission) return { systemPrompt: event.systemPrompt };
17945
- pi.setActiveTools(executionToolsFor(loadWorkflowSettings(ctx.cwd)));
18680
+ pi.setActiveTools(missionRepairToolsFor(loadWorkflowSettings(ctx.cwd)));
18681
+ const missionRepairSurfaceBlock = verifyWorkflowToolSurface(ctx, "Repair", "mission repair before_agent_start");
18682
+ if (missionRepairSurfaceBlock) {
18683
+ return { systemPrompt: `${event.systemPrompt}\n\nPI MISSION REPAIR BLOCKED: ${missionRepairSurfaceBlock}\n\nDo not repair the mission. Run /mission repair or /mission resume after the tool surface is repaired.` };
18684
+ }
17946
18685
  return { systemPrompt: `${event.systemPrompt}\n\n${missionRepairPrompt(mission, loadWorkflowSettings(ctx.cwd), phasePreflightBlocks.Repair)}` };
17947
18686
  }
17948
18687
 
@@ -17950,6 +18689,10 @@ Public workflow commands:
17950
18689
  const mission = (state.activeMissionId ? loadMissionState(state.activeMissionId) : undefined) ?? activeMission ?? loadMissionState("latest");
17951
18690
  if (!mission) return { systemPrompt: event.systemPrompt };
17952
18691
  pi.setActiveTools(validationToolsFor(loadWorkflowSettings(ctx.cwd)));
18692
+ const missionValidationSurfaceBlock = verifyWorkflowToolSurface(ctx, "Validation", "mission validation before_agent_start");
18693
+ if (missionValidationSurfaceBlock) {
18694
+ return { systemPrompt: `${event.systemPrompt}\n\nPI MISSION VALIDATION BLOCKED: ${missionValidationSurfaceBlock}\n\nDo not validate the mission. Run /mission revalidate after the tool surface is repaired.` };
18695
+ }
17953
18696
  return { systemPrompt: `${event.systemPrompt}\n\n${missionValidationPrompt(mission, loadWorkflowSettings(ctx.cwd), state.executionSummary, phasePreflightBlocks.Validation)}` };
17954
18697
  }
17955
18698
 
@@ -17957,6 +18700,10 @@ Public workflow commands:
17957
18700
  const mission = (state.activeMissionId ? loadMissionState(state.activeMissionId) : undefined) ?? activeMission ?? loadMissionState("latest");
17958
18701
  if (!mission) return { systemPrompt: event.systemPrompt };
17959
18702
  pi.setActiveTools(validationToolsFor(loadWorkflowSettings(ctx.cwd)));
18703
+ const missionFinalValidationSurfaceBlock = verifyWorkflowToolSurface(ctx, "Validation", "mission final validation before_agent_start");
18704
+ if (missionFinalValidationSurfaceBlock) {
18705
+ return { systemPrompt: `${event.systemPrompt}\n\nPI MISSION FINAL VALIDATION BLOCKED: ${missionFinalValidationSurfaceBlock}\n\nDo not validate the mission. Run /mission revalidate after the tool surface is repaired.` };
18706
+ }
17960
18707
  return { systemPrompt: `${event.systemPrompt}\n\n${missionFinalValidationPrompt(mission, loadWorkflowSettings(ctx.cwd), state.executionSummary, phasePreflightBlocks.Validation)}` };
17961
18708
  }
17962
18709
 
@@ -17972,13 +18719,13 @@ Public workflow commands:
17972
18719
  const questions = state.clarifyingQuestions ?? parseClarifyingQuestions(state.draftPlan ?? "");
17973
18720
  const answerSummary = formatAnswersForPlanner(questions, parsed);
17974
18721
  const settings = loadWorkflowSettings(ctx.cwd);
17975
- if (!beginForcedSubagentPhase(ctx, "Planning", settings)) return { systemPrompt: `${event.systemPrompt}\n\nPI WORKFLOW PLAN MODE BLOCKED: forced planning sub-agent requirements are unavailable. Report the blocker and wait.` };
18722
+ if (!beginForcedSubagentPhase(ctx, "Planning", settings, { task: state.task ?? state.originalTask })) return { systemPrompt: `${event.systemPrompt}\n\nPI WORKFLOW PLAN MODE BLOCKED: forced planning sub-agent requirements are unavailable. Report the blocker and wait.` };
17976
18723
  updateState({ mode: "planning", clarifyingAnswers: parsed, modelsUsed: { ...(state.modelsUsed ?? {}), planner: modelLabel(route) } }, ctx);
17977
18724
  return { systemPrompt: `${event.systemPrompt}\n\n${planPrompt(state.task ?? "Plan with answers", state.approvedPlan, answerSummary, settings, { feedbackKind: "clarification" })}` };
17978
18725
  }
17979
18726
  // Not a shorthand answer — treat as free-text clarification
17980
18727
  const settings = loadWorkflowSettings(ctx.cwd);
17981
- if (!beginForcedSubagentPhase(ctx, "Planning", settings)) return { systemPrompt: `${event.systemPrompt}\n\nPI WORKFLOW PLAN MODE BLOCKED: forced planning sub-agent requirements are unavailable. Report the blocker and wait.` };
18728
+ if (!beginForcedSubagentPhase(ctx, "Planning", settings, { task: state.task ?? state.originalTask })) return { systemPrompt: `${event.systemPrompt}\n\nPI WORKFLOW PLAN MODE BLOCKED: forced planning sub-agent requirements are unavailable. Report the blocker and wait.` };
17982
18729
  updateState({ mode: "planning", clarifyingAnswers: input ? [{ index: 1, letter: "D", custom: input }] : state.clarifyingAnswers, modelsUsed: { ...(state.modelsUsed ?? {}), planner: modelLabel(route) } }, ctx);
17983
18730
  return { systemPrompt: `${event.systemPrompt}\n\n${planPrompt(state.task ?? "Plan with answers", state.approvedPlan, input, settings, { feedbackKind: "clarification" })}` };
17984
18731
  }
@@ -18001,6 +18748,10 @@ Public workflow commands:
18001
18748
  }
18002
18749
  if (state.mode === "reviewing") {
18003
18750
  pi.setActiveTools(reviewToolsFor(loadWorkflowSettings(ctx.cwd)));
18751
+ const reviewSurfaceBlock = verifyWorkflowToolSurface(ctx, "Review", "plan review before_agent_start");
18752
+ if (reviewSurfaceBlock) {
18753
+ return { systemPrompt: `${event.systemPrompt}\n\nPI WORKFLOW REVIEW BLOCKED: ${reviewSurfaceBlock}\n\nDo not review the approved plan. Run /plan continue after the review tool surface is repaired.` };
18754
+ }
18004
18755
  return { systemPrompt: `${event.systemPrompt}\n\n${reviewerPrompt(state, loadWorkflowSettings(ctx.cwd), phasePreflightBlocks.Review)}` };
18005
18756
  }
18006
18757
  if (state.mode === "reviewed") {
@@ -18011,23 +18762,26 @@ Public workflow commands:
18011
18762
  const settings = loadWorkflowSettings(ctx.cwd);
18012
18763
  clearReviewHandoffSuppression(ctx, "execution turn started after typed Plan review handoff");
18013
18764
  pi.setActiveTools(executionToolsFor(settings));
18014
- const missingExecutionTools = missingRequiredPlanExecutionTools(pi.getActiveTools(), settings);
18015
- traceWorkflowTracking(ctx, "before-agent-execution-tool-snapshot", { activeTools: pi.getActiveTools(), missingExecutionTools });
18016
- if (missingExecutionTools.length) {
18017
- const reason = `Required execution tools are unavailable after before_agent_start execution tool rearm: ${missingExecutionTools.join(", ")}. Execution prompt was not injected.`;
18018
- updateState({ mode: "plan_approved", planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: "plan_approved" }, settings, { lifecycleStatus: "blocked", nextAction: "repair execution tool surface" }, state.approvedPlan) : state.planProgress, lastReviewFailure: reason }, ctx);
18019
- pi.setActiveTools(webSafePlanTools(PLAN_TOOLS));
18020
- recordWorkflowInternalEvent(ctx, `Plan execution blocked at before_agent_start because required execution tools were unavailable after rearm: ${missingExecutionTools.join(", ")}.`);
18021
- return { systemPrompt: `${event.systemPrompt}\n\nPI WORKFLOW EXECUTION BLOCKED: ${reason}\n\nDo not execute the approved plan. Run /plan continue after the execution tool surface is repaired.` };
18765
+ const executionSurfaceBlock = verifyWorkflowToolSurface(ctx, "Execution", "plan execution before_agent_start");
18766
+ if (executionSurfaceBlock) {
18767
+ return { systemPrompt: `${event.systemPrompt}\n\nPI WORKFLOW EXECUTION BLOCKED: ${executionSurfaceBlock}\n\nDo not execute the approved plan. Run /plan continue after the execution tool surface is repaired.` };
18022
18768
  }
18023
18769
  return { systemPrompt: `${event.systemPrompt}\n\n${executePrompt(state, settings, phasePreflightBlocks.Execution)}` };
18024
18770
  }
18025
18771
  if (state.mode === "repairing") {
18026
18772
  pi.setActiveTools(executionToolsFor(loadWorkflowSettings(ctx.cwd)));
18773
+ const repairSurfaceBlock = verifyWorkflowToolSurface(ctx, "Repair", "plan repair before_agent_start");
18774
+ if (repairSurfaceBlock) {
18775
+ return { systemPrompt: `${event.systemPrompt}\n\nPI WORKFLOW REPAIR BLOCKED: ${repairSurfaceBlock}\n\nDo not repair the approved plan. Run /plan repair after the tool surface is repaired.` };
18776
+ }
18027
18777
  return { systemPrompt: `${event.systemPrompt}\n\n${workflowRepairPrompt(state, loadWorkflowSettings(ctx.cwd), phasePreflightBlocks.Repair)}` };
18028
18778
  }
18029
18779
  if (state.mode === "validating" || state.mode === "revalidating") {
18030
18780
  pi.setActiveTools(validationToolsFor(loadWorkflowSettings(ctx.cwd)));
18781
+ const validationSurfaceBlock = verifyWorkflowToolSurface(ctx, "Validation", "plan validation before_agent_start");
18782
+ if (validationSurfaceBlock) {
18783
+ return { systemPrompt: `${event.systemPrompt}\n\nPI WORKFLOW VALIDATION BLOCKED: ${validationSurfaceBlock}\n\nDo not validate the approved plan. Run /plan revalidate after the tool surface is repaired.` };
18784
+ }
18031
18785
  return { systemPrompt: `${event.systemPrompt}\n\n${validatePrompt(state, loadWorkflowSettings(ctx.cwd), phasePreflightBlocks.Validation)}` };
18032
18786
  }
18033
18787
  if (state.mode === "validated") {
@@ -18060,6 +18814,11 @@ Public workflow commands:
18060
18814
  return { message: { ...message, content: [{ type: "text" as const, text: "" }] } };
18061
18815
  }
18062
18816
 
18817
+ if (missionReviewParentSuppressed()) {
18818
+ traceWorkflowTracking(ctx, "typed-mission-review-parent-message-suppressed", { mode: state.mode, activeMissionId: state.activeMissionId });
18819
+ return { message: { ...message, content: [{ type: "text" as const, text: "" }] } };
18820
+ }
18821
+
18063
18822
  if (text && planReviewRepairActive(state) && state.lastWorkflowHandoff?.type === WORKFLOW_REVIEW_RESULT_TOOL) {
18064
18823
  const firstTextIndex = message.content.findIndex((block) => block.type === "text");
18065
18824
  if (firstTextIndex >= 0) {
@@ -18144,6 +18903,12 @@ Public workflow commands:
18144
18903
  return;
18145
18904
  }
18146
18905
 
18906
+ if (missionReviewParentSuppressed()) {
18907
+ traceWorkflowTracking(ctx, "typed-mission-review-parent-agent-end-suppressed", { mode: state.mode, activeMissionId: state.activeMissionId });
18908
+ clearReviewHandoffSuppression(ctx, "typed Mission review parent turn ended after accepted approval handoff");
18909
+ return;
18910
+ }
18911
+
18147
18912
  if (!text) {
18148
18913
  const interruptionReason = workflowTransientInterruptionReason(event, "Workflow turn stopped before producing assistant output.");
18149
18914
  if (recoverContextInterruptedWorkflowTurn(event, ctx)) return;
@@ -18162,14 +18927,6 @@ Public workflow commands:
18162
18927
 
18163
18928
  if ((state.mode === "validating" || state.mode === "revalidating") && state.lastWorkflowHandoff?.type === WORKFLOW_VALIDATION_RESULT_TOOL) {
18164
18929
  traceWorkflowTracking(ctx, "typed-plan-validation-agent-end-boundary", { source: "typed_validation_tool_agent_end", mode: state.mode, verdict: state.validationVerdict, nextAction: "route validation verdict" });
18165
- planForcedSubagentPreflightReconcile(ctx, "Validation");
18166
- const forcedValidationBlock = forcedSubagentUsageSatisfied(ctx, "Validation");
18167
- if (forcedValidationBlock) {
18168
- const report = state.validationReport ?? text;
18169
- if (queuePlanValidationHandoffRetry(ctx, report, forcedValidationBlock, "typed validation accepted without forced Validation evidence")) return;
18170
- blockPlanValidationHandoff(ctx, report, forcedValidationBlock);
18171
- return;
18172
- }
18173
18930
  const report = state.validationReport ?? text;
18174
18931
  const verdict = state.validationVerdict ?? normalizeValidationVerdict(extractVerdict(report), report);
18175
18932
  const validationStatus = planValidationStatusForVerdict(verdict);
@@ -18326,11 +19083,34 @@ Public workflow commands:
18326
19083
  return;
18327
19084
  }
18328
19085
 
19086
+ if (state.mode === "revalidating" && state.lastWorkflowHandoff?.type === WORKFLOW_REPAIR_RESULT_TOOL) {
19087
+ traceWorkflowTracking(ctx, "typed-plan-stale-repair-agent-end-boundary", { source: "typed_repair_tool_agent_end", mode: state.mode, nextAction: "discard stale repair handoff before validation prose fallback" });
19088
+ updateState({ lastWorkflowHandoff: undefined }, ctx);
19089
+ return;
19090
+ }
19091
+
18329
19092
  if (state.mode === "executed" && state.lastWorkflowHandoff?.type === WORKFLOW_EXECUTION_RESULT_TOOL) {
18330
19093
  const validationAvailable = planValidationModelAvailable(settings);
18331
19094
  const validateAfterExecution = planAutoValidationEnabled(settings);
18332
19095
  const validationGateActive = planValidationGateActive(settings);
18333
19096
  traceWorkflowTracking(ctx, "typed-execution-agent-end-validation-boundary", { source: "typed_execution_tool_agent_end", mode: "executed", validationAvailable, validateAfterExecution, validationGateActive });
19097
+ if (planExecutionIncomplete(state, settings)) {
19098
+ const reason = "Typed execution completed while approved Plan progress still has incomplete steps. Resume execution to complete remaining steps before validation.";
19099
+ updateState({
19100
+ mode: "reviewed",
19101
+ planExecutionStepIndex: undefined,
19102
+ planStepValidationIndex: undefined,
19103
+ validationReport: undefined,
19104
+ validationVerdict: undefined,
19105
+ lastValidationFailure: undefined,
19106
+ lastRepairStatus: "none",
19107
+ lastRepairAttempt: undefined,
19108
+ planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: "reviewed" }, settings, { lifecycleStatus: "reviewed", validationStatus: "pending", repairStatus: "none", nextAction: "continue execution" }) : state.planProgress,
19109
+ }, ctx);
19110
+ queuePlanTerminalSummary(ctx, "blocked", "Plan execution incomplete", { reason });
19111
+ showBlockedPlanRecoveryMenu(ctx);
19112
+ return;
19113
+ }
18334
19114
  if (validationAvailable && validateAfterExecution) {
18335
19115
  schedulePlanValidationAfterExecution(ctx, "typed execution agent end", "begin validation after typed execution agent end", "Typed execution completed but validation handoff failed");
18336
19116
  } else if (validationAvailable && settings.workflow.offerValidationAfterExecute !== false) {
@@ -18742,7 +19522,7 @@ Public workflow commands:
18742
19522
  return;
18743
19523
  }
18744
19524
  if (state.reviewRepairInProgress) {
18745
- recordWorkflowInternalEvent(ctx, "Reviewer-requested Plan repair text completed; accepting structurally valid repair without forced Planning retry.");
19525
+ recordWorkflowInternalEvent(ctx, "Reviewer-requested Plan repair text completed; enforcing forced Planning evidence before accepting repair.");
18746
19526
  }
18747
19527
  const requiredClarificationStillMissing = state.clarificationRequiredBeforePlan === true && !state.clarifyingAnswers?.length;
18748
19528
  if (requiredClarificationStillMissing) {
@@ -18761,6 +19541,23 @@ Public workflow commands:
18761
19541
  recordWorkflowInternalEvent(ctx, "Internal workflow lifecycle event suppressed.");
18762
19542
  return;
18763
19543
  }
19544
+ planForcedSubagentPreflightReconcile(ctx, "Planning");
19545
+ const forcedPlanningBlock = forcedSubagentUsageSatisfied(ctx, "Planning");
19546
+ if (forcedPlanningBlock) {
19547
+ updateState({
19548
+ mode: "plan_draft",
19549
+ activePlanId: state.activePlanId ?? ensureActivePlanId(ctx),
19550
+ draftPlan: text,
19551
+ approvedPlan: undefined,
19552
+ lastReviewFailure: forcedPlanningBlock,
19553
+ planProgress: workflowPlanProgressEnabled(settings)
19554
+ ? mergePlanProgress({ ...state, mode: "validated", draftPlan: text, approvedPlan: undefined }, settings, { lifecycleStatus: "blocked", nextAction: "planning blocked by forced sub-agent policy", steps: [] }, undefined)
19555
+ : state.planProgress,
19556
+ }, ctx);
19557
+ showInternal(pi, `# Sub-Agent Policy Blocked\n\n${forcedSubagentMessage("Planning", forcedPlanningBlock, "Planning")}`);
19558
+ recordWorkflowInternalEvent(ctx, "Internal workflow lifecycle event suppressed.");
19559
+ return;
19560
+ }
18764
19561
  const planOrchestratorProblem = orchestrationProblem(ctx, "plan");
18765
19562
  const planContractProblem = planDraftContractProblem(text);
18766
19563
  if (planOrchestratorProblem || planContractProblem) {
@@ -18904,8 +19701,19 @@ Public workflow commands:
18904
19701
  planForcedSubagentPreflightReconcile(ctx, "Execution");
18905
19702
  if (!allTrackedStepsCompleted && blockIfForcedSubagentsMissing(ctx, "Execution")) {
18906
19703
  const reason = "Execution stopped before all approved Plan steps were tracked and the forced execution worker requirement was not satisfied.";
18907
- updateState({ mode: "validated", executionSummary: text, validationReport: reason, validationVerdict: "UNKNOWN", planExecutionStepIndex: undefined, planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: "validated", validationVerdict: "UNKNOWN" }, settings, { lifecycleStatus: "blocked", validationStatus: "unknown", nextAction: "fix execution progress/sub-agent blocker then /plan continue" }) : state.planProgress }, ctx);
18908
- queuePlanTerminalSummary(ctx, "blocked", "Plan execution blocked", { validationText: reason, verdict: "UNKNOWN", reason });
19704
+ updateState({
19705
+ mode: "reviewed",
19706
+ executionSummary: text,
19707
+ validationReport: undefined,
19708
+ validationVerdict: undefined,
19709
+ lastValidationFailure: undefined,
19710
+ lastRepairStatus: "none",
19711
+ lastRepairAttempt: undefined,
19712
+ planExecutionStepIndex: undefined,
19713
+ planStepValidationIndex: undefined,
19714
+ planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: "reviewed" }, settings, { lifecycleStatus: "reviewed", validationStatus: "pending", repairStatus: "none", nextAction: "fix execution progress/sub-agent blocker then /plan continue" }) : state.planProgress,
19715
+ }, ctx);
19716
+ queuePlanTerminalSummary(ctx, "blocked", "Plan execution blocked", { reason });
18909
19717
  return;
18910
19718
  }
18911
19719
  const progressWarning = !allTrackedStepsCompleted && settings.workflow.validateAfterEachStep !== true && settings.workflow.requireApprovalPerStep !== true && workflowPlanProgressEnabled(settings) && state.planProgress?.steps.length
@@ -18945,6 +19753,23 @@ Public workflow commands:
18945
19753
  return;
18946
19754
  }
18947
19755
  }
19756
+ if (planExecutionIncomplete(state, settings)) {
19757
+ const reason = "Execution summary arrived while approved Plan progress still has incomplete steps. Resume execution to complete remaining steps before validation.";
19758
+ updateState({
19759
+ mode: "reviewed",
19760
+ planExecutionStepIndex: undefined,
19761
+ planStepValidationIndex: undefined,
19762
+ validationReport: undefined,
19763
+ validationVerdict: undefined,
19764
+ lastValidationFailure: undefined,
19765
+ lastRepairStatus: "none",
19766
+ lastRepairAttempt: undefined,
19767
+ planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: "reviewed" }, settings, { lifecycleStatus: "reviewed", validationStatus: "pending", repairStatus: "none", nextAction: "continue execution" }) : state.planProgress,
19768
+ }, ctx);
19769
+ queuePlanTerminalSummary(ctx, "blocked", "Plan execution incomplete", { reason });
19770
+ showBlockedPlanRecoveryMenu(ctx);
19771
+ return;
19772
+ }
18948
19773
  if (validationAvailable && validateAfterExecution) {
18949
19774
  schedulePlanValidationAfterExecution(ctx, "execution complete", "begin validation after execution", "Execution completed but validation handoff failed");
18950
19775
  } else if (validationAvailable && settings.workflow.offerValidationAfterExecute !== false) {
@@ -18969,10 +19794,10 @@ Public workflow commands:
18969
19794
  const repairWorkers = workerCount(settings, "Repair");
18970
19795
  const repairRequired = workerTargetForPolicy(repairPolicy, repairWorkers);
18971
19796
  const availableRepairAgents = listEffectiveAgents(ctx.cwd);
18972
- const repairSuitable = repairPolicy === "forced" ? chooseForcedSubagents("Repair", repairRequired, "Repair", availableRepairAgents) : [];
19797
+ const repairSuitable = subagentPolicyRequiresRequiredEvidence(repairPolicy) ? chooseForcedSubagents("Repair", repairRequired, "Repair", availableRepairAgents) : [];
18973
19798
  const hasWriteCapableSubagents = repairSuitable.some(a => subagentToolsAllowMutation(a.tools));
18974
19799
  if (hasWriteCapableSubagents) {
18975
- const required = workerTargetForPolicy("forced", workerCount(settings, "Repair"));
19800
+ const required = workerTargetForPolicy(repairPolicy, workerCount(settings, "Repair"));
18976
19801
  const observed = subagentUsageByPhase.Repair ?? 0;
18977
19802
  updateState({ mode: "validated", lastRepairStatus: "blocked", lastRepairAttempt: compact(text, 1200), repairHistory: appendWorkflowRepairHistory({ timestamp: new Date().toISOString(), retry: state.currentValidationRetry ?? 0, status: "blocked", repairSummary: compact(text, 800), nextAction: `Repair summary saved, but revalidation blocked by forced repair sub-agent policy (${observed}/${required} workers).` }), planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: "validated" }, settings, { lifecycleStatus: "blocked", repairStatus: "blocked", nextAction: "repair summary saved; forced repair sub-agents missing" }) : state.planProgress }, ctx);
18978
19803
  return;
@@ -18989,6 +19814,22 @@ Public workflow commands:
18989
19814
  recordWorkflowInternalEvent(ctx, "Internal workflow lifecycle event suppressed.");
18990
19815
  return;
18991
19816
  }
19817
+ if (!planValidationBoundaryReached(state, settings)) {
19818
+ updateState({
19819
+ mode: "reviewed",
19820
+ executionSummary: `${state.executionSummary ?? ""}\n\nRepair summary received before validation boundary (not revalidated):\n${text}`.trim(),
19821
+ lastRepairStatus: "none",
19822
+ lastRepairAttempt: undefined,
19823
+ repairRetryState: { ...(state.repairRetryState ?? {}), validation: undefined },
19824
+ validationReport: undefined,
19825
+ validationVerdict: undefined,
19826
+ lastValidationFailure: undefined,
19827
+ planStepValidationIndex: undefined,
19828
+ planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: "reviewed" }, settings, { lifecycleStatus: "reviewed", validationStatus: "pending", repairStatus: "none", nextAction: "continue execution" }) : state.planProgress,
19829
+ }, ctx);
19830
+ show(pi, "# Plan Repair Ignored\n\nApproved Plan execution is incomplete. Repair/revalidation is not available until the configured validation boundary is reached.\n\nUse /plan continue.");
19831
+ return;
19832
+ }
18992
19833
  updateState({ mode: "revalidating", executionSummary: `${state.executionSummary ?? ""}\n\nRepair summary:\n${text}`.trim(), lastRepairStatus: "completed", repairRetryState: { ...(state.repairRetryState ?? {}), validation: state.repairRetryState?.validation ? { ...state.repairRetryState.validation, status: "completed", inProgress: false, lastAttempt: compact(text, 1200) } : undefined }, planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: "revalidating", lastRepairStatus: "completed" }, settings, { lifecycleStatus: "revalidating", validationStatus: "running", repairStatus: "completed", nextAction: "validation result" }) : state.planProgress, lastRepairAttempt: compact(text, 1200), repairHistory: appendWorkflowRepairHistory({ timestamp: new Date().toISOString(), retry: state.currentValidationRetry ?? 0, status: "completed", repairSummary: compact(text, 800), nextAction: "Revalidate repaired Plan Mode workflow." }) }, ctx);
18993
19834
  deferWorkflowAction(pi, "begin revalidation after repair", async () => {
18994
19835
  const revalidationStarted = await beginValidation(ctx, true, true);
@@ -19000,11 +19841,19 @@ Public workflow commands:
19000
19841
  // ── Validation complete ──
19001
19842
  if (state.mode === "validating" || state.mode === "revalidating") {
19002
19843
  if (state.lastWorkflowHandoff?.type === WORKFLOW_VALIDATION_RESULT_TOOL) return;
19003
- planForcedSubagentPreflightReconcile(ctx, "Validation");
19004
- const forcedValidationBlock = forcedSubagentUsageSatisfied(ctx, "Validation");
19005
- if (forcedValidationBlock) {
19006
- if (queuePlanValidationHandoffRetry(ctx, text, forcedValidationBlock, "validation completed without forced Validation evidence")) return;
19007
- blockPlanValidationHandoff(ctx, text, forcedValidationBlock);
19844
+ if (!planValidationBoundaryReached(state, settings)) {
19845
+ const reason = "Approved Plan execution is incomplete. Validation output was ignored until the configured validation boundary is reached.";
19846
+ updateState({
19847
+ mode: "reviewed",
19848
+ validationReport: undefined,
19849
+ validationVerdict: undefined,
19850
+ lastValidationFailure: undefined,
19851
+ lastRepairStatus: "none",
19852
+ lastRepairAttempt: undefined,
19853
+ planStepValidationIndex: undefined,
19854
+ planProgress: workflowPlanProgressEnabled(settings) ? mergePlanProgress({ ...state, mode: "reviewed" }, settings, { lifecycleStatus: "reviewed", validationStatus: "pending", repairStatus: "none", nextAction: "continue execution" }) : state.planProgress,
19855
+ }, ctx);
19856
+ show(pi, `# Plan Validation Ignored\n\n${reason}\n\nUse /plan continue.`);
19008
19857
  return;
19009
19858
  }
19010
19859
  const verdict = normalizeValidationVerdict(extractVerdict(text), text);