aiwcli 0.12.6 → 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 (124) 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 -12
  11. package/dist/templates/_shared/.claude/commands/handoff.md +12 -12
  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 -421
  16. package/dist/templates/_shared/handoff-system/lib/document-generator.ts +215 -215
  17. package/dist/templates/_shared/handoff-system/lib/handoff-reader.ts +158 -158
  18. package/dist/templates/_shared/handoff-system/scripts/resume_handoff.ts +373 -373
  19. package/dist/templates/_shared/handoff-system/scripts/save_handoff.ts +469 -469
  20. package/dist/templates/_shared/handoff-system/workflows/handoff-resume.md +66 -66
  21. package/dist/templates/_shared/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 -196
  28. package/dist/templates/_shared/hooks-ts/session_start.ts +163 -163
  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 -202
  40. package/dist/templates/_shared/lib-ts/base/stop-words.ts +184 -184
  41. package/dist/templates/_shared/lib-ts/base/utils.ts +184 -184
  42. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +566 -566
  43. package/dist/templates/_shared/lib-ts/context/context-selector.ts +524 -524
  44. package/dist/templates/_shared/lib-ts/context/context-store.ts +712 -712
  45. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +312 -312
  46. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +185 -185
  47. package/dist/templates/_shared/lib-ts/package.json +20 -20
  48. package/dist/templates/_shared/lib-ts/templates/formatters.ts +102 -102
  49. package/dist/templates/_shared/lib-ts/templates/plan-context.ts +58 -58
  50. package/dist/templates/_shared/lib-ts/tsconfig.json +13 -13
  51. package/dist/templates/_shared/lib-ts/types.ts +186 -186
  52. package/dist/templates/_shared/scripts/resolve_context.ts +33 -33
  53. package/dist/templates/_shared/scripts/status_line.ts +690 -690
  54. package/dist/templates/cc-native/.claude/commands/cc-native/rlm/ask.md +136 -136
  55. package/dist/templates/cc-native/.claude/commands/cc-native/rlm/index.md +21 -21
  56. package/dist/templates/cc-native/.claude/commands/cc-native/rlm/overview.md +56 -56
  57. package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +10 -10
  58. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fix.md +8 -8
  59. package/dist/templates/cc-native/.windsurf/workflows/cc-native/implement.md +8 -8
  60. package/dist/templates/cc-native/.windsurf/workflows/cc-native/research.md +8 -8
  61. package/dist/templates/cc-native/CC-NATIVE-README.md +189 -189
  62. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +304 -304
  63. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +143 -143
  64. package/dist/templates/cc-native/_cc-native/agents/PLAN-ORCHESTRATOR.md +213 -213
  65. package/dist/templates/cc-native/_cc-native/agents/plan-questions/PLAN-QUESTIONER.md +70 -70
  66. package/dist/templates/cc-native/_cc-native/cc-native.config.json +96 -96
  67. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +247 -247
  68. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +76 -76
  69. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_subagent.ts +54 -54
  70. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_write.ts +51 -51
  71. package/dist/templates/cc-native/_cc-native/hooks/mark_questions_asked.ts +53 -53
  72. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -61
  73. package/dist/templates/cc-native/_cc-native/lib-ts/agent-selection.ts +163 -163
  74. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +156 -156
  75. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/format.ts +597 -597
  76. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/index.ts +26 -26
  77. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/tracker.ts +107 -107
  78. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/write.ts +119 -119
  79. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +21 -21
  80. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +319 -319
  81. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -144
  82. package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -57
  83. package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -83
  84. package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +119 -119
  85. package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +79 -79
  86. package/dist/templates/cc-native/_cc-native/lib-ts/graduation.ts +132 -132
  87. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +116 -116
  88. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -168
  89. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +70 -70
  90. package/dist/templates/cc-native/_cc-native/lib-ts/output-builder.ts +130 -130
  91. package/dist/templates/cc-native/_cc-native/lib-ts/plan-discovery.ts +80 -80
  92. package/dist/templates/cc-native/_cc-native/lib-ts/plan-enhancement.ts +41 -41
  93. package/dist/templates/cc-native/_cc-native/lib-ts/plan-questions.ts +101 -101
  94. package/dist/templates/cc-native/_cc-native/lib-ts/review-pipeline.ts +511 -511
  95. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +71 -71
  96. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/base/base-agent.ts +217 -217
  97. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +12 -12
  98. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/claude-agent.ts +66 -66
  99. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/codex-agent.ts +184 -184
  100. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/gemini-agent.ts +39 -39
  101. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +196 -196
  102. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/schemas.ts +201 -201
  103. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +21 -21
  104. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/CLAUDE.md +480 -480
  105. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/embedding-indexer.ts +287 -287
  106. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/hyde.ts +148 -148
  107. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/index.ts +54 -54
  108. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/logger.ts +58 -58
  109. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/ollama-client.ts +208 -208
  110. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/retrieval-pipeline.ts +460 -460
  111. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-indexer.ts +446 -446
  112. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-loader.ts +280 -280
  113. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-searcher.ts +274 -274
  114. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/types.ts +201 -201
  115. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/vector-store.ts +278 -278
  116. package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +184 -184
  117. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +275 -275
  118. package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -18
  119. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +329 -329
  120. package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -72
  121. package/dist/templates/cc-native/_cc-native/workflows/specdev.md +9 -9
  122. package/oclif.manifest.json +1 -1
  123. package/package.json +108 -108
  124. 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
+ }