aiwcli 0.11.0 → 0.12.0

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 (118) hide show
  1. package/dist/commands/clear.d.ts +8 -0
  2. package/dist/commands/clear.js +86 -0
  3. package/dist/lib/bmad-installer.d.ts +2 -27
  4. package/dist/lib/bmad-installer.js +3 -43
  5. package/dist/lib/claude-settings-types.d.ts +2 -1
  6. package/dist/lib/env-compat.d.ts +0 -8
  7. package/dist/lib/env-compat.js +0 -12
  8. package/dist/lib/git/index.d.ts +0 -1
  9. package/dist/lib/gitignore-manager.d.ts +0 -2
  10. package/dist/lib/gitignore-manager.js +1 -1
  11. package/dist/lib/hooks-merger.d.ts +1 -15
  12. package/dist/lib/hooks-merger.js +1 -1
  13. package/dist/lib/index.d.ts +3 -7
  14. package/dist/lib/index.js +3 -11
  15. package/dist/lib/output.d.ts +2 -1
  16. package/dist/lib/settings-hierarchy.d.ts +1 -13
  17. package/dist/lib/settings-hierarchy.js +1 -1
  18. package/dist/lib/template-installer.d.ts +5 -9
  19. package/dist/lib/template-installer.js +2 -12
  20. package/dist/lib/template-linter.d.ts +3 -10
  21. package/dist/lib/template-linter.js +2 -2
  22. package/dist/lib/template-resolver.d.ts +6 -0
  23. package/dist/lib/template-resolver.js +10 -0
  24. package/dist/lib/template-settings-reconstructor.d.ts +1 -1
  25. package/dist/lib/template-settings-reconstructor.js +17 -24
  26. package/dist/lib/terminal.d.ts +3 -14
  27. package/dist/lib/terminal.js +0 -4
  28. package/dist/lib/version.d.ts +2 -11
  29. package/dist/lib/version.js +2 -2
  30. package/dist/lib/windsurf-hooks-merger.d.ts +1 -15
  31. package/dist/lib/windsurf-hooks-merger.js +1 -1
  32. package/dist/templates/_shared/.codex/workflows/handoff.md +1 -1
  33. package/dist/templates/_shared/.windsurf/workflows/handoff.md +1 -1
  34. package/dist/templates/_shared/hooks-ts/session_end.ts +4 -1
  35. package/dist/templates/_shared/hooks-ts/session_start.ts +15 -20
  36. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +12 -14
  37. package/dist/templates/_shared/lib-ts/CLAUDE.md +56 -7
  38. package/dist/templates/_shared/lib-ts/base/git-state.ts +1 -1
  39. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +174 -43
  40. package/dist/templates/_shared/lib-ts/base/logger.ts +15 -18
  41. package/dist/templates/_shared/lib-ts/base/state-io.ts +11 -2
  42. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +181 -162
  43. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +26 -30
  44. package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +12 -13
  45. package/dist/templates/_shared/lib-ts/package.json +1 -2
  46. package/dist/templates/_shared/lib-ts/templates/plan-context.ts +27 -34
  47. package/dist/templates/_shared/lib-ts/types.ts +17 -2
  48. package/dist/templates/_shared/scripts/resume_handoff.ts +62 -38
  49. package/dist/templates/_shared/scripts/save_handoff.ts +24 -24
  50. package/dist/templates/_shared/scripts/status_line.ts +102 -148
  51. package/dist/templates/_shared/workflows/handoff.md +1 -1
  52. package/dist/templates/cc-native/.claude/settings.json +183 -175
  53. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +23 -1
  54. package/dist/templates/cc-native/_cc-native/agents/plan-questions/PLAN-QUESTIONER.md +70 -0
  55. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +6 -1
  56. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +316 -176
  57. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_subagent.ts +38 -0
  58. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_write.ts +51 -0
  59. package/dist/templates/cc-native/_cc-native/hooks/mark_questions_asked.ts +53 -0
  60. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +15 -15
  61. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +11 -12
  62. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +227 -114
  63. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +64 -16
  64. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +23 -3
  65. package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +119 -0
  66. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +1 -4
  67. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +7 -1
  68. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +40 -218
  69. package/dist/templates/cc-native/_cc-native/lib-ts/plan-enhancement.ts +41 -0
  70. package/dist/templates/cc-native/_cc-native/lib-ts/plan-questions.ts +101 -0
  71. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +27 -111
  72. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/base/base-agent.ts +217 -0
  73. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +5 -3
  74. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/claude-agent.ts +65 -0
  75. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/codex-agent.ts +184 -0
  76. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/gemini-agent.ts +39 -0
  77. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +195 -0
  78. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/schemas.ts +201 -0
  79. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +3 -5
  80. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +30 -33
  81. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +104 -132
  82. package/dist/templates/cc-native/_cc-native/plan-review.config.json +22 -13
  83. package/oclif.manifest.json +1 -1
  84. package/package.json +2 -3
  85. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +0 -119
  86. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +0 -130
  87. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +0 -106
  88. /package/dist/templates/cc-native/_cc-native/agents/{ARCH-EVOLUTION.md → plan-review/ARCH-EVOLUTION.md} +0 -0
  89. /package/dist/templates/cc-native/_cc-native/agents/{ARCH-PATTERNS.md → plan-review/ARCH-PATTERNS.md} +0 -0
  90. /package/dist/templates/cc-native/_cc-native/agents/{ARCH-STRUCTURE.md → plan-review/ARCH-STRUCTURE.md} +0 -0
  91. /package/dist/templates/cc-native/_cc-native/agents/{ASSUMPTION-TRACER.md → plan-review/ASSUMPTION-TRACER.md} +0 -0
  92. /package/dist/templates/cc-native/_cc-native/agents/{CLARITY-AUDITOR.md → plan-review/CLARITY-AUDITOR.md} +0 -0
  93. /package/dist/templates/cc-native/_cc-native/agents/{COMPLETENESS-FEASIBILITY.md → plan-review/COMPLETENESS-FEASIBILITY.md} +0 -0
  94. /package/dist/templates/cc-native/_cc-native/agents/{COMPLETENESS-GAPS.md → plan-review/COMPLETENESS-GAPS.md} +0 -0
  95. /package/dist/templates/cc-native/_cc-native/agents/{COMPLETENESS-ORDERING.md → plan-review/COMPLETENESS-ORDERING.md} +0 -0
  96. /package/dist/templates/cc-native/_cc-native/agents/{CONSTRAINT-VALIDATOR.md → plan-review/CONSTRAINT-VALIDATOR.md} +0 -0
  97. /package/dist/templates/cc-native/_cc-native/agents/{DESIGN-ADR-VALIDATOR.md → plan-review/DESIGN-ADR-VALIDATOR.md} +0 -0
  98. /package/dist/templates/cc-native/_cc-native/agents/{DESIGN-SCALE-MATCHER.md → plan-review/DESIGN-SCALE-MATCHER.md} +0 -0
  99. /package/dist/templates/cc-native/_cc-native/agents/{DEVILS-ADVOCATE.md → plan-review/DEVILS-ADVOCATE.md} +0 -0
  100. /package/dist/templates/cc-native/_cc-native/agents/{DOCUMENTATION-PHILOSOPHY.md → plan-review/DOCUMENTATION-PHILOSOPHY.md} +0 -0
  101. /package/dist/templates/cc-native/_cc-native/agents/{HANDOFF-READINESS.md → plan-review/HANDOFF-READINESS.md} +0 -0
  102. /package/dist/templates/cc-native/_cc-native/agents/{HIDDEN-COMPLEXITY.md → plan-review/HIDDEN-COMPLEXITY.md} +0 -0
  103. /package/dist/templates/cc-native/_cc-native/agents/{INCREMENTAL-DELIVERY.md → plan-review/INCREMENTAL-DELIVERY.md} +0 -0
  104. /package/dist/templates/cc-native/_cc-native/agents/{RISK-DEPENDENCY.md → plan-review/RISK-DEPENDENCY.md} +0 -0
  105. /package/dist/templates/cc-native/_cc-native/agents/{RISK-FMEA.md → plan-review/RISK-FMEA.md} +0 -0
  106. /package/dist/templates/cc-native/_cc-native/agents/{RISK-PREMORTEM.md → plan-review/RISK-PREMORTEM.md} +0 -0
  107. /package/dist/templates/cc-native/_cc-native/agents/{RISK-REVERSIBILITY.md → plan-review/RISK-REVERSIBILITY.md} +0 -0
  108. /package/dist/templates/cc-native/_cc-native/agents/{SCOPE-BOUNDARY.md → plan-review/SCOPE-BOUNDARY.md} +0 -0
  109. /package/dist/templates/cc-native/_cc-native/agents/{SIMPLICITY-GUARDIAN.md → plan-review/SIMPLICITY-GUARDIAN.md} +0 -0
  110. /package/dist/templates/cc-native/_cc-native/agents/{SKEPTIC.md → plan-review/SKEPTIC.md} +0 -0
  111. /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-BEHAVIOR-AUDITOR.md → plan-review/TESTDRIVEN-BEHAVIOR-AUDITOR.md} +0 -0
  112. /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-CHARACTERIZATION.md → plan-review/TESTDRIVEN-CHARACTERIZATION.md} +0 -0
  113. /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-FIRST-VALIDATOR.md → plan-review/TESTDRIVEN-FIRST-VALIDATOR.md} +0 -0
  114. /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-PYRAMID-ANALYZER.md → plan-review/TESTDRIVEN-PYRAMID-ANALYZER.md} +0 -0
  115. /package/dist/templates/cc-native/_cc-native/agents/{TRADEOFF-COSTS.md → plan-review/TRADEOFF-COSTS.md} +0 -0
  116. /package/dist/templates/cc-native/_cc-native/agents/{TRADEOFF-STAKEHOLDERS.md → plan-review/TRADEOFF-STAKEHOLDERS.md} +0 -0
  117. /package/dist/templates/cc-native/_cc-native/agents/{VERIFY-COVERAGE.md → plan-review/VERIFY-COVERAGE.md} +0 -0
  118. /package/dist/templates/cc-native/_cc-native/agents/{VERIFY-STRENGTH.md → plan-review/VERIFY-STRENGTH.md} +0 -0
