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.
- package/dist/commands/clear.d.ts +8 -0
- package/dist/commands/clear.js +86 -0
- package/dist/lib/bmad-installer.d.ts +2 -27
- package/dist/lib/bmad-installer.js +3 -43
- package/dist/lib/claude-settings-types.d.ts +2 -1
- package/dist/lib/env-compat.d.ts +0 -8
- package/dist/lib/env-compat.js +0 -12
- package/dist/lib/git/index.d.ts +0 -1
- package/dist/lib/gitignore-manager.d.ts +0 -2
- package/dist/lib/gitignore-manager.js +1 -1
- package/dist/lib/hooks-merger.d.ts +1 -15
- package/dist/lib/hooks-merger.js +1 -1
- package/dist/lib/index.d.ts +3 -7
- package/dist/lib/index.js +3 -11
- package/dist/lib/output.d.ts +2 -1
- package/dist/lib/settings-hierarchy.d.ts +1 -13
- package/dist/lib/settings-hierarchy.js +1 -1
- package/dist/lib/template-installer.d.ts +5 -9
- package/dist/lib/template-installer.js +2 -12
- package/dist/lib/template-linter.d.ts +3 -10
- package/dist/lib/template-linter.js +2 -2
- package/dist/lib/template-resolver.d.ts +6 -0
- package/dist/lib/template-resolver.js +10 -0
- package/dist/lib/template-settings-reconstructor.d.ts +1 -1
- package/dist/lib/template-settings-reconstructor.js +17 -24
- package/dist/lib/terminal.d.ts +3 -14
- package/dist/lib/terminal.js +0 -4
- package/dist/lib/version.d.ts +2 -11
- package/dist/lib/version.js +2 -2
- package/dist/lib/windsurf-hooks-merger.d.ts +1 -15
- package/dist/lib/windsurf-hooks-merger.js +1 -1
- package/dist/templates/_shared/.codex/workflows/handoff.md +1 -1
- package/dist/templates/_shared/.windsurf/workflows/handoff.md +1 -1
- package/dist/templates/_shared/hooks-ts/session_end.ts +4 -1
- package/dist/templates/_shared/hooks-ts/session_start.ts +15 -20
- package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +12 -14
- package/dist/templates/_shared/lib-ts/CLAUDE.md +56 -7
- package/dist/templates/_shared/lib-ts/base/git-state.ts +1 -1
- package/dist/templates/_shared/lib-ts/base/hook-utils.ts +174 -43
- package/dist/templates/_shared/lib-ts/base/logger.ts +15 -18
- package/dist/templates/_shared/lib-ts/base/state-io.ts +11 -2
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +181 -162
- 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/lib-ts/package.json +1 -2
- package/dist/templates/_shared/lib-ts/templates/plan-context.ts +27 -34
- package/dist/templates/_shared/lib-ts/types.ts +17 -2
- 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 +102 -148
- package/dist/templates/_shared/workflows/handoff.md +1 -1
- package/dist/templates/cc-native/.claude/settings.json +183 -175
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +23 -1
- package/dist/templates/cc-native/_cc-native/agents/plan-questions/PLAN-QUESTIONER.md +70 -0
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +6 -1
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +316 -176
- package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_subagent.ts +38 -0
- package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_write.ts +51 -0
- package/dist/templates/cc-native/_cc-native/hooks/mark_questions_asked.ts +53 -0
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +15 -15
- 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 +227 -114
- package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +64 -16
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +23 -3
- package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +119 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +1 -4
- 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 +40 -218
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-enhancement.ts +41 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-questions.ts +101 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +27 -111
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/base/base-agent.ts +217 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +5 -3
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/claude-agent.ts +65 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/codex-agent.ts +184 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/gemini-agent.ts +39 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +195 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/schemas.ts +201 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +3 -5
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +30 -33
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +104 -132
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +22 -13
- package/oclif.manifest.json +1 -1
- package/package.json +2 -3
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +0 -119
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +0 -130
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +0 -106
- /package/dist/templates/cc-native/_cc-native/agents/{ARCH-EVOLUTION.md → plan-review/ARCH-EVOLUTION.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{ARCH-PATTERNS.md → plan-review/ARCH-PATTERNS.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{ARCH-STRUCTURE.md → plan-review/ARCH-STRUCTURE.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{ASSUMPTION-TRACER.md → plan-review/ASSUMPTION-TRACER.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{CLARITY-AUDITOR.md → plan-review/CLARITY-AUDITOR.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{COMPLETENESS-FEASIBILITY.md → plan-review/COMPLETENESS-FEASIBILITY.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{COMPLETENESS-GAPS.md → plan-review/COMPLETENESS-GAPS.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{COMPLETENESS-ORDERING.md → plan-review/COMPLETENESS-ORDERING.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{CONSTRAINT-VALIDATOR.md → plan-review/CONSTRAINT-VALIDATOR.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{DESIGN-ADR-VALIDATOR.md → plan-review/DESIGN-ADR-VALIDATOR.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{DESIGN-SCALE-MATCHER.md → plan-review/DESIGN-SCALE-MATCHER.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{DEVILS-ADVOCATE.md → plan-review/DEVILS-ADVOCATE.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{DOCUMENTATION-PHILOSOPHY.md → plan-review/DOCUMENTATION-PHILOSOPHY.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{HANDOFF-READINESS.md → plan-review/HANDOFF-READINESS.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{HIDDEN-COMPLEXITY.md → plan-review/HIDDEN-COMPLEXITY.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{INCREMENTAL-DELIVERY.md → plan-review/INCREMENTAL-DELIVERY.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{RISK-DEPENDENCY.md → plan-review/RISK-DEPENDENCY.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{RISK-FMEA.md → plan-review/RISK-FMEA.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{RISK-PREMORTEM.md → plan-review/RISK-PREMORTEM.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{RISK-REVERSIBILITY.md → plan-review/RISK-REVERSIBILITY.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{SCOPE-BOUNDARY.md → plan-review/SCOPE-BOUNDARY.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{SIMPLICITY-GUARDIAN.md → plan-review/SIMPLICITY-GUARDIAN.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{SKEPTIC.md → plan-review/SKEPTIC.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-BEHAVIOR-AUDITOR.md → plan-review/TESTDRIVEN-BEHAVIOR-AUDITOR.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-CHARACTERIZATION.md → plan-review/TESTDRIVEN-CHARACTERIZATION.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-FIRST-VALIDATOR.md → plan-review/TESTDRIVEN-FIRST-VALIDATOR.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-PYRAMID-ANALYZER.md → plan-review/TESTDRIVEN-PYRAMID-ANALYZER.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{TRADEOFF-COSTS.md → plan-review/TRADEOFF-COSTS.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{TRADEOFF-STAKEHOLDERS.md → plan-review/TRADEOFF-STAKEHOLDERS.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{VERIFY-COVERAGE.md → plan-review/VERIFY-COVERAGE.md} +0 -0
- /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
|
|
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
|
|
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
|
|
144
|
-
*
|
|
145
|
-
* Agents with "skip"/"error"
|
|
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
|
|
148
|
-
const
|
|
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") {
|
|
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
|
-
|
|
162
|
+
eligible.push(name);
|
|
156
163
|
}
|
|
157
164
|
}
|
|
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 ?? []);
|
|
165
|
+
return eligible;
|
|
168
166
|
}
|
|
169
167
|
|
|
170
168
|
// ---------------------------------------------------------------------------
|
|
171
169
|
// Default Configuration
|
|
172
170
|
// ---------------------------------------------------------------------------
|
|
173
171
|
|
|
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: "
|
|
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:
|
|
214
|
-
high: { min:
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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,
|
|
329
|
-
const defaults: Record<string,
|
|
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
|
-
|
|
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,
|
|
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
|
|
448
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
506
|
-
|
|
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
|
|
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
|
-
//
|
|
527
|
-
const graduatedSet =
|
|
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
|
|
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:
|
|
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
|
-
//
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|
-
//
|
|
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
|
|
722
|
-
if (
|
|
723
|
-
logInfo(HOOK, `
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
814
|
-
const
|
|
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
|
|
837
|
-
// - On
|
|
838
|
-
// -
|
|
839
|
-
|
|
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:
|
|
850
|
-
iterationState.current = iterationState.max;
|
|
851
|
-
logInfo(HOOK, `Pass:
|
|
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
|
-
//
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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:
|
|
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
|
-
//
|
|
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
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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
|
|
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
|
-
|
|
917
|
-
|
|
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
|
|