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.
- package/dist/templates/_shared/hooks-ts/session_end.ts +4 -1
- package/dist/templates/_shared/lib-ts/base/git-state.ts +1 -1
- package/dist/templates/_shared/lib-ts/base/logger.ts +15 -18
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +18 -15
- package/dist/templates/_shared/lib-ts/context/plan-manager.ts +26 -30
- package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +12 -13
- package/dist/templates/_shared/scripts/resume_handoff.ts +62 -38
- package/dist/templates/_shared/scripts/save_handoff.ts +24 -24
- package/dist/templates/_shared/scripts/status_line.ts +101 -147
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +239 -133
- package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +11 -12
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +139 -56
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +22 -2
- package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +115 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +1 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +7 -1
- package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +5 -4
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +133 -13
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +6 -6
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +5 -4
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +30 -33
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +118 -43
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +21 -0
- package/oclif.manifest.json +1 -1
- 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
|
|
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
|
|
144
|
-
*
|
|
145
|
-
* Agents with "skip"/"error"
|
|
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
|
|
148
|
-
const
|
|
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") {
|
|
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
|
-
|
|
160
|
+
eligible.push(name);
|
|
156
161
|
}
|
|
157
162
|
}
|
|
158
|
-
return
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
{ name: "
|
|
181
|
-
{ name: "
|
|
182
|
-
{
|
|
183
|
-
{ name: "
|
|
184
|
-
{ name: "
|
|
185
|
-
{ name: "
|
|
186
|
-
{ name: "
|
|
187
|
-
{ name: "
|
|
188
|
-
{ name: "
|
|
189
|
-
{ name: "
|
|
190
|
-
{ name: "
|
|
191
|
-
{ name: "
|
|
192
|
-
{ name: "
|
|
193
|
-
{ name: "
|
|
194
|
-
{ name: "
|
|
195
|
-
{ name: "
|
|
196
|
-
{ name: "
|
|
197
|
-
{ name: "
|
|
198
|
-
{ name: "
|
|
199
|
-
{ name: "
|
|
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:
|
|
214
|
-
high: { min:
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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,
|
|
329
|
-
const defaults: Record<string,
|
|
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
|
-
|
|
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,
|
|
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
|
|
448
|
-
const
|
|
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
|
-
|
|
506
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
527
|
-
const graduatedSet =
|
|
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:
|
|
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
|
-
//
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|
-
//
|
|
789
|
+
// Enforce per-agent issue limit (truncate to top N by severity)
|
|
720
790
|
// ============================================
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
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
|
|
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
|
-
|
|
814
|
-
const
|
|
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
|
|
837
|
-
// - On
|
|
838
|
-
// -
|
|
839
|
-
|
|
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:
|
|
850
|
-
iterationState.current = iterationState.max;
|
|
851
|
-
logInfo(HOOK, `Pass:
|
|
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
|
-
//
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
1022
|
+
markPlanReviewed(sessionId, planHash, base, HOOK, iterationState, "allow");
|
|
917
1023
|
emitContext(contextText);
|
|
918
1024
|
}
|
|
919
1025
|
}
|