@@ -35,14 +35,17 @@ import {
35
35
  emitContext,
36
36
  emitContextAndBlock,
37
37
  } from "../../_shared/lib-ts/base/hook-utils.js";
38
- import { isInternalCall } from "../../_shared/lib-ts/base/subprocess-utils.js";
38
+ import { isInternalCall, findExecutable } from "../../_shared/lib-ts/base/subprocess-utils.js";
39
39
  import { getProjectRoot, getAiwcliDir, getContextReviewsDir, getContextDir, getReviewFolderPath } from "../../_shared/lib-ts/base/constants.js";
40
40
  import { eprint } from "../../_shared/lib-ts/base/utils.js";
41
41
  import { getContextBySessionId, getAllContexts } from "../../_shared/lib-ts/context/context-store.js";
42
+ import { findPlanPathInTranscript } from "../../_shared/lib-ts/context/plan-manager.js";
42
43
 
43
44
  import type {
44
45
  AgentConfig,
45
46
  OrchestratorConfig,
47
+ ProviderConfig,
48
+ ModelsConfig,
46
49
  ReviewerResult,
47
50
  CombinedReviewResult,
48
51
  OrchestratorResult,
@@ -59,10 +62,14 @@ import {
59
62
  import {
60
63
  isPlanAlreadyReviewed,
61
64
  wasPlanPreviouslyDenied,
65
+ getLastPlanReview,
62
66
  markPlanReviewed,
67
+ wasQuestionsAsked,
68
+ markQuestionsAsked,
63
69
  } from "../lib-ts/cc-native-state.js";
64
70
 
65
- import { worstVerdict, computeReviewDecision } from "../lib-ts/verdict.js";
71
+ import { worstVerdict } from "../lib-ts/verdict.js";
72
+ import { computeCorroboratedDecision } from "../lib-ts/corroboration.js";
66
73
  import { loadConfig, getDisplaySettings } from "../lib-ts/config.js";
67
74
  import { runOrchestrator } from "../lib-ts/orchestrator.js";
68
75
  import { aggregateAgents } from "../lib-ts/aggregate-agents.js";
@@ -72,10 +79,13 @@ import {
72
79
  buildInlineReviewSummary,
73
80
  extractTopIssuesText,
74
81
  buildHighIssuesDocument,
82
+ buildCorroborationReport,
75
83
  writeReviewTracker,
76
84
  } from "../lib-ts/artifacts.js";
77
85
  import type { ReviewTrackerEntry } from "../lib-ts/artifacts.js";
78
- import { runAgentReview, runCodexReview, runGeminiReview } from "../lib-ts/reviewers/index.js";
86
+ import { runAgentReview } from "../lib-ts/reviewers/index.js";
87
+ import { DEFAULT_REVIEW_ITERATIONS } from "../lib-ts/state.js";
88
+ import { runPlanQuestions } from "../lib-ts/plan-questions.js";
79
89
 
80
90
  // ---------------------------------------------------------------------------
81
91
  // Hook Name
@@ -113,10 +123,7 @@ function extractTopIssuesForTracker(
113
123
  combined: CombinedReviewResult,
114
124
  maxCount = 5,
115
125
  ): string[] {
116
- const allReviewers = [
117
- ...Object.values(combined.cli_reviewers),
118
- ...Object.values(combined.agents),
119
- ];
126
+ const allReviewers = Object.values(combined.agents);
120
127
  const issues: string[] = [];
121
128
  for (const r of allReviewers) {
122
129
  if (!r.data) continue;
@@ -140,78 +147,68 @@ function extractTopIssuesForTracker(
140
147
  // ---------------------------------------------------------------------------
141
148
 
142
149
  /**
143
- * Determine which agents should graduate based on their review results.
144
- * Graduation criteria: verdict === "pass" OR zero high-severity issues.
145
- * Agents with "skip"/"error" do NOT graduate (no signal).
150
+ * Determine which agents are pass-eligible this iteration.
151
+ * Criteria: verdict === "pass" OR zero high-severity issues.
152
+ * Agents with "skip"/"error" are NOT eligible (no signal).
146
153
  */
147
- function computeGraduated(agentResults: Record<string, ReviewerResult>): string[] {
148
- const graduated: string[] = [];
154
+ function computePassEligible(agentResults: Record<string, ReviewerResult>): string[] {
155
+ const eligible: string[] = [];
149
156
  for (const [name, result] of Object.entries(agentResults)) {
150
157
  if (result.verdict === "skip" || result.verdict === "error") continue;
151
- if (result.verdict === "pass") { graduated.push(name); continue; }
158
+ if (result.verdict === "pass") { eligible.push(name); continue; }
152
159
  const issues = Array.isArray(result.data?.issues)
153
160
  ? (result.data.issues as Array<{ severity?: string }>) : [];
154
161
  if (issues.filter(i => i.severity === "high").length === 0) {
155
- graduated.push(name);
162
+ eligible.push(name);
156
163
  }
157
164
  }
158
- return graduated;
159
- }
160
-
161
- /**
162
- * Load the set of graduated agent names from previous iterations.
163
- * Returns empty set on iteration 1 (no iteration.json exists).
164
- */
165
- function loadGraduatedSet(reviewsDir: string): Set<string> {
166
- const existing = loadIterationState(reviewsDir);
167
- return new Set(existing?.graduated ?? []);
165
+ return eligible;
168
166
  }
169
167
 
170
168
  // ---------------------------------------------------------------------------
171
169
  // Default Configuration
172
170
  // ---------------------------------------------------------------------------
173
171
 
174
- const DEFAULT_AGENTS: Array<{ name: string; model: string; focus: string; enabled: boolean; categories: string[] }> = [
175
- { name: "handoff-readiness", model: "sonnet", focus: "fresh context execution readiness", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
176
- { name: "clarity-auditor", model: "sonnet", focus: "communication clarity and execution readiness", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
177
- { name: "skeptic", model: "sonnet", focus: "problem-solution alignment and assumption validation", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
178
- { name: "documentation-philosophy", model: "sonnet", focus: "knowledge capture and documentation placement", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
179
- { name: "risk-premortem", model: "sonnet", focus: "pre-mortem failure analysis", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
180
- { name: "risk-fmea", model: "sonnet", focus: "systematic failure mode analysis", enabled: true, categories: ["code", "infrastructure", "design"] },
181
- { name: "risk-dependency", model: "sonnet", focus: "dependency chain and blast radius analysis", enabled: true, categories: ["code", "infrastructure"] },
182
- { name: "risk-reversibility", model: "sonnet", focus: "decision reversibility and optionality", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
183
- { name: "completeness-gaps", model: "sonnet", focus: "structural gap analysis", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
184
- { name: "completeness-feasibility", model: "sonnet", focus: "feasibility and resource analysis", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
185
- { name: "completeness-ordering", model: "sonnet", focus: "step ordering and critical path analysis", enabled: true, categories: ["code", "infrastructure", "design"] },
186
- { name: "arch-structure", model: "sonnet", focus: "coupling, cohesion, and boundary analysis", enabled: true, categories: ["code", "infrastructure", "design"] },
187
- { name: "arch-evolution", model: "sonnet", focus: "evolutionary architecture and change amplification", enabled: true, categories: ["code", "infrastructure", "design"] },
188
- { name: "arch-patterns", model: "sonnet", focus: "pattern selection and technology fit", enabled: true, categories: ["code", "infrastructure"] },
189
- { name: "verify-coverage", model: "sonnet", focus: "verification coverage mapping", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
190
- { name: "verify-strength", model: "sonnet", focus: "test quality and mutation analysis", enabled: true, categories: ["code", "infrastructure"] },
191
- { name: "tradeoff-costs", model: "sonnet", focus: "opportunity cost and capability sacrifice", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
192
- { name: "tradeoff-stakeholders", model: "sonnet", focus: "stakeholder impact and cost-benefit asymmetry", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
193
- { name: "scope-boundary", model: "sonnet", focus: "scope drift and boundary enforcement", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
194
- { name: "hidden-complexity", model: "sonnet", focus: "understated complexity and hidden difficulty", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
195
- { name: "simplicity-guardian", model: "sonnet", focus: "over-engineering and unnecessary complexity", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
196
- { name: "devils-advocate", model: "sonnet", focus: "contrarian analysis and reductio ad absurdum", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
197
- { name: "assumption-tracer", model: "sonnet", focus: "dependency chains and foundational assumptions", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
198
- { name: "incremental-delivery", model: "sonnet", focus: "incremental delivery and vertical slicing", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
199
- { name: "constraint-validator", model: "sonnet", focus: "constraint identification and satisfaction", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
172
+ const ALL_CATEGORIES = ["code", "infrastructure", "documentation", "design", "research", "life", "business"];
173
+ const CODE_INFRA_DESIGN = ["code", "infrastructure", "design"];
174
+ const CODE_INFRA = ["code", "infrastructure"];
175
+ const AGENT_DEFAULTS = { model: "sonnet", provider: "claude", enabled: true } as const;
176
+
177
+ const DEFAULT_AGENTS: Array<{ name: string; model: string; provider: string; focus: string; enabled: boolean; categories: string[] }> = [
178
+ { ...AGENT_DEFAULTS, name: "handoff-readiness", focus: "fresh context execution readiness", categories: ALL_CATEGORIES },
179
+ { ...AGENT_DEFAULTS, name: "clarity-auditor", focus: "communication clarity and execution readiness", categories: ALL_CATEGORIES },
180
+ { ...AGENT_DEFAULTS, name: "skeptic", focus: "problem-solution alignment and assumption validation", categories: ALL_CATEGORIES },
181
+ { ...AGENT_DEFAULTS, name: "documentation-philosophy", focus: "knowledge capture and documentation placement", categories: ALL_CATEGORIES },
182
+ { ...AGENT_DEFAULTS, name: "risk-premortem", focus: "pre-mortem failure analysis", categories: ALL_CATEGORIES },
183
+ { ...AGENT_DEFAULTS, name: "risk-fmea", focus: "systematic failure mode analysis", categories: CODE_INFRA_DESIGN },
184
+ { ...AGENT_DEFAULTS, name: "risk-dependency", focus: "dependency chain and blast radius analysis", categories: CODE_INFRA },
185
+ { ...AGENT_DEFAULTS, name: "risk-reversibility", focus: "decision reversibility and optionality", categories: ALL_CATEGORIES },
186
+ { ...AGENT_DEFAULTS, name: "completeness-gaps", focus: "structural gap analysis", categories: ALL_CATEGORIES },
187
+ { ...AGENT_DEFAULTS, name: "completeness-feasibility", focus: "feasibility and resource analysis", categories: ALL_CATEGORIES },
188
+ { ...AGENT_DEFAULTS, name: "completeness-ordering", focus: "step ordering and critical path analysis", categories: CODE_INFRA_DESIGN },
189
+ { ...AGENT_DEFAULTS, name: "arch-structure", focus: "coupling, cohesion, and boundary analysis", categories: CODE_INFRA_DESIGN },
190
+ { ...AGENT_DEFAULTS, name: "arch-evolution", focus: "evolutionary architecture and change amplification", categories: CODE_INFRA_DESIGN },
191
+ { ...AGENT_DEFAULTS, name: "arch-patterns", focus: "pattern selection and technology fit", categories: CODE_INFRA },
192
+ { ...AGENT_DEFAULTS, name: "verify-coverage", focus: "verification coverage mapping", categories: ALL_CATEGORIES },
193
+ { ...AGENT_DEFAULTS, name: "verify-strength", focus: "test quality and mutation analysis", categories: CODE_INFRA },
194
+ { ...AGENT_DEFAULTS, name: "tradeoff-costs", focus: "opportunity cost and capability sacrifice", categories: ALL_CATEGORIES },
195
+ { ...AGENT_DEFAULTS, name: "tradeoff-stakeholders", focus: "stakeholder impact and cost-benefit asymmetry", categories: ALL_CATEGORIES },
196
+ { ...AGENT_DEFAULTS, name: "scope-boundary", focus: "scope drift and boundary enforcement", categories: ALL_CATEGORIES },
197
+ { ...AGENT_DEFAULTS, name: "hidden-complexity", focus: "understated complexity and hidden difficulty", categories: ALL_CATEGORIES },
198
+ { ...AGENT_DEFAULTS, name: "simplicity-guardian", focus: "over-engineering and unnecessary complexity", categories: ALL_CATEGORIES },
199
+ { ...AGENT_DEFAULTS, name: "devils-advocate", focus: "contrarian analysis and reductio ad absurdum", categories: ALL_CATEGORIES },
200
+ { ...AGENT_DEFAULTS, name: "assumption-tracer", focus: "dependency chains and foundational assumptions", categories: ALL_CATEGORIES },
201
+ { ...AGENT_DEFAULTS, name: "incremental-delivery", focus: "incremental delivery and vertical slicing", categories: ALL_CATEGORIES },
202
+ { ...AGENT_DEFAULTS, name: "constraint-validator", focus: "constraint identification and satisfaction", categories: ALL_CATEGORIES },
200
203
  ];
201
204
 
202
205
  const DEFAULT_ORCHESTRATOR: { enabled: boolean; model: string; timeout: number } = { enabled: true, model: "opus", timeout: 60 };
203
206
  const DEFAULT_AGENT_MODEL = "sonnet";
204
207
 
205
- const DEFAULT_REVIEW_ITERATIONS: Record<string, number> = {
206
- simple: 1,
207
- medium: 2,
208
- high: 2,
209
- };
210
-
211
208
  const DEFAULT_AGENT_SELECTION: Record<string, unknown> = {
212
209
  simple: { min: 3, max: 3 },
213
- medium: { min: 8, max: 8 },
214
- high: { min: 12, max: 12 },
210
+ medium: { min: 5, max: 5 },
211
+ high: { min: 7, max: 7 },
215
212
  fallbackCount: 3,
216
213
  };
217
214
 
@@ -298,35 +295,71 @@ function saveIterationState(reviewsDir: string, state: IterationState & { schema
298
295
  }
299
296
  }
300
297
 
301
- function getIterationStateFromContext(
302
- reviewsDir: string,
303
- complexity: string,
304
- config?: Record<string, unknown>,
305
- ): IterationState {
306
- const existing = loadIterationState(reviewsDir);
307
- if (existing) return existing;
308
- const reviewIterations: Record<string, number> = { ...DEFAULT_REVIEW_ITERATIONS };
309
- if (config) {
310
- const overrides = config.reviewIterations;
311
- if (overrides && typeof overrides === "object") {
312
- Object.assign(reviewIterations, overrides);
313
- }
298
+ // ---------------------------------------------------------------------------
299
+ // Model Provider Assignment
300
+ // ---------------------------------------------------------------------------
301
+
302
+ const DEFAULT_MODELS_CONFIG: ModelsConfig = {
303
+ providers: {
304
+ claude: { enabled: true, models: ["sonnet"] },
305
+ codex: { enabled: true, models: ["gpt-5.1-codex-mini"] },
306
+ },
307
+ };
308
+
309
+ function loadModelsConfig(settings: Record<string, unknown>): ModelsConfig {
310
+ const raw = settings.models as Record<string, unknown> | undefined;
311
+ if (!raw?.providers || typeof raw.providers !== "object") {
312
+ return DEFAULT_MODELS_CONFIG;
314
313
  }
315
- return {
316
- current: 1,
317
- max: reviewIterations[complexity] ?? 1,
318
- complexity,
319
- history: [],
320
- graduated: [],
321
- };
314
+ const providers: Record<string, ProviderConfig> = {};
315
+ for (const [name, cfg] of Object.entries(raw.providers as Record<string, unknown>)) {
316
+ const c = cfg as Record<string, unknown>;
317
+ providers[name] = {
318
+ enabled: c.enabled !== false,
319
+ models: Array.isArray(c.models) ? (c.models as string[]).filter(Boolean) : [],
320
+ };
321
+ }
322
+ return { providers };
323
+ }
324
+
325
+ function assignModelsToAgents(
326
+ agents: AgentConfig[],
327
+ modelsConfig: ModelsConfig,
328
+ ): AgentConfig[] {
329
+ // Filter to providers that are enabled, have models, AND whose CLI exists
330
+ const enabledProviders = Object.entries(modelsConfig.providers)
331
+ .filter(([name, config]) => {
332
+ if (!config.enabled || config.models.length === 0) return false;
333
+ const cliName = name === "claude" ? "claude" : name; // CLI name matches provider name
334
+ const found = findExecutable(cliName);
335
+ if (!found) {
336
+ logWarn(HOOK, `Provider '${name}' enabled but CLI '${cliName}' not found on PATH — skipping`);
337
+ }
338
+ return !!found;
339
+ });
340
+
341
+ if (enabledProviders.length === 0) {
342
+ logWarn(HOOK, "No providers with available CLI found, falling back to Claude with agent defaults");
343
+ return agents.map(a => ({ ...a, provider: "claude" }));
344
+ }
345
+
346
+ return agents.map(agent => {
347
+ const idx = Math.floor(Math.random() * enabledProviders.length);
348
+ const entry = enabledProviders[idx];
349
+ if (!entry) return { ...agent, provider: "claude" };
350
+ const [providerName, providerConfig] = entry;
351
+ const modelIdx = Math.floor(Math.random() * providerConfig.models.length);
352
+ const model = providerConfig.models[modelIdx] ?? providerConfig.models[0] ?? agent.model;
353
+ return { ...agent, provider: providerName, model };
354
+ });
322
355
  }
323
356
 
324
357
  // ---------------------------------------------------------------------------
325
358
  // Settings Loading
326
359
  // ---------------------------------------------------------------------------
327
360
 
328
- function loadSettings(projDir: string): Record<string, any> {
329
- const defaults: Record<string, any> = {
361
+ function loadSettings(projDir: string): Record<string, unknown> {
362
+ const defaults: Record<string, unknown> = {
330
363
  planReview: {
331
364
  enabled: true,
332
365
  reviewers: {
@@ -350,7 +383,7 @@ function loadSettings(projDir: string): Record<string, any> {
350
383
  };
351
384
 
352
385
  const config = loadConfig(projDir);
353
- if (!config || Object.keys(config).length === 0) return defaults;
386
+ if (!config || Object.keys(config).length === 0) return { ...defaults, models: {} };
354
387
 
355
388
  // Merge planReview
356
389
  const planReview = config.planReview ?? {};
@@ -376,14 +409,15 @@ function loadSettings(projDir: string): Record<string, any> {
376
409
  mergedAgent.sanitization = { ...DEFAULT_SANITIZATION, ...((configRecord.sanitization as Record<string, unknown>) ?? {}) };
377
410
  mergedAgent.reviewIterations = { ...DEFAULT_REVIEW_ITERATIONS, ...agentReview.reviewIterations ?? {} };
378
411
 
379
- return { planReview: mergedPlan, agentReview: mergedAgent };
412
+ const modelsRaw = (config as Record<string, unknown>).models ?? {};
413
+ return { planReview: mergedPlan, agentReview: mergedAgent, models: modelsRaw };
380
414
  }
381
415
 
382
416
  function loadAgentLibrary(
383
417
  projDir: string,
384
- settings?: Record<string, any>,
418
+ settings?: Record<string, unknown>,
385
419
  ): AgentConfig[] {
386
- const agentsData = aggregateAgents(path.join(projDir, "_cc-native", "agents"));
420
+ const agentsData = aggregateAgents(path.join(projDir, "_cc-native", "agents", "plan-review"));
387
421
  const defaultModel = settings?.agentDefaults?.model ?? DEFAULT_AGENT_MODEL;
388
422
 
389
423
  if (!agentsData || agentsData.length === 0) {
@@ -391,6 +425,7 @@ function loadAgentLibrary(
391
425
  return DEFAULT_AGENTS.map(a => ({
392
426
  name: a.name,
393
427
  model: a.model ?? defaultModel,
428
+ provider: a.provider ?? "claude",
394
429
  focus: a.focus ?? "general review",
395
430
  enabled: a.enabled ?? true,
396
431
  categories: a.categories ?? ["code"],
@@ -444,8 +479,23 @@ async function main(): Promise<void> {
444
479
  return;
445
480
  }
446
481
 
447
- // Find and read plan
448
- const planPath = findPlanFile();
482
+ // Find plan file: prefer transcript-based discovery (session-accurate), fall back to mtime scan
483
+ const transcriptPath = payload.transcript_path as string | undefined;
484
+ let planPath: string | null = null;
485
+
486
+ if (transcriptPath) {
487
+ planPath = findPlanPathInTranscript(transcriptPath);
488
+ if (planPath) {
489
+ logInfo(HOOK, `Found plan via transcript: ${planPath}`);
490
+ } else {
491
+ logDebug(HOOK, "No plan Write found in transcript, falling back to mtime scan");
492
+ }
493
+ }
494
+
495
+ if (!planPath) {
496
+ planPath = findPlanFile();
497
+ }
498
+
449
499
  if (!planPath) {
450
500
  skipWithInfo("No plan file found in ~/.claude/plans/. The plan may not have been written yet.");
451
501
  return;
@@ -467,6 +517,43 @@ async function main(): Promise<void> {
467
517
  logInfo(HOOK, `Found plan at: ${planPath}`);
468
518
  logDebug(HOOK, `Plan length: ${plan.length} chars`);
469
519
 
520
+ // ============================================
521
+ // Questions Gate: ask user questions before review
522
+ // ============================================
523
+ if (!wasQuestionsAsked(sessionId, base)) {
524
+ logInfo(HOOK, "Questions gate: user has not been asked questions yet, running plan-questions agent");
525
+ const timeout = typeof (settings.agentReview ?? {}).timeout === "number"
526
+ ? (settings.agentReview as Record<string, unknown>).timeout as number : 120;
527
+ const questionsResult = await runPlanQuestions(plan, aiwcliDir, timeout, undefined, sessionId);
528
+
529
+ // Mark questions as asked NOW — prevents infinite gate loop if Claude
530
+ // doesn't use AskUserQuestion after denial. Gate fires at most once.
531
+ markQuestionsAsked(sessionId, base);
532
+
533
+ const hasQuestions = questionsResult && (
534
+ questionsResult.questions.length > 0 ||
535
+ questionsResult.assumptions.length > 0 ||
536
+ questionsResult.ambiguities.length > 0
537
+ );
538
+
539
+ if (hasQuestions) {
540
+ const questionsList = questionsResult.questions.map((q: string, i: number) => `${i + 1}. ${q}`).join("\n");
541
+ const assumptionsList = questionsResult.assumptions.length > 0
542
+ ? `\n\nAssumptions detected:\n${questionsResult.assumptions.map((a: string) => `- ${a}`).join("\n")}`
543
+ : "";
544
+ const ambiguitiesList = questionsResult.ambiguities.length > 0
545
+ ? `\n\nAmbiguities detected:\n${questionsResult.ambiguities.map((a: string) => `- ${a}`).join("\n")}`
546
+ : "";
547
+ 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.`;
548
+ emitContextAndBlock(contextMsg, "Ask the user clarifying questions before submitting the plan. Use AskUserQuestion with the questions above.");
549
+ return;
550
+ }
551
+
552
+ logInfo(HOOK, "Questions gate: no questions generated, proceeding to review");
553
+ } else {
554
+ logInfo(HOOK, "Questions gate: questions already asked, skipping");
555
+ }
556
+
470
557
  const planHash = computePlanHash(plan);
471
558
  logDiagnostic(HOOK, "receive", `plan_size=${plan.length}, session=${sessionId.slice(0, 8)}`, {
472
559
  inputs: { plan_hash: planHash, plan_size: plan.length, session_id: sessionId.slice(0, 12) },
@@ -489,42 +576,57 @@ async function main(): Promise<void> {
489
576
  // Plan-hash deduplication
490
577
  logDebug(HOOK, `Plan hash: ${planHash}`);
491
578
  if (isPlanAlreadyReviewed(sessionId, planHash, base)) {
579
+ const lastReview = getLastPlanReview(sessionId, planHash, base);
580
+
492
581
  if (wasPlanPreviouslyDenied(sessionId, planHash, base)) {
582
+ // Plan unchanged since last FAIL verdict
493
583
  emitContextAndBlock(
494
584
  "[Plan Review] Plan content unchanged since last review which found issues.",
495
585
  "Plan unchanged since denial. Modify the plan to address review findings, then attempt ExitPlanMode again.",
496
586
  );
497
587
  return;
498
588
  } else {
499
- skipWithInfo("Plan already reviewed and approved (same hash).");
589
+ // Plan already reviewed with PASS or WARN verdict - skip review
590
+ const verdict = lastReview?.iteration?.latest_verdict || "pass";
591
+ const skipMsg = `[Plan Review] Plan already reviewed (verdict: ${verdict}). Skipping re-review.`;
592
+ emitContext(skipMsg);
593
+ logInfo(HOOK, skipMsg);
500
594
  return;
501
595
  }
502
596
  }
503
597
 
598
+ // Single load of iteration state — reused throughout, saved once at end.
599
+ // Default max=1 is safe: first iteration 1>1=false (runs), Edit E updates max from config before save.
600
+ let iterationState: IterationState = loadIterationState(reviewsDir) ?? {
601
+ current: 1, max: 1, complexity: "medium",
602
+ history: [], graduated: [], passStreaks: {}, lastPlanHash: "",
603
+ };
604
+
605
+ // Log plan hash changes for diagnostics (iteration counter no longer resets —
606
+ // plans change every iteration as Claude addresses feedback, so resetting
607
+ // would keep iteration perpetually at 1).
608
+ const lastHash = iterationState.lastPlanHash ?? "";
609
+ if (lastHash && lastHash !== planHash) {
610
+ logInfo(HOOK, `Plan hash changed (${lastHash.slice(0, 8)}→${planHash.slice(0, 8)}), iteration continues at ${iterationState.current}`);
611
+ }
612
+
504
613
  // Early iteration check: if we've exhausted max iterations, allow plan through
505
- const earlyIterState = loadIterationState(reviewsDir);
506
- if (earlyIterState && earlyIterState.current > earlyIterState.max) {
507
- skipWithInfo(`Max review iterations reached (${earlyIterState.current - 1}/${earlyIterState.max}), allowing plan through.`);
614
+ if (iterationState.current > iterationState.max) {
615
+ skipWithInfo(`Max review iterations reached (${iterationState.current - 1}/${iterationState.max}), allowing plan through.`);
508
616
  return;
509
617
  }
510
618
 
511
619
  // Initialize result containers
512
- const cliResults: Record<string, ReviewerResult> = {};
513
620
  let orchResult: OrchestratorResult | null = null;
514
621
  const agentResults: Record<string, ReviewerResult> = {};
515
- let allVerdicts: Verdict[] = [];
516
- let iterationState: IterationState | null = null;
517
622
  let detectedComplexity = "medium";
518
623
 
519
624
  // ============================================
520
- // PHASE 1 & 2: CLI Reviewers + Orchestrator (PARALLEL)
625
+ // PHASE 1: Orchestrator (Complexity Analysis)
521
626
  // ============================================
522
- const reviewersConfig = planReviewEnabled ? (planSettings.reviewers ?? {}) : {};
523
- const codexEnabled = planReviewEnabled && (reviewersConfig.codex?.enabled ?? true);
524
- const geminiEnabled = planReviewEnabled && (reviewersConfig.gemini?.enabled ?? false);
525
627
 
526
- // Load graduated agents from previous iterations (empty on iteration 1)
527
- const graduatedSet = loadGraduatedSet(reviewsDir);
628
+ // Graduated agents from previous iterations (empty after hash reset or on iteration 1)
629
+ const graduatedSet = new Set(iterationState.graduated);
528
630
  if (graduatedSet.size > 0) {
529
631
  logInfo(HOOK, `Graduated agents from previous iterations: ${[...graduatedSet].sort().join(", ")}`);
530
632
  }
@@ -546,7 +648,6 @@ async function main(): Promise<void> {
546
648
  const alwaysMandatory = resolveMandatoryAgents(mandatoryConfig, "simple");
547
649
  let mandatoryNames = alwaysMandatory;
548
650
 
549
- logDebug(HOOK, `Codex enabled: ${codexEnabled}, Gemini enabled: ${geminiEnabled}`);
550
651
  logDebug(HOOK, `Agent library: ${agentLibrary.map(a => a.name)}`);
551
652
  logDebug(HOOK, `Mandatory agents: ${[...mandatoryNames].sort()}`);
552
653
  logDebug(HOOK, `Orchestrator enabled: ${orchestratorConfig.enabled}`);
@@ -554,18 +655,6 @@ async function main(): Promise<void> {
554
655
  // Build phase 1 tasks as promises
555
656
  const phase1Promises: Array<{ name: string; promise: Promise<ReviewerResult | OrchestratorResult> }> = [];
556
657
 
557
- if (codexEnabled) {
558
- phase1Promises.push({
559
- name: "codex",
560
- promise: runCodexReview(plan, REVIEW_SCHEMA, planSettings),
561
- });
562
- }
563
- if (geminiEnabled) {
564
- phase1Promises.push({
565
- name: "gemini",
566
- promise: runGeminiReview(plan, REVIEW_SCHEMA, planSettings),
567
- });
568
- }
569
658
  if (orchestratorConfig.enabled && enabledAgents.length > 0 && !legacyMode) {
570
659
  phase1Promises.push({
571
660
  name: "orchestrator",
@@ -594,9 +683,7 @@ async function main(): Promise<void> {
594
683
  }
595
684
  }
596
685
 
597
- // Collect CLI results
598
- if (phase1Results.codex) cliResults.codex = phase1Results.codex as ReviewerResult;
599
- if (phase1Results.gemini) cliResults.gemini = phase1Results.gemini as ReviewerResult;
686
+ // Collect orchestrator result
600
687
  if (phase1Results.orchestrator) orchResult = phase1Results.orchestrator as OrchestratorResult;
601
688
 
602
689
  // ============================================
@@ -606,7 +693,7 @@ async function main(): Promise<void> {
606
693
  logInfo(HOOK, "=== PHASE 2: Agent Selection ===");
607
694
 
608
695
  let selectedAgents: AgentConfig[] = [];
609
- const fallbackByComplexity = agentSettings.fallbackByComplexity ?? { simple: 0, medium: 5, high: 9 };
696
+ const fallbackByComplexity = agentSettings.fallbackByComplexity ?? { simple: 0, medium: 2, high: 4 };
610
697
 
611
698
  if (enabledAgents.length > 0) {
612
699
  let mandatoryAgents = enabledAgents.filter(a => mandatoryNames.has(a.name));
@@ -671,11 +758,19 @@ async function main(): Promise<void> {
671
758
  },
672
759
  });
673
760
 
674
- // Initialize iteration state
675
- if (reviewsDir) {
676
- iterationState = getIterationStateFromContext(reviewsDir, detectedComplexity, agentSettings);
677
- logDebug(HOOK, `Iteration state: ${iterationState.current}/${iterationState.max} (${detectedComplexity})`);
678
- }
761
+ // Update complexity/max on the already-loaded iteration state (no second disk read)
762
+ const reviewIterations: Record<string, number> = {
763
+ ...DEFAULT_REVIEW_ITERATIONS,
764
+ ...(agentSettings.reviewIterations ?? {}),
765
+ };
766
+ iterationState.complexity = detectedComplexity;
767
+ iterationState.max = reviewIterations[detectedComplexity] ?? iterationState.max;
768
+ logDebug(HOOK, `Iteration state: ${iterationState.current}/${iterationState.max} (${detectedComplexity})`);
769
+
770
+ // Assign random providers + models to selected agents
771
+ const modelsConfig = loadModelsConfig(settings);
772
+ selectedAgents = assignModelsToAgents(selectedAgents, modelsConfig);
773
+ logInfo(HOOK, `Model assignments: ${selectedAgents.map(a => `${a.name}→${a.provider}:${a.model}`).join(", ")}`);
679
774
 
680
775
  // PHASE 3: Run selected agents in parallel
681
776
  if (selectedAgents.length > 0) {
@@ -716,20 +811,36 @@ async function main(): Promise<void> {
716
811
  }
717
812
 
718
813
  // ============================================
719
- // Persist newly graduated agents (before verdict overrides)
814
+ // Enforce per-agent issue limit (truncate to top N by severity)
815
+ // ============================================
816
+ const maxIssuesPerAgent = typeof agentSettings.maxIssuesPerAgent === "number"
817
+ ? agentSettings.maxIssuesPerAgent : 3;
818
+
819
+ for (const r of Object.values(agentResults)) {
820
+ if (!Array.isArray(r.data?.issues)) continue;
821
+ const issues = r.data.issues as Array<{ severity?: string }>;
822
+ if (issues.length <= maxIssuesPerAgent) continue;
823
+ const severityOrder: Record<string, number> = { high: 0, medium: 1, low: 2 };
824
+ issues.sort((a, b) => (severityOrder[a.severity ?? "low"] ?? 2) - (severityOrder[b.severity ?? "low"] ?? 2));
825
+ const originalCount = issues.length;
826
+ r.data.issues = issues.slice(0, maxIssuesPerAgent);
827
+ logInfo(HOOK, `${r.name}: truncated issues ${originalCount} → ${maxIssuesPerAgent}`);
828
+ }
829
+
830
+ // ============================================
831
+ // Compute pass-eligible agents (before verdict overrides)
720
832
  // ============================================
721
- const newlyGraduated = computeGraduated(agentResults);
722
- if (newlyGraduated.length > 0) {
723
- logInfo(HOOK, `Newly graduated agents: ${newlyGraduated.join(", ")}`);
833
+ const passEligible = computePassEligible(agentResults);
834
+ if (passEligible.length > 0) {
835
+ logInfo(HOOK, `Pass-eligible agents this iteration: ${passEligible.join(", ")}`);
724
836
  }
725
837
 
726
838
  // ============================================
727
839
  // Per-agent high-severity threshold: override verdict to "fail"
728
840
  // ============================================
729
841
  const highIssueThreshold = typeof agentSettings.highIssueThreshold === "number" ? agentSettings.highIssueThreshold : 3;
730
- allVerdicts = [];
731
842
 
732
- for (const r of [...Object.values(cliResults), ...Object.values(agentResults)]) {
843
+ for (const r of Object.values(agentResults)) {
733
844
  if (!r.verdict || r.verdict === "skip" || r.verdict === "error") continue;
734
845
  const issues = Array.isArray(r.data?.issues) ? r.data.issues as Array<{ severity?: string }> : [];
735
846
  const agentHigh = issues.filter(i => i.severity === "high").length;
@@ -739,7 +850,6 @@ async function main(): Promise<void> {
739
850
  verdict = "fail";
740
851
  r.verdict = verdict;
741
852
  }
742
- allVerdicts.push(verdict);
743
853
  }
744
854
 
745
855
  // ============================================
@@ -747,7 +857,7 @@ async function main(): Promise<void> {
747
857
  // ============================================
748
858
  logInfo(HOOK, "=== PHASE 4: Generate Output ===");
749
859
 
750
- if (Object.keys(cliResults).length === 0 && Object.keys(agentResults).length === 0) {
860
+ if (Object.keys(agentResults).length === 0) {
751
861
  if (graduatedSet.size > 0 && originalAgentCount > 0) {
752
862
  skipWithInfo("All agent reviewers graduated from previous iterations — no review needed.");
753
863
  } else {
@@ -756,12 +866,17 @@ async function main(): Promise<void> {
756
866
  return;
757
867
  }
758
868
 
759
- const overall = allVerdicts.length > 0 ? worstVerdict(allVerdicts) : "pass";
869
+ // Review decision corroboration-based (proportional threshold per dimension)
870
+ // Must be computed before writeCombinedArtifacts and buildInlineReviewSummary which consume it.
871
+ const allReviewerResults: Record<string, ReviewerResult> = agentResults;
872
+ const corroborationResult = computeCorroboratedDecision(allReviewerResults);
873
+
874
+ // Use corroboration verdict as single source of truth (not worstVerdict from individual agents)
875
+ const overall = corroborationResult.verdict;
760
876
 
761
877
  const combinedResult: CombinedReviewResult = {
762
878
  plan_hash: planHash,
763
879
  overall_verdict: overall,
764
- cli_reviewers: cliResults,
765
880
  orchestration: orchResult,
766
881
  agents: agentResults,
767
882
  timestamp: new Date().toISOString(),
@@ -774,7 +889,7 @@ async function main(): Promise<void> {
774
889
  const combinedSettings = { display: displaySettings };
775
890
 
776
891
  // Get current iteration number
777
- const currentIteration = iterationState?.current ?? 1;
892
+ const currentIteration = iterationState.current;
778
893
 
779
894
  // Create review folder
780
895
  const reviewFolder = getReviewFolderPath(contextId, currentIteration, base);
@@ -790,9 +905,16 @@ async function main(): Promise<void> {
790
905
  undefined,
791
906
  reviewFolder,
792
907
  currentIteration,
908
+ corroborationResult,
793
909
  );
794
910
  logInfo(HOOK, `Saved review: ${reviewFile}`);
795
911
 
912
+ // Write corroboration analysis report
913
+ const corroborationReport = buildCorroborationReport(corroborationResult);
914
+ const corroborationPath = path.join(reviewFolder, "corroboration.md");
915
+ fs.writeFileSync(corroborationPath, corroborationReport, "utf-8");
916
+ logInfo(HOOK, `Saved corroboration report: ${corroborationPath}`);
917
+
796
918
  // Save plan snapshot for diffing between iterations
797
919
  try {
798
920
  fs.writeFileSync(path.join(reviewFolder, "plan.md"), plan, "utf-8");
@@ -802,16 +924,16 @@ async function main(): Promise<void> {
802
924
  }
803
925
 
804
926
  // Build inline summary with top issues (always emitted, even on pass)
805
- const inlineSummary = buildInlineReviewSummary(combinedResult);
927
+ const inlineSummary = buildInlineReviewSummary(combinedResult, 5, 800, corroborationResult);
806
928
  const topIssuesList = extractTopIssuesForTracker(combinedResult, 5);
807
929
  const contextParts = [inlineSummary];
808
930
  if (topIssuesList.length > 0) {
809
931
  contextParts.push(`\nTop high-severity issues:\n${topIssuesList.map(i => `- ${i}`).join("\n")}`);
810
932
  }
811
933
  contextParts.push(`\nFull review: \`${reviewFile}\`\n`);
812
-
813
- // Review decision
814
- const { should_deny: shouldDeny, reason: denyReason, score: reviewScore } = computeReviewDecision(allVerdicts);
934
+ const shouldDeny = corroborationResult.blocking.length > 0;
935
+ const denyReason = shouldDeny ? "corroborated_issues" : "no_corroboration";
936
+ const reviewScore = shouldDeny ? 1.0 : 0.0;
815
937
 
816
938
  logInfo(HOOK, `REVIEW_DECISION: verdict=${combinedResult.overall_verdict}, deny=${shouldDeny}, reason=${denyReason}, score=${reviewScore.toFixed(2)}`);
817
939
  logDiagnostic(HOOK, "result", `verdict=${combinedResult.overall_verdict}, deny=${shouldDeny}, reason=${denyReason}`, {
@@ -820,7 +942,6 @@ async function main(): Promise<void> {
820
942
  inputs: {
821
943
  overall_verdict: combinedResult.overall_verdict,
822
944
  review_score: Math.round(reviewScore * 100) / 100,
823
- cli_count: Object.keys(cliResults).length,
824
945
  agent_count: Object.keys(agentResults).length,
825
946
  },
826
947
  });
@@ -833,34 +954,51 @@ async function main(): Promise<void> {
833
954
  }
834
955
 
835
956
  // Iteration logic:
836
- // - On FAIL at max: extend max by 1 (grant one more revision chance)
837
- // - On WARN: block but do NOT extend max (warns don't earn extra iterations)
838
- // - On PASS: jump current to max so next call triggers early exit (no more reviews)
839
- const isFail = overall === "fail";
840
- if (iterationState && reviewsDir) {
957
+ // - On PASS/WARRANT: set current past max so no more reviews happen
958
+ // - On DENY (fail/warn): increment current toward max (safety valve)
959
+ // - Max iterations (high=5, medium=3, simple=1) caps total reviews before auto-allow
960
+ if (reviewsDir) {
841
961
  iterationState.history.push({ hash: planHash, verdict: overall, timestamp: new Date().toISOString() });
842
-
843
- if (isFail && iterationState.current >= iterationState.max) {
844
- iterationState.max += 1;
845
- logInfo(HOOK, `Extending max iterations to ${iterationState.max} due to fail at boundary (${iterationState.current}/${iterationState.max})`);
846
- }
962
+ iterationState.lastPlanHash = planHash;
847
963
 
848
964
  if (!shouldDeny) {
849
- // Pass: set current to max so next call (current+1 > max) triggers early exit
850
- iterationState.current = iterationState.max;
851
- logInfo(HOOK, `Pass: setting current to max (${iterationState.max}) to exhaust iterations`);
965
+ // Pass/warrant: stop iterating set current past max
966
+ iterationState.current = iterationState.max + 1;
967
+ logInfo(HOOK, `Pass/warrant: stopping iterations`);
968
+ } else {
969
+ // Deny: advance iteration counter toward max so safety valve triggers
970
+ iterationState.current += 1;
971
+ logInfo(HOOK, `Deny: advancing iteration (${iterationState.current}/${iterationState.max})`);
852
972
  }
853
973
 
854
- // Merge newly graduated agents into persistent state
855
- if (newlyGraduated.length > 0) {
856
- const allGraduated = new Set([
857
- ...(iterationState.graduated ?? []),
858
- ...newlyGraduated,
859
- ]);
860
- iterationState.graduated = [...allGraduated];
974
+ // Update pass streaks — only for agents that actually ran this iteration
975
+ const passStreaks = { ...(iterationState.passStreaks ?? {}) };
976
+ const passEligibleSet = new Set(passEligible);
977
+ const graduatedSetCurrent = new Set(iterationState.graduated);
978
+
979
+ for (const name of Object.keys(agentResults)) {
980
+ if (graduatedSetCurrent.has(name)) continue;
981
+ if (passEligibleSet.has(name)) {
982
+ passStreaks[name] = (passStreaks[name] ?? 0) + 1;
983
+ } else {
984
+ passStreaks[name] = 0;
985
+ }
986
+ }
987
+ iterationState.passStreaks = passStreaks;
988
+
989
+ // Graduate agents that reached threshold
990
+ const GRADUATION_THRESHOLD = 2;
991
+ const newGrads: string[] = [];
992
+ for (const [name, streak] of Object.entries(passStreaks)) {
993
+ if (streak >= GRADUATION_THRESHOLD && !graduatedSetCurrent.has(name)) {
994
+ newGrads.push(name);
995
+ }
996
+ }
997
+ if (newGrads.length > 0) {
998
+ iterationState.graduated = [...iterationState.graduated, ...newGrads];
999
+ logInfo(HOOK, `Newly graduated (${GRADUATION_THRESHOLD} consecutive passes): ${newGrads.join(", ")}`);
861
1000
  }
862
1001
 
863
- iterationState.current += 1;
864
1002
  saveIterationState(reviewsDir, iterationState);
865
1003
  }
866
1004
 
@@ -874,13 +1012,14 @@ async function main(): Promise<void> {
874
1012
  verdict: combinedResult.overall_verdict,
875
1013
  decision: trackerDecision,
876
1014
  score: reviewScore,
877
- topIssues: extractTopIssuesForTracker(combinedResult, 5),
1015
+ topIssues: topIssuesList,
878
1016
  reviewFolder,
879
1017
  };
880
1018
  writeReviewTracker(ccNativeReviewsDir, trackerEntry);
881
1019
  logInfo(HOOK, `Updated review tracker: ${path.join(ccNativeReviewsDir, "review-tracker.md")}`);
882
1020
 
883
- // Emit output always emit context with top issues + link; block only on fail
1021
+ // ALL first-time reviews block ExitPlanMode and inject feedback
1022
+ // Verdict controls iteration logic and next-run skip decision only
884
1023
  const contextText = contextParts.join("");
885
1024
 
886
1025
  logDebug(HOOK, `REVIEW_CONTEXT_INJECTED: chars=${contextText.length}, inline_chars=${inlineSummary.length}`);
@@ -888,33 +1027,34 @@ async function main(): Promise<void> {
888
1027
  const REVIEWER_CAVEAT = "Reviewers have limited context compared to your full session — use your judgment to adopt valid points and dismiss genuine false positives. However, treat false positives as a clarity signal: if a reviewer misunderstood your plan, an agent executing it will likely hit the same confusion. Revise those sections to be unambiguous so no future reader — human or AI — makes the same mistake.";
889
1028
  const RESUBMIT_INSTRUCTION = "IMPORTANT: After revising the plan file, you MUST call ExitPlanMode again to trigger re-review. Do not end your turn or ask the user without calling ExitPlanMode.";
890
1029
 
1030
+ const iterInfo = ` (iteration ${iterationState.current - 1}/${iterationState.max}, score=${reviewScore.toFixed(2)})`;
1031
+
891
1032
  if (shouldDeny) {
892
- const disposition = iterationState
893
- ? `hook_deny_iter_${iterationState.current - 1}`
894
- : "hook_deny";
895
- markPlanReviewed(sessionId, planHash, base, HOOK, iterationState ?? undefined, disposition);
1033
+ // FAIL verdict - critical issues found
1034
+ const disposition = `hook_deny_iter_${iterationState.current - 1}`;
1035
+ markPlanReviewed(sessionId, planHash, base, HOOK, iterationState, disposition);
896
1036
  const topIssuesText = extractTopIssuesText(combinedResult, 3, "high");
897
- const highIssuesDoc = buildHighIssuesDocument(combinedResult);
1037
+ const highIssuesDoc = buildHighIssuesDocument(combinedResult, corroborationResult);
898
1038
  const highIssuesPath = path.join(reviewFolder, "high-issues.md");
899
1039
  fs.writeFileSync(highIssuesPath, highIssuesDoc, "utf-8");
900
1040
 
901
- const iterInfo = iterationState
902
- ? ` (iteration ${iterationState.current - 1}/${iterationState.max}, score=${reviewScore.toFixed(2)})`
903
- : ` (score=${reviewScore.toFixed(2)})`;
904
-
905
- emitContextAndBlock(
906
- contextText,
907
- `Plan review FAILED${iterInfo}. ` +
1041
+ const blockReason = `Plan review FAILED${iterInfo}. ` +
908
1042
  `Critical issues: ${topIssuesText}. ` +
909
1043
  `IMPORTANT: Read \`${highIssuesPath}\` for ALL high-severity issues — ` +
910
1044
  `this file contains only the most critical findings, no noise. ` +
911
1045
  `${REVIEWER_CAVEAT} ` +
912
1046
  `Revise the plan to address these issues, then call ExitPlanMode again. ` +
913
- RESUBMIT_INSTRUCTION,
914
- );
1047
+ RESUBMIT_INSTRUCTION;
1048
+
1049
+ emitContextAndBlock(contextText, blockReason);
915
1050
  } else {
916
- markPlanReviewed(sessionId, planHash, base, HOOK, iterationState ?? undefined, "allow");
917
- emitContext(contextText);
1051
+ // PASS or WARN verdict - block to inject feedback, but mark as allowed
1052
+ const disposition = `hook_allow_iter_${iterationState.current - 1}`;
1053
+ markPlanReviewed(sessionId, planHash, base, HOOK, iterationState, disposition);
1054
+
1055
+ const blockReason = `Plan review ${overall.toUpperCase()}${iterInfo}. Review complete. ${REVIEWER_CAVEAT}`;
1056
+
1057
+ emitContextAndBlock(contextText, blockReason);
918
1058
  }
919
1059
  }
920
1060