aiwcli 0.11.0 → 0.11.1

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 (25) hide show
  1. package/dist/templates/_shared/hooks-ts/session_end.ts +4 -1
  2. package/dist/templates/_shared/lib-ts/base/git-state.ts +1 -1
  3. package/dist/templates/_shared/lib-ts/base/logger.ts +15 -18
  4. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +18 -15
  5. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +26 -30
  6. package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +12 -13
  7. package/dist/templates/_shared/scripts/resume_handoff.ts +62 -38
  8. package/dist/templates/_shared/scripts/save_handoff.ts +24 -24
  9. package/dist/templates/_shared/scripts/status_line.ts +101 -147
  10. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +239 -133
  11. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +11 -12
  12. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +139 -56
  13. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +22 -2
  14. package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +115 -0
  15. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +1 -0
  16. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +7 -1
  17. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +5 -4
  18. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +133 -13
  19. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +6 -6
  20. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +5 -4
  21. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +30 -33
  22. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +118 -43
  23. package/dist/templates/cc-native/_cc-native/plan-review.config.json +21 -0
  24. package/oclif.manifest.json +1 -1
  25. package/package.json +2 -2
@@ -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,
@@ -62,7 +65,8 @@ import {
62
65
  markPlanReviewed,
63
66
  } from "../lib-ts/cc-native-state.js";
64
67
 
65
- import { worstVerdict, computeReviewDecision } from "../lib-ts/verdict.js";
68
+ import { worstVerdict } from "../lib-ts/verdict.js";
69
+ import { computeCorroboratedDecision } from "../lib-ts/corroboration.js";
66
70
  import { loadConfig, getDisplaySettings } from "../lib-ts/config.js";
67
71
  import { runOrchestrator } from "../lib-ts/orchestrator.js";
68
72
  import { aggregateAgents } from "../lib-ts/aggregate-agents.js";
@@ -76,6 +80,7 @@ import {
76
80
  } from "../lib-ts/artifacts.js";
77
81
  import type { ReviewTrackerEntry } from "../lib-ts/artifacts.js";
78
82
  import { runAgentReview, runCodexReview, runGeminiReview } from "../lib-ts/reviewers/index.js";
83
+ import { DEFAULT_REVIEW_ITERATIONS } from "../lib-ts/state.js";
79
84
 
80
85
  // ---------------------------------------------------------------------------
81
86
  // Hook Name
