aiwcli 0.12.7 → 0.12.8

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 (79) hide show
  1. package/dist/templates/CLAUDE.md +27 -0
  2. package/dist/templates/_shared/.claude/{commands/handoff.md → skills/handoff/SKILL.md} +3 -2
  3. package/dist/templates/_shared/.claude/{commands/handoff-resume.md → skills/handoff-resume/SKILL.md} +2 -1
  4. package/dist/templates/_shared/handoff-system/CLAUDE.md +433 -421
  5. package/dist/templates/_shared/lib-ts/CLAUDE.md +3 -3
  6. package/dist/templates/_shared/lib-ts/base/constants.ts +324 -306
  7. package/dist/templates/_shared/lib-ts/base/inference.ts +3 -3
  8. package/dist/templates/_shared/lib-ts/context/CLAUDE.md +134 -0
  9. package/dist/templates/_shared/scripts/status_line.ts +26 -29
  10. package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +1 -1
  11. package/dist/templates/cc-native/.claude/settings.json +3 -2
  12. package/dist/templates/cc-native/_cc-native/artifacts/CLAUDE.md +64 -0
  13. package/dist/templates/cc-native/_cc-native/{lib-ts/artifacts → artifacts/lib}/format.ts +597 -597
  14. package/dist/templates/cc-native/_cc-native/{lib-ts/artifacts → artifacts/lib}/index.ts +26 -26
  15. package/dist/templates/cc-native/_cc-native/{lib-ts/artifacts → artifacts/lib}/tracker.ts +107 -107
  16. package/dist/templates/cc-native/_cc-native/{lib-ts/artifacts → artifacts/lib}/write.ts +119 -119
  17. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +237 -247
  18. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +76 -76
  19. package/dist/templates/cc-native/_cc-native/hooks/validate_task_prompt.ts +76 -0
  20. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +163 -156
  21. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +116 -116
  22. package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +1 -1
  23. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +329 -329
  24. package/dist/templates/cc-native/_cc-native/plan-review/CLAUDE.md +149 -0
  25. package/dist/templates/cc-native/_cc-native/plan-review/agents/CLAUDE.md +143 -0
  26. package/dist/templates/cc-native/_cc-native/plan-review/agents/PLAN-ORCHESTRATOR.md +213 -0
  27. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-questions/PLAN-QUESTIONER.md +70 -0
  28. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/ARCH-EVOLUTION.md +62 -0
  29. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/ARCH-PATTERNS.md +61 -0
  30. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/ARCH-STRUCTURE.md +62 -0
  31. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/ASSUMPTION-TRACER.md +56 -0
  32. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/CLARITY-AUDITOR.md +53 -0
  33. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/COMPLETENESS-FEASIBILITY.md +66 -0
  34. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/COMPLETENESS-GAPS.md +70 -0
  35. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/COMPLETENESS-ORDERING.md +62 -0
  36. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/CONSTRAINT-VALIDATOR.md +72 -0
  37. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/DESIGN-ADR-VALIDATOR.md +61 -0
  38. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/DESIGN-SCALE-MATCHER.md +64 -0
  39. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/DEVILS-ADVOCATE.md +56 -0
  40. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/DOCUMENTATION-PHILOSOPHY.md +86 -0
  41. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/HANDOFF-READINESS.md +59 -0
  42. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/HIDDEN-COMPLEXITY.md +58 -0
  43. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/INCREMENTAL-DELIVERY.md +66 -0
  44. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/RISK-DEPENDENCY.md +62 -0
  45. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/RISK-FMEA.md +66 -0
  46. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/RISK-PREMORTEM.md +71 -0
  47. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/RISK-REVERSIBILITY.md +74 -0
  48. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/SCOPE-BOUNDARY.md +77 -0
  49. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/SIMPLICITY-GUARDIAN.md +62 -0
  50. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/SKEPTIC.md +68 -0
  51. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TESTDRIVEN-BEHAVIOR-AUDITOR.md +61 -0
  52. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TESTDRIVEN-CHARACTERIZATION.md +71 -0
  53. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TESTDRIVEN-FIRST-VALIDATOR.md +61 -0
  54. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TESTDRIVEN-PYRAMID-ANALYZER.md +61 -0
  55. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TRADEOFF-COSTS.md +67 -0
  56. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TRADEOFF-STAKEHOLDERS.md +65 -0
  57. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/VERIFY-COVERAGE.md +74 -0
  58. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/VERIFY-STRENGTH.md +69 -0
  59. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/agent-selection.ts +163 -163
  60. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/corroboration.ts +119 -119
  61. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/graduation.ts +132 -132
  62. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/orchestrator.ts +70 -70
  63. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/output-builder.ts +130 -130
  64. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/plan-questions.ts +102 -102
  65. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/review-pipeline.ts +511 -511
  66. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/agent.ts +74 -74
  67. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/base/base-agent.ts +217 -217
  68. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/index.ts +12 -12
  69. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/providers/claude-agent.ts +66 -66
  70. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/providers/codex-agent.ts +185 -185
  71. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/providers/gemini-agent.ts +39 -39
  72. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/providers/orchestrator-claude-agent.ts +196 -196
  73. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/schemas.ts +201 -201
  74. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/types.ts +23 -23
  75. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/verdict.ts +72 -72
  76. package/dist/templates/cc-native/_cc-native/{workflows → plan-review/workflows}/specdev.md +9 -9
  77. package/oclif.manifest.json +1 -1
  78. package/package.json +1 -1
  79. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +0 -21
@@ -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 "../../lib-ts/types.js";
31
+ import { REVIEW_SCHEMA } from "../../lib-ts/types.js";
32
+ import type { ContextState } from "../../../_shared/lib-ts/types.js";
33
+
34
+ import { discoverPlan } from "../../lib-ts/plan-discovery.js";
35
+ import { loadSettings, loadModelsConfig, loadAgentLibrary, DEFAULT_ORCHESTRATOR } from "../../lib-ts/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 "../../lib-ts/state.js";
40
+
41
+ import {
42
+ isPlanAlreadyReviewed,
43
+ wasPlanPreviouslyDenied,
44
+ getLastPlanReview,
45
+ markPlanReviewed,
46
+ wasPlanQuestionsAgentAsked,
47
+ markQuestionsAsked,
48
+ resetPlanQuestionsAsked,
49
+ } from "../../lib-ts/cc-native-state.js";
50
+
51
+ import { computeCorroboratedDecision } from "./corroboration.js";
52
+ import { runOrchestrator } from "./orchestrator.js";
53
+ import { debugLog } from "../../lib-ts/debug.js";
54
+ import { writeCombinedArtifacts, buildCorroborationReport, buildHighIssuesDocument, writeReviewTracker } from "../../artifacts/lib/index.js";
55
+ import type { ReviewTrackerEntry } from "../../artifacts/lib/index.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
+ }