aiwcli 0.12.3 → 0.12.7

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.
Files changed (125) hide show
  1. package/bin/dev.cmd +3 -3
  2. package/bin/dev.js +16 -16
  3. package/bin/run.cmd +3 -3
  4. package/bin/run.js +21 -21
  5. package/dist/commands/branch.js +7 -2
  6. package/dist/lib/bmad-installer.js +37 -37
  7. package/dist/lib/terminal.d.ts +2 -0
  8. package/dist/lib/terminal.js +57 -7
  9. package/dist/templates/CLAUDE.md +205 -205
  10. package/dist/templates/_shared/.claude/commands/handoff-resume.md +12 -64
  11. package/dist/templates/_shared/.claude/commands/handoff.md +12 -198
  12. package/dist/templates/_shared/.claude/settings.json +65 -65
  13. package/dist/templates/_shared/.codex/workflows/handoff.md +226 -226
  14. package/dist/templates/_shared/.windsurf/workflows/handoff.md +226 -226
  15. package/dist/templates/_shared/handoff-system/CLAUDE.md +421 -0
  16. package/dist/templates/_shared/{lib-ts/handoff → handoff-system/lib}/document-generator.ts +215 -216
  17. package/dist/templates/_shared/{lib-ts/handoff → handoff-system/lib}/handoff-reader.ts +157 -158
  18. package/dist/templates/_shared/{scripts → handoff-system/scripts}/resume_handoff.ts +373 -373
  19. package/dist/templates/_shared/{scripts → handoff-system/scripts}/save_handoff.ts +469 -358
  20. package/dist/templates/_shared/handoff-system/workflows/handoff-resume.md +66 -0
  21. package/dist/templates/_shared/{workflows → handoff-system/workflows}/handoff.md +254 -254
  22. package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -2
  23. package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -159
  24. package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -147
  25. package/dist/templates/_shared/hooks-ts/file-suggestion.ts +128 -128
  26. package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -49
  27. package/dist/templates/_shared/hooks-ts/session_end.ts +196 -183
  28. package/dist/templates/_shared/hooks-ts/session_start.ts +163 -151
  29. package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -48
  30. package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -74
  31. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +93 -93
  32. package/dist/templates/_shared/lib-ts/CLAUDE.md +367 -367
  33. package/dist/templates/_shared/lib-ts/base/atomic-write.ts +138 -138
  34. package/dist/templates/_shared/lib-ts/base/constants.ts +303 -303
  35. package/dist/templates/_shared/lib-ts/base/git-state.ts +58 -58
  36. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +582 -582
  37. package/dist/templates/_shared/lib-ts/base/inference.ts +301 -301
  38. package/dist/templates/_shared/lib-ts/base/logger.ts +247 -247
  39. package/dist/templates/_shared/lib-ts/base/state-io.ts +202 -130
  40. package/dist/templates/_shared/lib-ts/base/stop-words.ts +184 -184
  41. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +56 -0
  42. package/dist/templates/_shared/lib-ts/base/utils.ts +184 -184
  43. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +566 -560
  44. package/dist/templates/_shared/lib-ts/context/context-selector.ts +524 -515
  45. package/dist/templates/_shared/lib-ts/context/context-store.ts +712 -668
  46. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +312 -312
  47. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +185 -185
  48. package/dist/templates/_shared/lib-ts/package.json +20 -20
  49. package/dist/templates/_shared/lib-ts/templates/formatters.ts +102 -102
  50. package/dist/templates/_shared/lib-ts/templates/plan-context.ts +58 -58
  51. package/dist/templates/_shared/lib-ts/tsconfig.json +13 -13
  52. package/dist/templates/_shared/lib-ts/types.ts +186 -180
  53. package/dist/templates/_shared/scripts/resolve_context.ts +33 -33
  54. package/dist/templates/_shared/scripts/status_line.ts +690 -690
  55. package/dist/templates/cc-native/.claude/commands/{rlm → cc-native/rlm}/ask.md +136 -136
  56. package/dist/templates/cc-native/.claude/commands/{rlm → cc-native/rlm}/index.md +21 -21
  57. package/dist/templates/cc-native/.claude/commands/{rlm → cc-native/rlm}/overview.md +56 -56
  58. package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +10 -10
  59. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fix.md +8 -8
  60. package/dist/templates/cc-native/.windsurf/workflows/cc-native/implement.md +8 -8
  61. package/dist/templates/cc-native/.windsurf/workflows/cc-native/research.md +8 -8
  62. package/dist/templates/cc-native/CC-NATIVE-README.md +189 -189
  63. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +304 -304
  64. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +143 -143
  65. package/dist/templates/cc-native/_cc-native/agents/PLAN-ORCHESTRATOR.md +213 -213
  66. package/dist/templates/cc-native/_cc-native/agents/plan-questions/PLAN-QUESTIONER.md +70 -70
  67. package/dist/templates/cc-native/_cc-native/cc-native.config.json +96 -96
  68. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +247 -247
  69. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +76 -76
  70. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_subagent.ts +54 -54
  71. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_write.ts +51 -51
  72. package/dist/templates/cc-native/_cc-native/hooks/mark_questions_asked.ts +53 -53
  73. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -61
  74. package/dist/templates/cc-native/_cc-native/lib-ts/agent-selection.ts +163 -163
  75. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +156 -156
  76. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/format.ts +597 -597
  77. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/index.ts +26 -26
  78. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/tracker.ts +107 -107
  79. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/write.ts +119 -119
  80. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +21 -21
  81. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +319 -319
  82. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -144
  83. package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -57
  84. package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -83
  85. package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +119 -119
  86. package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +79 -79
  87. package/dist/templates/cc-native/_cc-native/lib-ts/graduation.ts +132 -132
  88. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +116 -116
  89. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -168
  90. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +70 -70
  91. package/dist/templates/cc-native/_cc-native/lib-ts/output-builder.ts +130 -130
  92. package/dist/templates/cc-native/_cc-native/lib-ts/plan-discovery.ts +80 -80
  93. package/dist/templates/cc-native/_cc-native/lib-ts/plan-enhancement.ts +41 -41
  94. package/dist/templates/cc-native/_cc-native/lib-ts/plan-questions.ts +101 -101
  95. package/dist/templates/cc-native/_cc-native/lib-ts/review-pipeline.ts +511 -511
  96. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +71 -71
  97. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/base/base-agent.ts +217 -217
  98. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +12 -12
  99. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/claude-agent.ts +66 -65
  100. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/codex-agent.ts +184 -184
  101. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/gemini-agent.ts +39 -39
  102. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +196 -195
  103. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/schemas.ts +201 -201
  104. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +21 -21
  105. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/CLAUDE.md +480 -480
  106. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/embedding-indexer.ts +287 -287
  107. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/hyde.ts +148 -148
  108. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/index.ts +54 -54
  109. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/logger.ts +58 -58
  110. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/ollama-client.ts +208 -208
  111. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/retrieval-pipeline.ts +460 -460
  112. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-indexer.ts +446 -447
  113. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-loader.ts +280 -280
  114. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-searcher.ts +274 -274
  115. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/types.ts +201 -201
  116. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/vector-store.ts +278 -278
  117. package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +184 -184
  118. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +275 -275
  119. package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -18
  120. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +329 -329
  121. package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -72
  122. package/dist/templates/cc-native/_cc-native/workflows/specdev.md +9 -9
  123. package/oclif.manifest.json +1 -1
  124. package/package.json +108 -108
  125. package/dist/templates/cc-native/_cc-native/lib-ts/nul +0 -3