@@ -140,78 +145,68 @@ function extractTopIssuesForTracker(
140
145
  // ---------------------------------------------------------------------------
141
146
 
142
147
  /**
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).
148
+ * Determine which agents are pass-eligible this iteration.
149
+ * Criteria: verdict === "pass" OR zero high-severity issues.
150
+ * Agents with "skip"/"error" are NOT eligible (no signal).
146
151
  */
147
- function computeGraduated(agentResults: Record<string, ReviewerResult>): string[] {
148
- const graduated: string[] = [];
152
+ function computePassEligible(agentResults: Record<string, ReviewerResult>): string[] {
153
+ const eligible: string[] = [];
149
154
  for (const [name, result] of Object.entries(agentResults)) {
150
155
  if (result.verdict === "skip" || result.verdict === "error") continue;
151
- if (result.verdict === "pass") { graduated.push(name); continue; }
156
+ if (result.verdict === "pass") { eligible.push(name); continue; }
152
157
  const issues = Array.isArray(result.data?.issues)
153
158
  ? (result.data.issues as Array<{ severity?: string }>) : [];
154
159
  if (issues.filter(i => i.severity === "high").length === 0) {
155
- graduated.push(name);
160
+ eligible.push(name);
156
161
  }
157
162
  }
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 ?? []);
163
+ return eligible;
168
164
  }
169
165
 
170
166
  // ---------------------------------------------------------------------------
171
167
  // Default Configuration
172
168
  // ---------------------------------------------------------------------------
173
169
 
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"] },
170
+ const ALL_CATEGORIES = ["code", "infrastructure", "documentation", "design", "research", "life", "business"];
171
+ const CODE_INFRA_DESIGN = ["code", "infrastructure", "design"];
172
+ const CODE_INFRA = ["code", "infrastructure"];
173
+ const AGENT_DEFAULTS = { model: "sonnet", provider: "claude", enabled: true } as const;
174
+
175
+ const DEFAULT_AGENTS: Array<{ name: string; model: string; provider: string; focus: string; enabled: boolean; categories: string[] }> = [
176
+ { ...AGENT_DEFAULTS, name: "handoff-readiness", focus: "fresh context execution readiness", categories: ALL_CATEGORIES },
177
+ { ...AGENT_DEFAULTS, name: "clarity-auditor", focus: "communication clarity and execution readiness", categories: ALL_CATEGORIES },
178
+ { ...AGENT_DEFAULTS, name: "skeptic", focus: "problem-solution alignment and assumption validation", categories: ALL_CATEGORIES },
179
+ { ...AGENT_DEFAULTS, name: "documentation-philosophy", focus: "knowledge capture and documentation placement", categories: ALL_CATEGORIES },
180
+ { ...AGENT_DEFAULTS, name: "risk-premortem", focus: "pre-mortem failure analysis", categories: ALL_CATEGORIES },
181
+ { ...AGENT_DEFAULTS, name: "risk-fmea", focus: "systematic failure mode analysis", categories: CODE_INFRA_DESIGN },
182
+ { ...AGENT_DEFAULTS, name: "risk-dependency", focus: "dependency chain and blast radius analysis", categories: CODE_INFRA },
183
+ { ...AGENT_DEFAULTS, name: "risk-reversibility", focus: "decision reversibility and optionality", categories: ALL_CATEGORIES },
184
+ { ...AGENT_DEFAULTS, name: "completeness-gaps", focus: "structural gap analysis", categories: ALL_CATEGORIES },
185
+ { ...AGENT_DEFAULTS, name: "completeness-feasibility", focus: "feasibility and resource analysis", categories: ALL_CATEGORIES },
186
+ { ...AGENT_DEFAULTS, name: "completeness-ordering", focus: "step ordering and critical path analysis", categories: CODE_INFRA_DESIGN },
187
+ { ...AGENT_DEFAULTS, name: "arch-structure", focus: "coupling, cohesion, and boundary analysis", categories: CODE_INFRA_DESIGN },
188
+ { ...AGENT_DEFAULTS, name: "arch-evolution", focus: "evolutionary architecture and change amplification", categories: CODE_INFRA_DESIGN },
189
+ { ...AGENT_DEFAULTS, name: "arch-patterns", focus: "pattern selection and technology fit", categories: CODE_INFRA },
190
+ { ...AGENT_DEFAULTS, name: "verify-coverage", focus: "verification coverage mapping", categories: ALL_CATEGORIES },
191
+ { ...AGENT_DEFAULTS, name: "verify-strength", focus: "test quality and mutation analysis", categories: CODE_INFRA },
192
+ { ...AGENT_DEFAULTS, name: "tradeoff-costs", focus: "opportunity cost and capability sacrifice", categories: ALL_CATEGORIES },
193
+ { ...AGENT_DEFAULTS, name: "tradeoff-stakeholders", focus: "stakeholder impact and cost-benefit asymmetry", categories: ALL_CATEGORIES },
194
+ { ...AGENT_DEFAULTS, name: "scope-boundary", focus: "scope drift and boundary enforcement", categories: ALL_CATEGORIES },
195
+ { ...AGENT_DEFAULTS, name: "hidden-complexity", focus: "understated complexity and hidden difficulty", categories: ALL_CATEGORIES },
196
+ { ...AGENT_DEFAULTS, name: "simplicity-guardian", focus: "over-engineering and unnecessary complexity", categories: ALL_CATEGORIES },
197
+ { ...AGENT_DEFAULTS, name: "devils-advocate", focus: "contrarian analysis and reductio ad absurdum", categories: ALL_CATEGORIES },
198
+ { ...AGENT_DEFAULTS, name: "assumption-tracer", focus: "dependency chains and foundational assumptions", categories: ALL_CATEGORIES },
199
+ { ...AGENT_DEFAULTS, name: "incremental-delivery", focus: "incremental delivery and vertical slicing", categories: ALL_CATEGORIES },
200
+ { ...AGENT_DEFAULTS, name: "constraint-validator", focus: "constraint identification and satisfaction", categories: ALL_CATEGORIES },
200
201
  ];
201
202
 
202
203
  const DEFAULT_ORCHESTRATOR: { enabled: boolean; model: string; timeout: number } = { enabled: true, model: "opus", timeout: 60 };
203
204
  const DEFAULT_AGENT_MODEL = "sonnet";
204
205
 
205
- const DEFAULT_REVIEW_ITERATIONS: Record<string, number> = {
206
- simple: 1,
207
- medium: 2,
208
- high: 2,
209
- };
210
-
211
206
  const DEFAULT_AGENT_SELECTION: Record<string, unknown> = {
212
207
  simple: { min: 3, max: 3 },
213
- medium: { min: 8, max: 8 },
214
- high: { min: 12, max: 12 },
208
+ medium: { min: 5, max: 5 },
209
+ high: { min: 7, max: 7 },
215
210
  fallbackCount: 3,
216
211
  };
217
212
 
@@ -298,35 +293,71 @@ function saveIterationState(reviewsDir: string, state: IterationState & { schema
298
293
  }
299
294
  }
300
295
 
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
- }
296
+ // ---------------------------------------------------------------------------
297
+ // Model Provider Assignment
298
+ // ---------------------------------------------------------------------------
299
+
300
+ const DEFAULT_MODELS_CONFIG: ModelsConfig = {
301
+ providers: {
302
+ claude: { enabled: true, models: ["sonnet"] },
303
+ codex: { enabled: true, models: ["gpt-5.1-codex-mini"] },
304
+ },
305
+ };
306
+
307
+ function loadModelsConfig(settings: Record<string, unknown>): ModelsConfig {
308
+ const raw = settings.models as Record<string, unknown> | undefined;
309
+ if (!raw?.providers || typeof raw.providers !== "object") {
310
+ return DEFAULT_MODELS_CONFIG;
314
311
  }
315
- return {
316
- current: 1,
317
- max: reviewIterations[complexity] ?? 1,
318
- complexity,
319
- history: [],
320
- graduated: [],
321
- };
312
+ const providers: Record<string, ProviderConfig> = {};
313
+ for (const [name, cfg] of Object.entries(raw.providers as Record<string, unknown>)) {
314
+ const c = cfg as Record<string, unknown>;
315
+ providers[name] = {
316
+ enabled: c.enabled !== false,
317
+ models: Array.isArray(c.models) ? (c.models as string[]).filter(Boolean) : [],
318
+ };
319
+ }
320
+ return { providers };
321
+ }
322
+
323
+ function assignModelsToAgents(
324
+ agents: AgentConfig[],
325
+ modelsConfig: ModelsConfig,
326
+ ): AgentConfig[] {
327
+ // Filter to providers that are enabled, have models, AND whose CLI exists
328
+ const enabledProviders = Object.entries(modelsConfig.providers)
329
+ .filter(([name, config]) => {
330
+ if (!config.enabled || config.models.length === 0) return false;
331
+ const cliName = name === "claude" ? "claude" : name; // CLI name matches provider name
332
+ const found = findExecutable(cliName);
333
+ if (!found) {
334
+ logWarn(HOOK, `Provider '${name}' enabled but CLI '${cliName}' not found on PATH — skipping`);
335
+ }
336
+ return !!found;
337
+ });
338
+
339
+ if (enabledProviders.length === 0) {
340
+ logWarn(HOOK, "No providers with available CLI found, falling back to Claude with agent defaults");
341
+ return agents.map(a => ({ ...a, provider: "claude" }));
342
+ }
343
+
344
+ return agents.map(agent => {
345
+ const idx = Math.floor(Math.random() * enabledProviders.length);
346
+ const entry = enabledProviders[idx];
347
+ if (!entry) return { ...agent, provider: "claude" };
348
+ const [providerName, providerConfig] = entry;
349
+ const modelIdx = Math.floor(Math.random() * providerConfig.models.length);
350
+ const model = providerConfig.models[modelIdx] ?? providerConfig.models[0] ?? agent.model;
351
+ return { ...agent, provider: providerName, model };
352
+ });
322
353
  }