@@ -1,511 +1,511 @@
1
- /**
2
- * Review pipeline: orchestrates the full plan review lifecycle.
3
- * Wires together plan-discovery, settings, agent-selection, graduation,
4
- * output-builder, and existing modules (orchestrator, corroboration, etc.).
5
- */
6
-
7
- import * as fs from "node:fs";
8
- import * as path from "node:path";
9
-
10
- import {
11
- logDebug,
12
- logInfo,
13
- logWarn,
14
- logError,
15
- } from "../../_shared/lib-ts/base/logger.js";
16
- import { logDiagnostic } from "../../_shared/lib-ts/base/hook-utils.js";
17
- import { eprint } from "../../_shared/lib-ts/base/utils.js";
18
- import { getContextReviewsDir, getContextDir, getReviewFolderPath } from "../../_shared/lib-ts/base/constants.js";
19
- import { getContextBySessionId, getAllContexts } from "../../_shared/lib-ts/context/context-store.js";
20
-
21
- import type {
22
- AgentConfig,
23
- OrchestratorConfig,
24
- ReviewerResult,
25
- CombinedReviewResult,
26
- OrchestratorResult,
27
- IterationState,
28
- PipelineInput,
29
- PipelineResult,
30
- } from "./types.js";
31
- import { REVIEW_SCHEMA } from "./types.js";
32
- import type { ContextState } from "../../_shared/lib-ts/types.js";
33
-
34
- import { discoverPlan } from "./plan-discovery.js";
35
- import { loadSettings, loadModelsConfig, loadAgentLibrary, DEFAULT_ORCHESTRATOR } from "./settings.js";
36
- import { resolveMandatoryAgents, assignModelsToAgents, selectAgents } from "./agent-selection.js";
37
- import { computePassEligible, extractTopIssuesForTracker, advanceIterationState } from "./graduation.js";
38
- import { truncateAgentIssues, overrideVerdictsByThreshold, buildReviewOutput } from "./output-builder.js";
39
- import { DEFAULT_REVIEW_ITERATIONS, loadIterationState, saveIterationState } from "./state.js";
40
-
41
- import {
42
- isPlanAlreadyReviewed,
43
- wasPlanPreviouslyDenied,
44
- getLastPlanReview,
45
- markPlanReviewed,
46
- wasPlanQuestionsAgentAsked,
47
- markQuestionsAsked,
48
- resetPlanQuestionsAsked,
49
- } from "./cc-native-state.js";
50
-
51
- import { computeCorroboratedDecision } from "./corroboration.js";
52
- import { runOrchestrator } from "./orchestrator.js";
53
- import { debugLog } from "./debug.js";
54
- import { writeCombinedArtifacts, buildCorroborationReport, buildHighIssuesDocument, writeReviewTracker } from "./artifacts.js";
55
- import type { ReviewTrackerEntry } from "./artifacts.js";
56
- import { runAgentReview } from "./reviewers/index.js";
57
- import { runPlanQuestions } from "./plan-questions.js";
58
-
59
- const HOOK = "review-pipeline";
60
-
61
- // ---------------------------------------------------------------------------
62
- // Context Lookup (private — only used here)
63
- // ---------------------------------------------------------------------------
64
-
65
- function getActiveContextForReview(sessionId: string, projectRoot: string): ContextState | null {
66
- const ctx = getContextBySessionId(sessionId, projectRoot);
67
- if (ctx) {
68
- logInfo(HOOK, `Found context by session_id: ${ctx.id}`);
69
- return ctx;
70
- }
71
- const allActive = getAllContexts("active", projectRoot);
72
- const planning = allActive.filter(c => c.mode === "active" || c.mode === "has_plan");
73
- if (planning.length === 1) {
74
- logInfo(HOOK, `Found single planning context: ${planning[0]!.id}`);
75
- return planning[0]!;
76
- }
77
- if (planning.length > 1) {
78
- logWarn(HOOK, `Multiple planning contexts (${planning.length}), cannot determine which to use`);
79
- } else if (allActive.length > 0) {
80
- logInfo(HOOK, `Found ${allActive.length} active context(s) but none in planning mode`);
81
- } else {
82
- logInfo(HOOK, "No active contexts found");
83
- }
84
- return null;
85
- }
86
-
87
- // ---------------------------------------------------------------------------
88
- // Pipeline
89
- // ---------------------------------------------------------------------------
90
-
91
- export async function runReviewPipeline(input: PipelineInput): Promise<PipelineResult> {
92
- const { sessionId, base, aiwcliDir, transcriptPath, payload } = input;
93
-
94
- // 1. Load settings
95
- const settings = loadSettings(aiwcliDir);
96
- const planSettings = settings.planReview ?? {};
97
- const agentSettings = settings.agentReview ?? {};
98
-
99
- const planReviewEnabled = planSettings.enabled ?? true;
100
- const agentReviewEnabled = agentSettings.enabled ?? true;
101
-
102
- if (!planReviewEnabled && !agentReviewEnabled) {
103
- return { action: "skip", reason: "Both plan and agent review disabled" };
104
- }
105
-
106
- // 2. Discover plan
107
- const discovered = discoverPlan(transcriptPath);
108
- if (!discovered) {
109
- return { action: "skip", reason: "No plan file found in ~/.claude/plans/. The plan may not have been written yet." };
110
- }
111
-
112
- const { content: plan, hash: planHash, path: planPath } = discovered;
113
-
114
- // 3. Find active context (moved before questions gate for plan path change detection)
115
- const activeContext = getActiveContextForReview(sessionId, base);
116
- if (!activeContext) {
117
- return { action: "skip", reason: "No active planning context found for this session." };
118
- }
119
-
120
- const contextId = activeContext.id;
121
- const reviewsDir = path.join(getContextReviewsDir(contextId, base), "cc-native");
122
- const contextPath = getContextDir(contextId, base);
123
-
124
- // 4a. Load iteration state
125
- let iterationState: IterationState | null = loadIterationState(reviewsDir);
126
-
127
- // 4a-migration. Backfill sessionId for old iteration files
128
- if (iterationState && !iterationState.sessionId) {
129
- logInfo(HOOK, `Migrating iteration state: adding sessionId=${sessionId}`);
130
- iterationState.sessionId = sessionId;
131
- saveIterationState(reviewsDir, iterationState); // Persist migration
132
- }
133
-
134
- // 4a-session. Detect session change — reset iteration state for new planning session
135
- if (iterationState && iterationState.sessionId && iterationState.sessionId !== sessionId) {
136
- logInfo(HOOK, `Session changed (${iterationState.sessionId} → ${sessionId}), resetting iteration state`);
137
- iterationState = null; // Force fresh state creation below
138
- }
139
-
140
- // 4a-init. Initialize if null
141
- if (!iterationState) {
142
- iterationState = {
143
- current: 1, max: 1, complexity: "medium",
144
- history: [], graduated: [], passStreaks: {},
145
- lastPlanHash: "", lastPlanPath: "",
146
- sessionId: sessionId,
147
- };
148
- saveIterationState(reviewsDir, iterationState);
149
- }
150
-
151
- // 4b. Detect plan file change — reset iteration state for new plan topic
152
- const lastPath = iterationState.lastPlanPath ?? "";
153
- if (lastPath && lastPath !== planPath) {
154
- logInfo(HOOK, `Plan file changed (${path.basename(lastPath)}→${path.basename(planPath)}), resetting iteration state for new plan`);
155
- iterationState = {
156
- current: 1, max: 1, complexity: "medium",
157
- history: [], graduated: [], passStreaks: {},
158
- lastPlanHash: "", lastPlanPath: "",
159
- sessionId: sessionId,
160
- };
161
- saveIterationState(reviewsDir, iterationState);
162
- resetPlanQuestionsAsked(sessionId, base);
163
- }
164
-
165
- // 5. Questions gate
166
- if (!wasPlanQuestionsAgentAsked(sessionId, base)) {
167
- logInfo(HOOK, "Questions gate: plan-questions agent has not run yet, running now");
168
- const timeout = typeof agentSettings.timeout === "number" ? agentSettings.timeout : 120;
169
- const questionsResult = await runPlanQuestions(plan, aiwcliDir, timeout, undefined, sessionId);
170
-
171
- markQuestionsAsked(sessionId, base, "agent");
172
-
173
- const hasQuestions = questionsResult && (
174
- questionsResult.questions.length > 0 ||
175
- questionsResult.assumptions.length > 0 ||
176
- questionsResult.ambiguities.length > 0
177
- );
178
-
179
- if (hasQuestions) {
180
- const questionsList = questionsResult.questions.map((q: string, i: number) => `${i + 1}. ${q}`).join("\n");
181
- const assumptionsList = questionsResult.assumptions.length > 0
182
- ? `\n\nAssumptions detected:\n${questionsResult.assumptions.map((a: string) => `- ${a}`).join("\n")}`
183
- : "";
184
- const ambiguitiesList = questionsResult.ambiguities.length > 0
185
- ? `\n\nAmbiguities detected:\n${questionsResult.ambiguities.map((a: string) => `- ${a}`).join("\n")}`
186
- : "";
187
- const contextMsg = `## Plan Questions (from independent review)\n\nAn agent reviewed your plan in a fresh context — without access to your session history or codebase exploration. It identified these questions:\n\n${questionsList}${assumptionsList}${ambiguitiesList}\n\nAsk the user these questions using AskUserQuestion before submitting the plan.`;
188
- return {
189
- action: "block",
190
- contextText: contextMsg,
191
- blockReason: "Ask the user clarifying questions before submitting the plan. Use AskUserQuestion with the questions above.",
192
- };
193
- }
194
-
195
- logInfo(HOOK, "Questions gate: no questions generated, proceeding to review");
196
- } else {
197
- logInfo(HOOK, "Questions gate: agent already ran, skipping");
198
- }
199
-
200
- // 6. Hash + dedup
201
- logDiagnostic(HOOK, "receive", `plan_size=${plan.length}, session=${sessionId.slice(0, 8)}`, {
202
- inputs: { plan_hash: planHash, plan_size: plan.length, session_id: sessionId.slice(0, 12) },
203
- });
204
-
205
- // Plan-hash deduplication
206
- logDebug(HOOK, `Plan hash: ${planHash}`);
207
- if (isPlanAlreadyReviewed(sessionId, planHash, base)) {
208
- const lastReview = getLastPlanReview(sessionId, planHash, base);
209
-
210
- if (wasPlanPreviouslyDenied(sessionId, planHash, base)) {
211
- return {
212
- action: "block",
213
- contextText: "[Plan Review] Plan content unchanged since last review which found issues.",
214
- blockReason: "Plan unchanged since denial. Modify the plan to address review findings, then attempt ExitPlanMode again.",
215
- };
216
- } else {
217
- const verdict = lastReview?.iteration?.latest_verdict || "pass";
218
- return { action: "skip", reason: `Plan already reviewed (verdict: ${verdict}). Skipping re-review.` };
219
- }
220
- }
221
-
222
- // 7. Iteration bounds check
223
- if (iterationState.current > iterationState.max) {
224
- return { action: "skip", reason: `Max review iterations reached (${iterationState.current - 1}/${iterationState.max}), allowing plan through.` };
225
- }
226
-
227
- // Initialize result containers
228
- let orchResult: OrchestratorResult | null = null;
229
- const agentResults: Record<string, ReviewerResult> = {};
230
- let detectedComplexity = "medium";
231
-
232
- // 7. PHASE 1: Orchestrator
233
- const graduatedSet = new Set(iterationState.graduated);
234
- if (graduatedSet.size > 0) {
235
- logInfo(HOOK, `Graduated agents from previous iterations: ${[...graduatedSet].sort().join(", ")}`);
236
- }
237
-
238
- const agentLibrary = agentReviewEnabled ? loadAgentLibrary(aiwcliDir, agentSettings) : [];
239
- const originalAgentCount = agentLibrary.length;
240
- const enabledAgents = agentLibrary.filter(a => !graduatedSet.has(a.name));
241
- const timeout = typeof agentSettings.timeout === "number" ? agentSettings.timeout : 120;
242
- const legacyMode = agentSettings.legacyMode === true;
243
-
244
- const orchSettings = agentSettings.orchestrator ?? DEFAULT_ORCHESTRATOR;
245
- const orchestratorConfig: OrchestratorConfig = {
246
- enabled: (orchSettings.enabled ?? true) && agentReviewEnabled,
247
- model: orchSettings.model ?? "haiku",
248
- timeout: orchSettings.timeout ?? 30,
249
- };
250
-
251
- const mandatoryConfig = agentSettings.mandatoryAgents ?? ["handoff-readiness", "clarity-auditor", "skeptic"];
252
- const alwaysMandatory = resolveMandatoryAgents(mandatoryConfig, "simple");
253
-
254
- logDebug(HOOK, `Agent library: ${agentLibrary.map(a => a.name)}`);
255
- logDebug(HOOK, `Mandatory agents: ${[...alwaysMandatory].sort()}`);
256
- logDebug(HOOK, `Orchestrator enabled: ${orchestratorConfig.enabled}`);
257
-
258
- const phase1Promises: Array<{ name: string; promise: Promise<ReviewerResult | OrchestratorResult> }> = [];
259
-
260
- if (orchestratorConfig.enabled && enabledAgents.length > 0 && !legacyMode) {
261
- phase1Promises.push({
262
- name: "orchestrator",
263
- promise: runOrchestrator(plan, enabledAgents, orchestratorConfig, agentSettings, alwaysMandatory),
264
- });
265
- }
266
-
267
- logInfo(HOOK, `=== PHASE 1: Running ${phase1Promises.length} tasks in parallel ===`);
268
-
269
- if (phase1Promises.length > 0) {
270
- const results = await Promise.allSettled(
271
- phase1Promises.map(async ({ name, promise }) => {
272
- const result = await promise;
273
- return { name, result };
274
- }),
275
- );
276
- for (const [i, r] of results.entries()) {
277
- if (r.status === "fulfilled") {
278
- if (r.value.name === "orchestrator") orchResult = r.value.result as OrchestratorResult;
279
- logInfo(HOOK, `${r.value.name} completed`);
280
- } else {
281
- const failedName = phase1Promises[i]?.name ?? "unknown";
282
- logError(HOOK, `${failedName} failed: ${r.reason}`);
283
- }
284
- }
285
- }
286
-
287
- // 8. PHASE 2: Agent Selection
288
- let selectedAgents: AgentConfig[] = [];
289
- let mandatoryNames = alwaysMandatory;
290
-
291
- if (agentReviewEnabled) {
292
- logInfo(HOOK, "=== PHASE 2: Agent Selection ===");
293
-
294
- const selectionResult = selectAgents({
295
- enabledAgents,
296
- orchResult,
297
- mandatoryConfig,
298
- agentSettings,
299
- legacyMode,
300
- });
301
-
302
- selectedAgents = selectionResult.selectedAgents;
303
- mandatoryNames = selectionResult.mandatoryNames;
304
- detectedComplexity = selectionResult.detectedComplexity;
305
-
306
- logDiagnostic(HOOK, "decide", `Selected ${selectedAgents.length} agents, complexity=${detectedComplexity}`, {
307
- decision: "agents_selected",
308
- reasoning: `orchestrator=${orchResult !== null}, legacy=${legacyMode}`,
309
- inputs: {
310
- agents: selectedAgents.map(a => a.name),
311
- complexity: detectedComplexity,
312
- mandatory_count: selectedAgents.filter(a => mandatoryNames.has(a.name)).length,
313
- },
314
- });
315
-
316
- // Update iteration state with complexity/max
317
- const reviewIterations: Record<string, number> = {
318
- ...DEFAULT_REVIEW_ITERATIONS,
319
- ...(agentSettings.reviewIterations ?? {}),
320
- };
321
- iterationState.complexity = detectedComplexity;
322
- iterationState.max = reviewIterations[detectedComplexity] ?? iterationState.max;
323
- logDebug(HOOK, `Iteration state: ${iterationState.current}/${iterationState.max} (${detectedComplexity})`);
324
-
325
- // Assign random providers + models
326
- const modelsConfig = loadModelsConfig(settings);
327
- selectedAgents = assignModelsToAgents(selectedAgents, modelsConfig);
328
- logInfo(HOOK, `Model assignments: ${selectedAgents.map(a => `${a.name}→${a.provider}:${a.model}`).join(", ")}`);
329
-
330
- // 9. PHASE 3: Run agents
331
- if (selectedAgents.length > 0) {
332
- logInfo(HOOK, "=== PHASE 3: Agent Reviews ===");
333
- logInfo(HOOK, `Launching ${selectedAgents.length} agents in parallel`);
334
-
335
- debugLog(contextPath, sessionId, "hook", "agent_review_start", {
336
- agents: selectedAgents.map(a => a.name),
337
- timeout,
338
- complexity: detectedComplexity,
339
- });
340
-
341
- const agentPromises = selectedAgents.map(async agent => {
342
- const result = await runAgentReview(plan, agent, REVIEW_SCHEMA, timeout, contextPath, sessionId);
343
- return { agent, result };
344
- });
345
-
346
- const agentSettled = await Promise.allSettled(agentPromises);
347
- for (const [i, r] of agentSettled.entries()) {
348
- if (r.status === "fulfilled") {
349
- const { agent, result } = r.value;
350
- agentResults[agent.name] = result;
351
- logInfo(HOOK, `${agent.name} completed with verdict: ${result.verdict}`);
352
- } else {
353
- const failedAgent = selectedAgents[i]!;
354
- logError(HOOK, `${failedAgent.name} failed with exception: ${r.reason}`);
355
- agentResults[failedAgent.name] = {
356
- name: failedAgent.name,
357
- ok: false,
358
- verdict: "error",
359
- data: {},
360
- raw: "",
361
- err: String(r.reason),
362
- };
363
- }
364
- }
365
- }
366
- }
367
-
368
- // 10. Issue truncation + verdict override
369
- const maxIssuesPerAgent = typeof agentSettings.maxIssuesPerAgent === "number"
370
- ? agentSettings.maxIssuesPerAgent : 3;
371
- truncateAgentIssues(agentResults, maxIssuesPerAgent);
372
-
373
- const passEligible = computePassEligible(agentResults);
374
- if (passEligible.length > 0) {
375
- logInfo(HOOK, `Pass-eligible agents this iteration: ${passEligible.join(", ")}`);
376
- }
377
-
378
- const highIssueThreshold = typeof agentSettings.highIssueThreshold === "number" ? agentSettings.highIssueThreshold : 3;
379
- overrideVerdictsByThreshold(agentResults, highIssueThreshold);
380
-
381
- // PHASE 4: Generate Output
382
- logInfo(HOOK, "=== PHASE 4: Generate Output ===");
383
-
384
- if (Object.keys(agentResults).length === 0) {
385
- if (graduatedSet.size > 0 && originalAgentCount > 0) {
386
- return { action: "skip", reason: "All agent reviewers graduated from previous iterations — no review needed." };
387
- }
388
- return { action: "skip", reason: "All reviewers failed to produce results. Check stderr logs for details." };
389
- }
390
-
391
- // 11. Corroboration
392
- const corroborationResult = computeCorroboratedDecision(agentResults);
393
- const overall = corroborationResult.verdict;
394
-
395
- const combinedResult: CombinedReviewResult = {
396
- plan_hash: planHash,
397
- overall_verdict: overall,
398
- orchestration: orchResult,
399
- agents: agentResults,
400
- timestamp: new Date().toISOString(),
401
- };
402
-
403
- const displaySettings = {
404
- ...(planSettings.display ?? {}),
405
- ...(agentSettings.display ?? {}),
406
- };
407
- const combinedSettings = { display: displaySettings };
408
-
409
- const currentIteration = iterationState.current;
410
-
411
- // 12. Write artifacts
412
- const reviewFolder = getReviewFolderPath(contextId, currentIteration, base);
413
- fs.mkdirSync(reviewFolder, { recursive: true });
414
- logInfo(HOOK, `Created review folder: ${reviewFolder}`);
415
-
416
- const reviewFile = writeCombinedArtifacts(
417
- base, plan, combinedResult, payload, combinedSettings,
418
- undefined, reviewFolder, currentIteration, corroborationResult,
419
- );
420
- logInfo(HOOK, `Saved review: ${reviewFile}`);
421
-
422
- const corroborationReport = buildCorroborationReport(corroborationResult);
423
- const corroborationPath = path.join(reviewFolder, "corroboration.md");
424
- fs.writeFileSync(corroborationPath, corroborationReport, "utf-8");
425
- logInfo(HOOK, `Saved corroboration report: ${corroborationPath}`);
426
-
427
- try {
428
- fs.writeFileSync(path.join(reviewFolder, "plan.md"), plan, "utf-8");
429
- logDebug(HOOK, `Saved plan snapshot: ${path.join(reviewFolder, "plan.md")}`);
430
- } catch (e) {
431
- logWarn(HOOK, `Failed to save plan snapshot: ${e}`);
432
- }
433
-
434
- // Build high-issues document
435
- const highIssuesDoc = buildHighIssuesDocument(combinedResult, corroborationResult);
436
- const highIssuesPath = path.join(reviewFolder, "high-issues.md");
437
- fs.writeFileSync(highIssuesPath, highIssuesDoc, "utf-8");
438
-
439
- // 15. Build output
440
- const shouldDeny = corroborationResult.blocking.length > 0;
441
- const reviewScore = shouldDeny ? 1.0 : 0.0;
442
- const denyReason = shouldDeny ? "corroborated_issues" : "no_corroboration";
443
-
444
- logInfo(HOOK, `REVIEW_DECISION: verdict=${combinedResult.overall_verdict}, deny=${shouldDeny}, reason=${denyReason}, score=${reviewScore.toFixed(2)}`);
445
- logDiagnostic(HOOK, "result", `verdict=${combinedResult.overall_verdict}, deny=${shouldDeny}, reason=${denyReason}`, {
446
- decision: shouldDeny ? "deny" : "allow",
447
- reasoning: `reason=${denyReason}, score=${reviewScore.toFixed(2)}`,
448
- inputs: {
449
- overall_verdict: combinedResult.overall_verdict,
450
- review_score: Math.round(reviewScore * 100) / 100,
451
- agent_count: Object.keys(agentResults).length,
452
- },
453
- });
454
-
455
- // Terminal progress
456
- const verdictEmoji = shouldDeny ? "❌" : "✅";
457
- eprint(`[plan-review] ${verdictEmoji} ${combinedResult.overall_verdict.toUpperCase()} (score=${reviewScore.toFixed(2)})`);
458
- if (shouldDeny) {
459
- eprint(`[plan-review] Blocking ExitPlanMode — ${denyReason}`);
460
- }
461
-
462
- // 13. Advance iteration
463
- const advancement = advanceIterationState(
464
- iterationState, planHash, planPath, overall, shouldDeny, passEligible, agentResults,
465
- );
466
- iterationState = advancement.updatedState;
467
- if (advancement.newGraduates.length > 0) {
468
- logInfo(HOOK, `Newly graduated (2 consecutive passes): ${advancement.newGraduates.join(", ")}`);
469
- }
470
-
471
- // 14. Save iteration state
472
- saveIterationState(reviewsDir, iterationState);
473
-
474
- // Write review tracker
475
- const ccNativeReviewsDir = path.dirname(reviewFolder);
476
- const trackerDecision = shouldDeny ? "blocked" : "allow";
477
- const topIssuesList = extractTopIssuesForTracker(combinedResult, 5);
478
- const trackerEntry: ReviewTrackerEntry = {
479
- iteration: currentIteration,
480
- timestamp: new Date().toISOString().replace("T", " ").slice(0, 16),
481
- planHash,
482
- verdict: combinedResult.overall_verdict,
483
- decision: trackerDecision,
484
- score: reviewScore,
485
- topIssues: topIssuesList,
486
- reviewFolder,
487
- };
488
- writeReviewTracker(ccNativeReviewsDir, trackerEntry);
489
- logInfo(HOOK, `Updated review tracker: ${path.join(ccNativeReviewsDir, "review-tracker.md")}`);
490
-
491
- // Build final output
492
- const output = buildReviewOutput({
493
- combinedResult,
494
- corroborationResult,
495
- iterationState: { ...iterationState, current: currentIteration },
496
- reviewFile,
497
- highIssuesPath,
498
- });
499
-
500
- // Mark plan reviewed
501
- const disposition = shouldDeny
502
- ? `hook_deny_iter_${currentIteration}`
503
- : `hook_allow_iter_${currentIteration}`;
504
- markPlanReviewed(sessionId, planHash, base, HOOK, { ...iterationState, current: currentIteration }, disposition);
505
-
506
- return {
507
- action: "block",
508
- contextText: output.contextText,
509
- blockReason: output.blockReason,
510
- };
511
- }
1
+ /**
2
+ * Review pipeline: orchestrates the full plan review lifecycle.
3
+ * Wires together plan-discovery, settings, agent-selection, graduation,
4
+ * output-builder, and existing modules (orchestrator, corroboration, etc.).
5
+ */
6
+
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+
10
+ import {
11
+ logDebug,
12
+ logInfo,
13
+ logWarn,
14
+ logError,
15
+ } from "../../_shared/lib-ts/base/logger.js";
16
+ import { logDiagnostic } from "../../_shared/lib-ts/base/hook-utils.js";
17
+ import { eprint } from "../../_shared/lib-ts/base/utils.js";
18
+ import { getContextReviewsDir, getContextDir, getReviewFolderPath } from "../../_shared/lib-ts/base/constants.js";
19
+ import { getContextBySessionId, getAllContexts } from "../../_shared/lib-ts/context/context-store.js";
20
+
21
+ import type {
22
+ AgentConfig,
23
+ OrchestratorConfig,
24
+ ReviewerResult,
25
+ CombinedReviewResult,
26
+ OrchestratorResult,
27
+ IterationState,
28
+ PipelineInput,
29
+ PipelineResult,
30
+ } from "./types.js";
31
+ import { REVIEW_SCHEMA } from "./types.js";
32
+ import type { ContextState } from "../../_shared/lib-ts/types.js";
33
+
34
+ import { discoverPlan } from "./plan-discovery.js";
35
+ import { loadSettings, loadModelsConfig, loadAgentLibrary, DEFAULT_ORCHESTRATOR } from "./settings.js";
36
+ import { resolveMandatoryAgents, assignModelsToAgents, selectAgents } from "./agent-selection.js";
37
+ import { computePassEligible, extractTopIssuesForTracker, advanceIterationState } from "./graduation.js";
38
+ import { truncateAgentIssues, overrideVerdictsByThreshold, buildReviewOutput } from "./output-builder.js";
39
+ import { DEFAULT_REVIEW_ITERATIONS, loadIterationState, saveIterationState } from "./state.js";
40
+
41
+ import {
42
+ isPlanAlreadyReviewed,
43
+ wasPlanPreviouslyDenied,
44
+ getLastPlanReview,
45
+ markPlanReviewed,
46
+ wasPlanQuestionsAgentAsked,
47
+ markQuestionsAsked,
48
+ resetPlanQuestionsAsked,
49
+ } from "./cc-native-state.js";
50
+
51
+ import { computeCorroboratedDecision } from "./corroboration.js";
52
+ import { runOrchestrator } from "./orchestrator.js";
53
+ import { debugLog } from "./debug.js";
54
+ import { writeCombinedArtifacts, buildCorroborationReport, buildHighIssuesDocument, writeReviewTracker } from "./artifacts.js";
55
+ import type { ReviewTrackerEntry } from "./artifacts.js";
56
+ import { runAgentReview } from "./reviewers/index.js";
57
+ import { runPlanQuestions } from "./plan-questions.js";
58
+
59
+ const HOOK = "review-pipeline";
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Context Lookup (private — only used here)
63
+ // ---------------------------------------------------------------------------
64
+
65
+ function getActiveContextForReview(sessionId: string, projectRoot: string): ContextState | null {
66
+ const ctx = getContextBySessionId(sessionId, projectRoot);
67
+ if (ctx) {
68
+ logInfo(HOOK, `Found context by session_id: ${ctx.id}`);
69
+ return ctx;
70
+ }
71
+ const allActive = getAllContexts("active", projectRoot);
72
+ const planning = allActive.filter(c => c.mode === "active" || c.mode === "has_plan");
73
+ if (planning.length === 1) {
74
+ logInfo(HOOK, `Found single planning context: ${planning[0]!.id}`);
75
+ return planning[0]!;
76
+ }
77
+ if (planning.length > 1) {
78
+ logWarn(HOOK, `Multiple planning contexts (${planning.length}), cannot determine which to use`);
79
+ } else if (allActive.length > 0) {
80
+ logInfo(HOOK, `Found ${allActive.length} active context(s) but none in planning mode`);
81
+ } else {
82
+ logInfo(HOOK, "No active contexts found");
83
+ }
84
+ return null;
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Pipeline
89
+ // ---------------------------------------------------------------------------
90
+
91
+ export async function runReviewPipeline(input: PipelineInput): Promise<PipelineResult> {
92
+ const { sessionId, base, aiwcliDir, transcriptPath, payload } = input;
93
+
94
+ // 1. Load settings
95
+ const settings = loadSettings(aiwcliDir);
96
+ const planSettings = settings.planReview ?? {};
97
+ const agentSettings = settings.agentReview ?? {};
98
+
99
+ const planReviewEnabled = planSettings.enabled ?? true;
100
+ const agentReviewEnabled = agentSettings.enabled ?? true;
101
+
102
+ if (!planReviewEnabled && !agentReviewEnabled) {
103
+ return { action: "skip", reason: "Both plan and agent review disabled" };
104
+ }
105
+
106
+ // 2. Discover plan
107
+ const discovered = discoverPlan(transcriptPath);
108
+ if (!discovered) {
109
+ return { action: "skip", reason: "No plan file found in ~/.claude/plans/. The plan may not have been written yet." };
110
+ }
111
+
112
+ const { content: plan, hash: planHash, path: planPath } = discovered;
113
+
114
+ // 3. Find active context (moved before questions gate for plan path change detection)
115
+ const activeContext = getActiveContextForReview(sessionId, base);
116
+ if (!activeContext) {
117
+ return { action: "skip", reason: "No active planning context found for this session." };
118
+ }
119
+
120
+ const contextId = activeContext.id;
121
+ const reviewsDir = path.join(getContextReviewsDir(contextId, base), "cc-native");
122
+ const contextPath = getContextDir(contextId, base);
123
+
124
+ // 4a. Load iteration state
125
+ let iterationState: IterationState | null = loadIterationState(reviewsDir);
126
+
127
+ // 4a-migration. Backfill sessionId for old iteration files
128
+ if (iterationState && !iterationState.sessionId) {
129
+ logInfo(HOOK, `Migrating iteration state: adding sessionId=${sessionId}`);
130
+ iterationState.sessionId = sessionId;
131
+ saveIterationState(reviewsDir, iterationState); // Persist migration
132
+ }
133
+
134
+ // 4a-session. Detect session change — reset iteration state for new planning session
135
+ if (iterationState && iterationState.sessionId && iterationState.sessionId !== sessionId) {
136
+ logInfo(HOOK, `Session changed (${iterationState.sessionId} → ${sessionId}), resetting iteration state`);
137
+ iterationState = null; // Force fresh state creation below
138
+ }
139
+
140
+ // 4a-init. Initialize if null
141
+ if (!iterationState) {
142
+ iterationState = {
143
+ current: 1, max: 1, complexity: "medium",
144
+ history: [], graduated: [], passStreaks: {},
145
+ lastPlanHash: "", lastPlanPath: "",
146
+ sessionId: sessionId,
147
+ };
148
+ saveIterationState(reviewsDir, iterationState);
149
+ }
150
+
151
+ // 4b. Detect plan file change — reset iteration state for new plan topic
152
+ const lastPath = iterationState.lastPlanPath ?? "";
153
+ if (lastPath && lastPath !== planPath) {
154
+ logInfo(HOOK, `Plan file changed (${path.basename(lastPath)}→${path.basename(planPath)}), resetting iteration state for new plan`);
155
+ iterationState = {
156
+ current: 1, max: 1, complexity: "medium",
157
+ history: [], graduated: [], passStreaks: {},
158
+ lastPlanHash: "", lastPlanPath: "",
159
+ sessionId: sessionId,
160
+ };
161
+ saveIterationState(reviewsDir, iterationState);
162
+ resetPlanQuestionsAsked(sessionId, base);
163
+ }
164
+
165
+ // 5. Questions gate
166
+ if (!wasPlanQuestionsAgentAsked(sessionId, base)) {
167
+ logInfo(HOOK, "Questions gate: plan-questions agent has not run yet, running now");
168
+ const timeout = typeof agentSettings.timeout === "number" ? agentSettings.timeout : 120;
169
+ const questionsResult = await runPlanQuestions(plan, aiwcliDir, timeout, undefined, sessionId);
170
+
171
+ markQuestionsAsked(sessionId, base, "agent");
172
+
173
+ const hasQuestions = questionsResult && (
174
+ questionsResult.questions.length > 0 ||
175
+ questionsResult.assumptions.length > 0 ||
176
+ questionsResult.ambiguities.length > 0
177
+ );
178
+
179
+ if (hasQuestions) {
180
+ const questionsList = questionsResult.questions.map((q: string, i: number) => `${i + 1}. ${q}`).join("\n");
181
+ const assumptionsList = questionsResult.assumptions.length > 0
182
+ ? `\n\nAssumptions detected:\n${questionsResult.assumptions.map((a: string) => `- ${a}`).join("\n")}`
183
+ : "";
184
+ const ambiguitiesList = questionsResult.ambiguities.length > 0
185
+ ? `\n\nAmbiguities detected:\n${questionsResult.ambiguities.map((a: string) => `- ${a}`).join("\n")}`
186
+ : "";
187
+ const contextMsg = `## Plan Questions (from independent review)\n\nAn agent reviewed your plan in a fresh context — without access to your session history or codebase exploration. It identified these questions:\n\n${questionsList}${assumptionsList}${ambiguitiesList}\n\nAsk the user these questions using AskUserQuestion before submitting the plan.`;
188
+ return {
189
+ action: "block",
190
+ contextText: contextMsg,
191
+ blockReason: "Ask the user clarifying questions before submitting the plan. Use AskUserQuestion with the questions above.",
192
+ };
193
+ }
194
+
195
+ logInfo(HOOK, "Questions gate: no questions generated, proceeding to review");
196
+ } else {
197
+ logInfo(HOOK, "Questions gate: agent already ran, skipping");
198
+ }
199
+
200
+ // 6. Hash + dedup
201
+ logDiagnostic(HOOK, "receive", `plan_size=${plan.length}, session=${sessionId.slice(0, 8)}`, {
202
+ inputs: { plan_hash: planHash, plan_size: plan.length, session_id: sessionId.slice(0, 12) },
203
+ });
204
+
205
+ // Plan-hash deduplication
206
+ logDebug(HOOK, `Plan hash: ${planHash}`);
207
+ if (isPlanAlreadyReviewed(sessionId, planHash, base)) {
208
+ const lastReview = getLastPlanReview(sessionId, planHash, base);
209
+
210
+ if (wasPlanPreviouslyDenied(sessionId, planHash, base)) {
211
+ return {
212
+ action: "block",
213
+ contextText: "[Plan Review] Plan content unchanged since last review which found issues.",
214
+ blockReason: "Plan unchanged since denial. Modify the plan to address review findings, then attempt ExitPlanMode again.",
215
+ };
216
+ } else {
217
+ const verdict = lastReview?.iteration?.latest_verdict || "pass";
218
+ return { action: "skip", reason: `Plan already reviewed (verdict: ${verdict}). Skipping re-review.` };
219
+ }
220
+ }
221
+
222
+ // 7. Iteration bounds check
223
+ if (iterationState.current > iterationState.max) {
224
+ return { action: "skip", reason: `Max review iterations reached (${iterationState.current - 1}/${iterationState.max}), allowing plan through.` };
225
+ }
226
+
227
+ // Initialize result containers
228
+ let orchResult: OrchestratorResult | null = null;
229
+ const agentResults: Record<string, ReviewerResult> = {};
230
+ let detectedComplexity = "medium";
231
+
232
+ // 7. PHASE 1: Orchestrator
233
+ const graduatedSet = new Set(iterationState.graduated);
234
+ if (graduatedSet.size > 0) {
235
+ logInfo(HOOK, `Graduated agents from previous iterations: ${[...graduatedSet].sort().join(", ")}`);
236
+ }
237
+
238
+ const agentLibrary = agentReviewEnabled ? loadAgentLibrary(aiwcliDir, agentSettings) : [];
239
+ const originalAgentCount = agentLibrary.length;
240
+ const enabledAgents = agentLibrary.filter(a => !graduatedSet.has(a.name));
241
+ const timeout = typeof agentSettings.timeout === "number" ? agentSettings.timeout : 120;
242
+ const legacyMode = agentSettings.legacyMode === true;
243
+
244
+ const orchSettings = agentSettings.orchestrator ?? DEFAULT_ORCHESTRATOR;
245
+ const orchestratorConfig: OrchestratorConfig = {
246
+ enabled: (orchSettings.enabled ?? true) && agentReviewEnabled,
247
+ model: orchSettings.model ?? "haiku",
248
+ timeout: orchSettings.timeout ?? 30,
249
+ };
250
+
251
+ const mandatoryConfig = agentSettings.mandatoryAgents ?? ["handoff-readiness", "clarity-auditor", "skeptic"];
252
+ const alwaysMandatory = resolveMandatoryAgents(mandatoryConfig, "simple");
253
+
254
+ logDebug(HOOK, `Agent library: ${agentLibrary.map(a => a.name)}`);
255
+ logDebug(HOOK, `Mandatory agents: ${[...alwaysMandatory].sort()}`);
256
+ logDebug(HOOK, `Orchestrator enabled: ${orchestratorConfig.enabled}`);
257
+
258
+ const phase1Promises: Array<{ name: string; promise: Promise<ReviewerResult | OrchestratorResult> }> = [];
259
+
260
+ if (orchestratorConfig.enabled && enabledAgents.length > 0 && !legacyMode) {
261
+ phase1Promises.push({
262
+ name: "orchestrator",
263
+ promise: runOrchestrator(plan, enabledAgents, orchestratorConfig, agentSettings, alwaysMandatory),
264
+ });
265
+ }
266
+
267
+ logInfo(HOOK, `=== PHASE 1: Running ${phase1Promises.length} tasks in parallel ===`);
268
+
269
+ if (phase1Promises.length > 0) {
270
+ const results = await Promise.allSettled(
271
+ phase1Promises.map(async ({ name, promise }) => {
272
+ const result = await promise;
273
+ return { name, result };
274
+ }),
275
+ );
276
+ for (const [i, r] of results.entries()) {
277
+ if (r.status === "fulfilled") {
278
+ if (r.value.name === "orchestrator") orchResult = r.value.result as OrchestratorResult;
279
+ logInfo(HOOK, `${r.value.name} completed`);
280
+ } else {
281
+ const failedName = phase1Promises[i]?.name ?? "unknown";
282
+ logError(HOOK, `${failedName} failed: ${r.reason}`);
283
+ }
284
+ }
285
+ }
286
+
287
+ // 8. PHASE 2: Agent Selection
288
+ let selectedAgents: AgentConfig[] = [];
289
+ let mandatoryNames = alwaysMandatory;
290
+
291
+ if (agentReviewEnabled) {
292
+ logInfo(HOOK, "=== PHASE 2: Agent Selection ===");
293
+
294
+ const selectionResult = selectAgents({
295
+ enabledAgents,
296
+ orchResult,
297
+ mandatoryConfig,
298
+ agentSettings,
299
+ legacyMode,
300
+ });
301
+
302
+ selectedAgents = selectionResult.selectedAgents;
303
+ mandatoryNames = selectionResult.mandatoryNames;
304
+ detectedComplexity = selectionResult.detectedComplexity;
305
+
306
+ logDiagnostic(HOOK, "decide", `Selected ${selectedAgents.length} agents, complexity=${detectedComplexity}`, {
307
+ decision: "agents_selected",
308
+ reasoning: `orchestrator=${orchResult !== null}, legacy=${legacyMode}`,
309
+ inputs: {
310
+ agents: selectedAgents.map(a => a.name),
311
+ complexity: detectedComplexity,
312
+ mandatory_count: selectedAgents.filter(a => mandatoryNames.has(a.name)).length,
313
+ },
314
+ });
315
+
316
+ // Update iteration state with complexity/max
317
+ const reviewIterations: Record<string, number> = {
318
+ ...DEFAULT_REVIEW_ITERATIONS,
319
+ ...(agentSettings.reviewIterations ?? {}),
320
+ };
321
+ iterationState.complexity = detectedComplexity;
322
+ iterationState.max = reviewIterations[detectedComplexity] ?? iterationState.max;
323
+ logDebug(HOOK, `Iteration state: ${iterationState.current}/${iterationState.max} (${detectedComplexity})`);
324
+
325
+ // Assign random providers + models
326
+ const modelsConfig = loadModelsConfig(settings);
327
+ selectedAgents = assignModelsToAgents(selectedAgents, modelsConfig);
328
+ logInfo(HOOK, `Model assignments: ${selectedAgents.map(a => `${a.name}→${a.provider}:${a.model}`).join(", ")}`);
329
+
330
+ // 9. PHASE 3: Run agents
331
+ if (selectedAgents.length > 0) {
332
+ logInfo(HOOK, "=== PHASE 3: Agent Reviews ===");
333
+ logInfo(HOOK, `Launching ${selectedAgents.length} agents in parallel`);
334
+
335
+ debugLog(contextPath, sessionId, "hook", "agent_review_start", {
336
+ agents: selectedAgents.map(a => a.name),
337
+ timeout,
338
+ complexity: detectedComplexity,
339
+ });
340
+
341
+ const agentPromises = selectedAgents.map(async agent => {
342
+ const result = await runAgentReview(plan, agent, REVIEW_SCHEMA, timeout, contextPath, sessionId);
343
+ return { agent, result };
344
+ });
345
+
346
+ const agentSettled = await Promise.allSettled(agentPromises);
347
+ for (const [i, r] of agentSettled.entries()) {
348
+ if (r.status === "fulfilled") {
349
+ const { agent, result } = r.value;
350
+ agentResults[agent.name] = result;
351
+ logInfo(HOOK, `${agent.name} completed with verdict: ${result.verdict}`);
352
+ } else {
353
+ const failedAgent = selectedAgents[i]!;
354
+ logError(HOOK, `${failedAgent.name} failed with exception: ${r.reason}`);
355
+ agentResults[failedAgent.name] = {
356
+ name: failedAgent.name,
357
+ ok: false,
358
+ verdict: "error",
359
+ data: {},
360
+ raw: "",
361
+ err: String(r.reason),
362
+ };
363
+ }
364
+ }
365
+ }
366
+ }
367
+
368
+ // 10. Issue truncation + verdict override
369
+ const maxIssuesPerAgent = typeof agentSettings.maxIssuesPerAgent === "number"
370
+ ? agentSettings.maxIssuesPerAgent : 3;
371
+ truncateAgentIssues(agentResults, maxIssuesPerAgent);
372
+
373
+ const passEligible = computePassEligible(agentResults);
374
+ if (passEligible.length > 0) {
375
+ logInfo(HOOK, `Pass-eligible agents this iteration: ${passEligible.join(", ")}`);
376
+ }
377
+
378
+ const highIssueThreshold = typeof agentSettings.highIssueThreshold === "number" ? agentSettings.highIssueThreshold : 3;
379
+ overrideVerdictsByThreshold(agentResults, highIssueThreshold);
380
+
381
+ // PHASE 4: Generate Output
382
+ logInfo(HOOK, "=== PHASE 4: Generate Output ===");
383
+
384
+ if (Object.keys(agentResults).length === 0) {
385
+ if (graduatedSet.size > 0 && originalAgentCount > 0) {
386
+ return { action: "skip", reason: "All agent reviewers graduated from previous iterations — no review needed." };
387
+ }
388
+ return { action: "skip", reason: "All reviewers failed to produce results. Check stderr logs for details." };
389
+ }
390
+
391
+ // 11. Corroboration
392
+ const corroborationResult = computeCorroboratedDecision(agentResults);
393
+ const overall = corroborationResult.verdict;
394
+
395
+ const combinedResult: CombinedReviewResult = {
396
+ plan_hash: planHash,
397
+ overall_verdict: overall,
398
+ orchestration: orchResult,
399
+ agents: agentResults,
400
+ timestamp: new Date().toISOString(),
401
+ };
402
+
403
+ const displaySettings = {
404
+ ...(planSettings.display ?? {}),
405
+ ...(agentSettings.display ?? {}),
406
+ };
407
+ const combinedSettings = { display: displaySettings };
408
+
409
+ const currentIteration = iterationState.current;
410
+
411
+ // 12. Write artifacts
412
+ const reviewFolder = getReviewFolderPath(contextId, currentIteration, base);
413
+ fs.mkdirSync(reviewFolder, { recursive: true });
414
+ logInfo(HOOK, `Created review folder: ${reviewFolder}`);
415
+
416
+ const reviewFile = writeCombinedArtifacts(
417
+ base, plan, combinedResult, payload, combinedSettings,
418
+ undefined, reviewFolder, currentIteration, corroborationResult,
419
+ );
420
+ logInfo(HOOK, `Saved review: ${reviewFile}`);
421
+
422
+ const corroborationReport = buildCorroborationReport(corroborationResult);
423
+ const corroborationPath = path.join(reviewFolder, "corroboration.md");
424
+ fs.writeFileSync(corroborationPath, corroborationReport, "utf-8");
425
+ logInfo(HOOK, `Saved corroboration report: ${corroborationPath}`);
426
+
427
+ try {
428
+ fs.writeFileSync(path.join(reviewFolder, "plan.md"), plan, "utf-8");
429
+ logDebug(HOOK, `Saved plan snapshot: ${path.join(reviewFolder, "plan.md")}`);
430
+ } catch (e) {
431
+ logWarn(HOOK, `Failed to save plan snapshot: ${e}`);
432
+ }
433
+
434
+ // Build high-issues document
435
+ const highIssuesDoc = buildHighIssuesDocument(combinedResult, corroborationResult);
436
+ const highIssuesPath = path.join(reviewFolder, "high-issues.md");
437
+ fs.writeFileSync(highIssuesPath, highIssuesDoc, "utf-8");
438
+
439
+ // 15. Build output
440
+ const shouldDeny = corroborationResult.blocking.length > 0;
441
+ const reviewScore = shouldDeny ? 1.0 : 0.0;
442
+ const denyReason = shouldDeny ? "corroborated_issues" : "no_corroboration";
443
+
444
+ logInfo(HOOK, `REVIEW_DECISION: verdict=${combinedResult.overall_verdict}, deny=${shouldDeny}, reason=${denyReason}, score=${reviewScore.toFixed(2)}`);
445
+ logDiagnostic(HOOK, "result", `verdict=${combinedResult.overall_verdict}, deny=${shouldDeny}, reason=${denyReason}`, {
446
+ decision: shouldDeny ? "deny" : "allow",
447
+ reasoning: `reason=${denyReason}, score=${reviewScore.toFixed(2)}`,
448
+ inputs: {
449
+ overall_verdict: combinedResult.overall_verdict,
450
+ review_score: Math.round(reviewScore * 100) / 100,
451
+ agent_count: Object.keys(agentResults).length,
452
+ },
453
+ });
454
+
455
+ // Terminal progress
456
+ const verdictEmoji = shouldDeny ? "❌" : "✅";
457
+ eprint(`[plan-review] ${verdictEmoji} ${combinedResult.overall_verdict.toUpperCase()} (score=${reviewScore.toFixed(2)})`);
458
+ if (shouldDeny) {
459
+ eprint(`[plan-review] Blocking ExitPlanMode — ${denyReason}`);
460
+ }
461
+
462
+ // 13. Advance iteration
463
+ const advancement = advanceIterationState(
464
+ iterationState, planHash, planPath, overall, shouldDeny, passEligible, agentResults,
465
+ );
466
+ iterationState = advancement.updatedState;
467
+ if (advancement.newGraduates.length > 0) {
468
+ logInfo(HOOK, `Newly graduated (2 consecutive passes): ${advancement.newGraduates.join(", ")}`);
469
+ }
470
+
471
+ // 14. Save iteration state
472
+ saveIterationState(reviewsDir, iterationState);
473
+
474
+ // Write review tracker
475
+ const ccNativeReviewsDir = path.dirname(reviewFolder);
476
+ const trackerDecision = shouldDeny ? "blocked" : "allow";
477
+ const topIssuesList = extractTopIssuesForTracker(combinedResult, 5);
478
+ const trackerEntry: ReviewTrackerEntry = {
479
+ iteration: currentIteration,
480
+ timestamp: new Date().toISOString().replace("T", " ").slice(0, 16),
481
+ planHash,
482
+ verdict: combinedResult.overall_verdict,
483
+ decision: trackerDecision,
484
+ score: reviewScore,
485
+ topIssues: topIssuesList,
486
+ reviewFolder,
487
+ };
488
+ writeReviewTracker(ccNativeReviewsDir, trackerEntry);
489
+ logInfo(HOOK, `Updated review tracker: ${path.join(ccNativeReviewsDir, "review-tracker.md")}`);
490
+
491
+ // Build final output
492
+ const output = buildReviewOutput({
493
+ combinedResult,
494
+ corroborationResult,
495
+ iterationState: { ...iterationState, current: currentIteration },
496
+ reviewFile,
497
+ highIssuesPath,
498
+ });
499
+
500
+ // Mark plan reviewed
501
+ const disposition = shouldDeny
502
+ ? `hook_deny_iter_${currentIteration}`
503
+ : `hook_allow_iter_${currentIteration}`;
504
+ markPlanReviewed(sessionId, planHash, base, HOOK, { ...iterationState, current: currentIteration }, disposition);
505
+
506
+ return {
507
+ action: "block",
508
+ contextText: output.contextText,
509
+ blockReason: output.blockReason,
510
+ };
511
+ }