323
354
 
324
355
  // ---------------------------------------------------------------------------
325
356
  // Settings Loading
326
357
  // ---------------------------------------------------------------------------
327
358
 
328
- function loadSettings(projDir: string): Record<string, any> {
329
- const defaults: Record<string, any> = {
359
+ function loadSettings(projDir: string): Record<string, unknown> {
360
+ const defaults: Record<string, unknown> = {
330
361
  planReview: {
331
362
  enabled: true,
332
363
  reviewers: {
@@ -350,7 +381,7 @@ function loadSettings(projDir: string): Record<string, any> {
350
381
  };
351
382
 
352
383
  const config = loadConfig(projDir);
353
- if (!config || Object.keys(config).length === 0) return defaults;
384
+ if (!config || Object.keys(config).length === 0) return { ...defaults, models: {} };
354
385
 
355
386
  // Merge planReview
356
387
  const planReview = config.planReview ?? {};
@@ -376,12 +407,13 @@ function loadSettings(projDir: string): Record<string, any> {
376
407
  mergedAgent.sanitization = { ...DEFAULT_SANITIZATION, ...((configRecord.sanitization as Record<string, unknown>) ?? {}) };
377
408
  mergedAgent.reviewIterations = { ...DEFAULT_REVIEW_ITERATIONS, ...agentReview.reviewIterations ?? {} };
378
409
 
379
- return { planReview: mergedPlan, agentReview: mergedAgent };
410
+ const modelsRaw = (config as Record<string, unknown>).models ?? {};
411
+ return { planReview: mergedPlan, agentReview: mergedAgent, models: modelsRaw };
380
412
  }
381
413
 
382
414
  function loadAgentLibrary(
383
415
  projDir: string,
384
- settings?: Record<string, any>,
416
+ settings?: Record<string, unknown>,
385
417
  ): AgentConfig[] {
386
418
  const agentsData = aggregateAgents(path.join(projDir, "_cc-native", "agents"));
387
419
  const defaultModel = settings?.agentDefaults?.model ?? DEFAULT_AGENT_MODEL;
@@ -391,6 +423,7 @@ function loadAgentLibrary(
391
423
  return DEFAULT_AGENTS.map(a => ({
392
424
  name: a.name,
393
425
  model: a.model ?? defaultModel,
426
+ provider: a.provider ?? "claude",
394
427
  focus: a.focus ?? "general review",
395
428
  enabled: a.enabled ?? true,
396
429
  categories: a.categories ?? ["code"],
@@ -444,8 +477,23 @@ async function main(): Promise<void> {
444
477
  return;
445
478
  }
446
479
 
447
- // Find and read plan
448
- const planPath = findPlanFile();
480
+ // Find plan file: prefer transcript-based discovery (session-accurate), fall back to mtime scan
481
+ const transcriptPath = payload.transcript_path as string | undefined;
482
+ let planPath: string | null = null;
483
+
484
+ if (transcriptPath) {
485
+ planPath = findPlanPathInTranscript(transcriptPath);
486
+ if (planPath) {
487
+ logInfo(HOOK, `Found plan via transcript: ${planPath}`);
488
+ } else {
489
+ logDebug(HOOK, "No plan Write found in transcript, falling back to mtime scan");
490
+ }
491
+ }
492
+
493
+ if (!planPath) {
494
+ planPath = findPlanFile();
495
+ }
496
+
449
497
  if (!planPath) {
450
498
  skipWithInfo("No plan file found in ~/.claude/plans/. The plan may not have been written yet.");
451
499
  return;
@@ -501,10 +549,24 @@ async function main(): Promise<void> {
501
549
  }
502
550
  }
503
551
 
552
+ // Single load of iteration state — reused throughout, saved once at end.
553
+ // Default max=1 is safe: first iteration 1>1=false (runs), Edit E updates max from config before save.
554
+ let iterationState: IterationState = loadIterationState(reviewsDir) ?? {
555
+ current: 1, max: 1, complexity: "medium",
556
+ history: [], graduated: [], passStreaks: {}, lastPlanHash: "",
557
+ };
558
+
559
+ // Reset iteration counter when plan content changes (BEFORE early exit check)
560
+ // Graduation state (graduated[], passStreaks{}) persists across plan changes.
561
+ const lastHash = iterationState.lastPlanHash ?? "";
562
+ if (lastHash && lastHash !== planHash) {
563
+ logInfo(HOOK, `Plan hash changed (${lastHash.slice(0, 8)}→${planHash.slice(0, 8)}), resetting iteration counter`);
564
+ iterationState.current = 1;
565
+ }
566
+
504
567
  // 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.`);
568
+ if (iterationState.current > iterationState.max) {
569
+ skipWithInfo(`Max review iterations reached (${iterationState.current - 1}/${iterationState.max}), allowing plan through.`);
508
570
  return;
509
571
  }
510
572
 
@@ -513,18 +575,18 @@ async function main(): Promise<void> {
513
575
  let orchResult: OrchestratorResult | null = null;
514
576
  const agentResults: Record<string, ReviewerResult> = {};
515
577
  let allVerdicts: Verdict[] = [];
516
- let iterationState: IterationState | null = null;
517
578
  let detectedComplexity = "medium";
518
579
 
519
580
  // ============================================
520
581
  // PHASE 1 & 2: CLI Reviewers + Orchestrator (PARALLEL)
521
582
  // ============================================
522
583
  const reviewersConfig = planReviewEnabled ? (planSettings.reviewers ?? {}) : {};
523
- const codexEnabled = planReviewEnabled && (reviewersConfig.codex?.enabled ?? true);
584
+ // Deprecated: agents now support Codex provider via models.providers.codex
585
+ const codexEnabled = planReviewEnabled && (reviewersConfig.codex?.enabled ?? false);
524
586
  const geminiEnabled = planReviewEnabled && (reviewersConfig.gemini?.enabled ?? false);
525
587
 
526
- // Load graduated agents from previous iterations (empty on iteration 1)
527
- const graduatedSet = loadGraduatedSet(reviewsDir);
588
+ // Graduated agents from previous iterations (empty after hash reset or on iteration 1)
589
+ const graduatedSet = new Set(iterationState.graduated);
528
590
  if (graduatedSet.size > 0) {
529
591
  logInfo(HOOK, `Graduated agents from previous iterations: ${[...graduatedSet].sort().join(", ")}`);
530
592
  }
@@ -606,7 +668,7 @@ async function main(): Promise<void> {
606
668
  logInfo(HOOK, "=== PHASE 2: Agent Selection ===");
607
669
 
608
670
  let selectedAgents: AgentConfig[] = [];
609
- const fallbackByComplexity = agentSettings.fallbackByComplexity ?? { simple: 0, medium: 5, high: 9 };
671
+ const fallbackByComplexity = agentSettings.fallbackByComplexity ?? { simple: 0, medium: 2, high: 4 };
610
672
 
611
673
  if (enabledAgents.length > 0) {
612
674
  let mandatoryAgents = enabledAgents.filter(a => mandatoryNames.has(a.name));
@@ -671,11 +733,19 @@ async function main(): Promise<void> {
671
733
  },
672
734
  });
673
735
 
674
- // Initialize iteration state
675
- if (reviewsDir) {
676
- iterationState = getIterationStateFromContext(reviewsDir, detectedComplexity, agentSettings);
677
- logDebug(HOOK, `Iteration state: ${iterationState.current}/${iterationState.max} (${detectedComplexity})`);
678
- }
736
+ // Update complexity/max on the already-loaded iteration state (no second disk read)
737
+ const reviewIterations: Record<string, number> = {
738
+ ...DEFAULT_REVIEW_ITERATIONS,
739
+ ...(agentSettings.reviewIterations ?? {}),
740
+ };
741
+ iterationState.complexity = detectedComplexity;
742
+ iterationState.max = reviewIterations[detectedComplexity] ?? iterationState.max;
743
+ logDebug(HOOK, `Iteration state: ${iterationState.current}/${iterationState.max} (${detectedComplexity})`);
744
+
745
+ // Assign random providers + models to selected agents
746
+ const modelsConfig = loadModelsConfig(settings);
747
+ selectedAgents = assignModelsToAgents(selectedAgents, modelsConfig);
748
+ logInfo(HOOK, `Model assignments: ${selectedAgents.map(a => `${a.name}→${a.provider}:${a.model}`).join(", ")}`);
679
749
 
680
750
  // PHASE 3: Run selected agents in parallel
681
751
  if (selectedAgents.length > 0) {
@@ -716,11 +786,28 @@ async function main(): Promise<void> {
716
786
  }
717
787
 
718
788
  // ============================================
719
- // Persist newly graduated agents (before verdict overrides)
789
+ // Enforce per-agent issue limit (truncate to top N by severity)
720
790
  // ============================================
721
- const newlyGraduated = computeGraduated(agentResults);
722
- if (newlyGraduated.length > 0) {
723
- logInfo(HOOK, `Newly graduated agents: ${newlyGraduated.join(", ")}`);
791
+ const maxIssuesPerAgent = typeof agentSettings.maxIssuesPerAgent === "number"
792
+ ? agentSettings.maxIssuesPerAgent : 3;
793
+
794
+ for (const r of [...Object.values(cliResults), ...Object.values(agentResults)]) {
795
+ if (!Array.isArray(r.data?.issues)) continue;
796
+ const issues = r.data.issues as Array<{ severity?: string }>;
797
+ if (issues.length <= maxIssuesPerAgent) continue;
798
+ const severityOrder: Record<string, number> = { high: 0, medium: 1, low: 2 };
799
+ issues.sort((a, b) => (severityOrder[a.severity ?? "low"] ?? 2) - (severityOrder[b.severity ?? "low"] ?? 2));
800
+ const originalCount = issues.length;
801
+ r.data.issues = issues.slice(0, maxIssuesPerAgent);
802
+ logInfo(HOOK, `${r.name}: truncated issues ${originalCount} → ${maxIssuesPerAgent}`);
803
+ }
804
+
805
+ // ============================================
806
+ // Compute pass-eligible agents (before verdict overrides)
807
+ // ============================================
808
+ const passEligible = computePassEligible(agentResults);
809
+ if (passEligible.length > 0) {
810
+ logInfo(HOOK, `Pass-eligible agents this iteration: ${passEligible.join(", ")}`);
724
811
  }
725
812
 
726
813
  // ============================================
@@ -774,13 +861,18 @@ async function main(): Promise<void> {
774
861
  const combinedSettings = { display: displaySettings };
775
862
 
776
863
  // Get current iteration number
777
- const currentIteration = iterationState?.current ?? 1;
864
+ const currentIteration = iterationState.current;
778
865
 
779
866
  // Create review folder
780
867
  const reviewFolder = getReviewFolderPath(contextId, currentIteration, base);
781
868
  fs.mkdirSync(reviewFolder, { recursive: true });
782
869
  logInfo(HOOK, `Created review folder: ${reviewFolder}`);
783
870
 
871
+ // Review decision — corroboration-based (proportional threshold per dimension)
872
+ // Must be computed before writeCombinedArtifacts and buildInlineReviewSummary which consume it.
873
+ const allReviewerResults: Record<string, ReviewerResult> = { ...cliResults, ...agentResults };
874
+ const corroborationResult = computeCorroboratedDecision(allReviewerResults);
875
+
784
876
  const reviewFile = writeCombinedArtifacts(
785
877
  base,
786
878
  plan,
@@ -790,6 +882,7 @@ async function main(): Promise<void> {
790
882
  undefined,
791
883
  reviewFolder,
792
884
  currentIteration,
885
+ corroborationResult,
793
886
  );
794
887
  logInfo(HOOK, `Saved review: ${reviewFile}`);
795
888
 
@@ -802,16 +895,16 @@ async function main(): Promise<void> {
802
895
  }
803
896
 
804
897
  // Build inline summary with top issues (always emitted, even on pass)
805
- const inlineSummary = buildInlineReviewSummary(combinedResult);
898
+ const inlineSummary = buildInlineReviewSummary(combinedResult, 5, 800, corroborationResult);
806
899
  const topIssuesList = extractTopIssuesForTracker(combinedResult, 5);
807
900
  const contextParts = [inlineSummary];
808
901
  if (topIssuesList.length > 0) {
809
902
  contextParts.push(`\nTop high-severity issues:\n${topIssuesList.map(i => `- ${i}`).join("\n")}`);
810
903
  }
811
904
  contextParts.push(`\nFull review: \`${reviewFile}\`\n`);
812
-
813
- // Review decision
814
- const { should_deny: shouldDeny, reason: denyReason, score: reviewScore } = computeReviewDecision(allVerdicts);
905
+ const shouldDeny = corroborationResult.blocking.length > 0;
906
+ const denyReason = shouldDeny ? "corroborated_issues" : "no_corroboration";
907
+ const reviewScore = shouldDeny ? 1.0 : 0.0;
815
908
 
816
909
  logInfo(HOOK, `REVIEW_DECISION: verdict=${combinedResult.overall_verdict}, deny=${shouldDeny}, reason=${denyReason}, score=${reviewScore.toFixed(2)}`);
817
910
  logDiagnostic(HOOK, "result", `verdict=${combinedResult.overall_verdict}, deny=${shouldDeny}, reason=${denyReason}`, {
@@ -833,34 +926,51 @@ async function main(): Promise<void> {
833
926
  }
834
927
 
835
928
  // 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) {
929
+ // - On PASS/WARRANT: set current past max so no more reviews happen
930
+ // - On DENY (fail/warn): increment current toward max (safety valve)
931
+ // - Max iterations (high=5, medium=3, simple=1) caps total reviews before auto-allow
932
+ if (reviewsDir) {
841
933
  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
- }
934
+ iterationState.lastPlanHash = planHash;
847
935
 
848
936
  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`);
937
+ // Pass/warrant: stop iterating set current past max
938
+ iterationState.current = iterationState.max + 1;
939
+ logInfo(HOOK, `Pass/warrant: stopping iterations`);
940
+ } else {
941
+ // Deny: advance iteration counter toward max so safety valve triggers
942
+ iterationState.current += 1;
943
+ logInfo(HOOK, `Deny: advancing iteration (${iterationState.current}/${iterationState.max})`);
852
944
  }
853
945
 
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];
946
+ // Update pass streaks — only for agents that actually ran this iteration
947
+ const passStreaks = { ...(iterationState.passStreaks ?? {}) };
948
+ const passEligibleSet = new Set(passEligible);
949
+ const graduatedSetCurrent = new Set(iterationState.graduated);
950
+
951
+ for (const name of Object.keys(agentResults)) {
952
+ if (graduatedSetCurrent.has(name)) continue;
953
+ if (passEligibleSet.has(name)) {
954
+ passStreaks[name] = (passStreaks[name] ?? 0) + 1;
955
+ } else {
956
+ passStreaks[name] = 0;
957
+ }
958
+ }
959
+ iterationState.passStreaks = passStreaks;
960
+
961
+ // Graduate agents that reached threshold
962
+ const GRADUATION_THRESHOLD = 2;
963
+ const newGrads: string[] = [];
964
+ for (const [name, streak] of Object.entries(passStreaks)) {
965
+ if (streak >= GRADUATION_THRESHOLD && !graduatedSetCurrent.has(name)) {
966
+ newGrads.push(name);
967
+ }
968
+ }
969
+ if (newGrads.length > 0) {
970
+ iterationState.graduated = [...iterationState.graduated, ...newGrads];
971
+ logInfo(HOOK, `Newly graduated (${GRADUATION_THRESHOLD} consecutive passes): ${newGrads.join(", ")}`);
861
972
  }
862
973
 
863
- iterationState.current += 1;
864
974
  saveIterationState(reviewsDir, iterationState);
865
975
  }
866
976
 
@@ -874,7 +984,7 @@ async function main(): Promise<void> {
874
984
  verdict: combinedResult.overall_verdict,
875
985
  decision: trackerDecision,
876
986
  score: reviewScore,
877
- topIssues: extractTopIssuesForTracker(combinedResult, 5),
987
+ topIssues: topIssuesList,
878
988
  reviewFolder,
879
989
  };
880
990
  writeReviewTracker(ccNativeReviewsDir, trackerEntry);
@@ -889,18 +999,14 @@ async function main(): Promise<void> {
889
999
  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
1000
 
891
1001
  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);
1002
+ const disposition = `hook_deny_iter_${iterationState.current - 1}`;
1003
+ markPlanReviewed(sessionId, planHash, base, HOOK, iterationState, disposition);
896
1004
  const topIssuesText = extractTopIssuesText(combinedResult, 3, "high");
897
- const highIssuesDoc = buildHighIssuesDocument(combinedResult);
1005
+ const highIssuesDoc = buildHighIssuesDocument(combinedResult, corroborationResult);
898
1006
  const highIssuesPath = path.join(reviewFolder, "high-issues.md");
899
1007
  fs.writeFileSync(highIssuesPath, highIssuesDoc, "utf-8");
900
1008
 
901
- const iterInfo = iterationState
902
- ? ` (iteration ${iterationState.current - 1}/${iterationState.max}, score=${reviewScore.toFixed(2)})`
903
- : ` (score=${reviewScore.toFixed(2)})`;
1009
+ const iterInfo = ` (iteration ${iterationState.current - 1}/${iterationState.max}, score=${reviewScore.toFixed(2)})`;
904
1010
 
905
1011
  emitContextAndBlock(
906
1012
  contextText,
@@ -913,7 +1019,7 @@ async function main(): Promise<void> {
913
1019
  RESUBMIT_INSTRUCTION,
914
1020
  );
915
1021
  } else {
916
- markPlanReviewed(sessionId, planHash, base, HOOK, iterationState ?? undefined, "allow");
1022
+ markPlanReviewed(sessionId, planHash, base, HOOK, iterationState, "allow");
917
1023
  emitContext(contextText);
918
1024
  }
919
1025
  }