aiwcli 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/dist/commands/clear.d.ts +8 -0
  2. package/dist/commands/clear.js +86 -0
  3. package/dist/lib/bmad-installer.d.ts +2 -27
  4. package/dist/lib/bmad-installer.js +3 -43
  5. package/dist/lib/claude-settings-types.d.ts +2 -1
  6. package/dist/lib/env-compat.d.ts +0 -8
  7. package/dist/lib/env-compat.js +0 -12
  8. package/dist/lib/git/index.d.ts +0 -1
  9. package/dist/lib/gitignore-manager.d.ts +0 -2
  10. package/dist/lib/gitignore-manager.js +1 -1
  11. package/dist/lib/hooks-merger.d.ts +1 -15
  12. package/dist/lib/hooks-merger.js +1 -1
  13. package/dist/lib/index.d.ts +3 -7
  14. package/dist/lib/index.js +3 -11
  15. package/dist/lib/output.d.ts +2 -1
  16. package/dist/lib/settings-hierarchy.d.ts +1 -13
  17. package/dist/lib/settings-hierarchy.js +1 -1
  18. package/dist/lib/template-installer.d.ts +5 -9
  19. package/dist/lib/template-installer.js +2 -12
  20. package/dist/lib/template-linter.d.ts +3 -10
  21. package/dist/lib/template-linter.js +2 -2
  22. package/dist/lib/template-resolver.d.ts +6 -0
  23. package/dist/lib/template-resolver.js +10 -0
  24. package/dist/lib/template-settings-reconstructor.d.ts +1 -1
  25. package/dist/lib/template-settings-reconstructor.js +17 -24
  26. package/dist/lib/terminal.d.ts +3 -14
  27. package/dist/lib/terminal.js +0 -4
  28. package/dist/lib/version.d.ts +2 -11
  29. package/dist/lib/version.js +2 -2
  30. package/dist/lib/windsurf-hooks-merger.d.ts +1 -15
  31. package/dist/lib/windsurf-hooks-merger.js +1 -1
  32. package/dist/templates/_shared/.codex/workflows/handoff.md +1 -1
  33. package/dist/templates/_shared/.windsurf/workflows/handoff.md +1 -1
  34. package/dist/templates/_shared/hooks-ts/session_start.ts +15 -20
  35. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +12 -14
  36. package/dist/templates/_shared/lib-ts/CLAUDE.md +56 -7
  37. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +174 -43
  38. package/dist/templates/_shared/lib-ts/base/state-io.ts +11 -2
  39. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +181 -165
  40. package/dist/templates/_shared/lib-ts/package.json +1 -2
  41. package/dist/templates/_shared/lib-ts/templates/plan-context.ts +27 -34
  42. package/dist/templates/_shared/lib-ts/types.ts +17 -2
  43. package/dist/templates/_shared/scripts/status_line.ts +1 -1
  44. package/dist/templates/_shared/workflows/handoff.md +1 -1
  45. package/dist/templates/cc-native/.claude/settings.json +183 -175
  46. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +23 -1
  47. package/dist/templates/cc-native/_cc-native/agents/plan-questions/PLAN-QUESTIONER.md +70 -0
  48. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +6 -1
  49. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +91 -57
  50. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_subagent.ts +38 -0
  51. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_write.ts +51 -0
  52. package/dist/templates/cc-native/_cc-native/hooks/mark_questions_asked.ts +53 -0
  53. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +15 -15
  54. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +95 -65
  55. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +64 -16
  56. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +1 -1
  57. package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +6 -2
  58. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +0 -4
  59. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +40 -219
  60. package/dist/templates/cc-native/_cc-native/lib-ts/plan-enhancement.ts +41 -0
  61. package/dist/templates/cc-native/_cc-native/lib-ts/plan-questions.ts +101 -0
  62. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +22 -226
  63. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/base/base-agent.ts +217 -0
  64. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +5 -3
  65. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/claude-agent.ts +65 -0
  66. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/codex-agent.ts +184 -0
  67. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/gemini-agent.ts +39 -0
  68. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +195 -0
  69. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/schemas.ts +201 -0
  70. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +3 -5
  71. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +4 -107
  72. package/dist/templates/cc-native/_cc-native/plan-review.config.json +2 -14
  73. package/oclif.manifest.json +1 -1
  74. package/package.json +1 -2
  75. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +0 -119
  76. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +0 -130
  77. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +0 -107
  78. /package/dist/templates/cc-native/_cc-native/agents/{ARCH-EVOLUTION.md → plan-review/ARCH-EVOLUTION.md} +0 -0
  79. /package/dist/templates/cc-native/_cc-native/agents/{ARCH-PATTERNS.md → plan-review/ARCH-PATTERNS.md} +0 -0
  80. /package/dist/templates/cc-native/_cc-native/agents/{ARCH-STRUCTURE.md → plan-review/ARCH-STRUCTURE.md} +0 -0
  81. /package/dist/templates/cc-native/_cc-native/agents/{ASSUMPTION-TRACER.md → plan-review/ASSUMPTION-TRACER.md} +0 -0
  82. /package/dist/templates/cc-native/_cc-native/agents/{CLARITY-AUDITOR.md → plan-review/CLARITY-AUDITOR.md} +0 -0
  83. /package/dist/templates/cc-native/_cc-native/agents/{COMPLETENESS-FEASIBILITY.md → plan-review/COMPLETENESS-FEASIBILITY.md} +0 -0
  84. /package/dist/templates/cc-native/_cc-native/agents/{COMPLETENESS-GAPS.md → plan-review/COMPLETENESS-GAPS.md} +0 -0
  85. /package/dist/templates/cc-native/_cc-native/agents/{COMPLETENESS-ORDERING.md → plan-review/COMPLETENESS-ORDERING.md} +0 -0
  86. /package/dist/templates/cc-native/_cc-native/agents/{CONSTRAINT-VALIDATOR.md → plan-review/CONSTRAINT-VALIDATOR.md} +0 -0
  87. /package/dist/templates/cc-native/_cc-native/agents/{DESIGN-ADR-VALIDATOR.md → plan-review/DESIGN-ADR-VALIDATOR.md} +0 -0
  88. /package/dist/templates/cc-native/_cc-native/agents/{DESIGN-SCALE-MATCHER.md → plan-review/DESIGN-SCALE-MATCHER.md} +0 -0
  89. /package/dist/templates/cc-native/_cc-native/agents/{DEVILS-ADVOCATE.md → plan-review/DEVILS-ADVOCATE.md} +0 -0
  90. /package/dist/templates/cc-native/_cc-native/agents/{DOCUMENTATION-PHILOSOPHY.md → plan-review/DOCUMENTATION-PHILOSOPHY.md} +0 -0
  91. /package/dist/templates/cc-native/_cc-native/agents/{HANDOFF-READINESS.md → plan-review/HANDOFF-READINESS.md} +0 -0
  92. /package/dist/templates/cc-native/_cc-native/agents/{HIDDEN-COMPLEXITY.md → plan-review/HIDDEN-COMPLEXITY.md} +0 -0
  93. /package/dist/templates/cc-native/_cc-native/agents/{INCREMENTAL-DELIVERY.md → plan-review/INCREMENTAL-DELIVERY.md} +0 -0
  94. /package/dist/templates/cc-native/_cc-native/agents/{RISK-DEPENDENCY.md → plan-review/RISK-DEPENDENCY.md} +0 -0
  95. /package/dist/templates/cc-native/_cc-native/agents/{RISK-FMEA.md → plan-review/RISK-FMEA.md} +0 -0
  96. /package/dist/templates/cc-native/_cc-native/agents/{RISK-PREMORTEM.md → plan-review/RISK-PREMORTEM.md} +0 -0
  97. /package/dist/templates/cc-native/_cc-native/agents/{RISK-REVERSIBILITY.md → plan-review/RISK-REVERSIBILITY.md} +0 -0
  98. /package/dist/templates/cc-native/_cc-native/agents/{SCOPE-BOUNDARY.md → plan-review/SCOPE-BOUNDARY.md} +0 -0
  99. /package/dist/templates/cc-native/_cc-native/agents/{SIMPLICITY-GUARDIAN.md → plan-review/SIMPLICITY-GUARDIAN.md} +0 -0
  100. /package/dist/templates/cc-native/_cc-native/agents/{SKEPTIC.md → plan-review/SKEPTIC.md} +0 -0
  101. /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-BEHAVIOR-AUDITOR.md → plan-review/TESTDRIVEN-BEHAVIOR-AUDITOR.md} +0 -0
  102. /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-CHARACTERIZATION.md → plan-review/TESTDRIVEN-CHARACTERIZATION.md} +0 -0
  103. /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-FIRST-VALIDATOR.md → plan-review/TESTDRIVEN-FIRST-VALIDATOR.md} +0 -0
  104. /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-PYRAMID-ANALYZER.md → plan-review/TESTDRIVEN-PYRAMID-ANALYZER.md} +0 -0
  105. /package/dist/templates/cc-native/_cc-native/agents/{TRADEOFF-COSTS.md → plan-review/TRADEOFF-COSTS.md} +0 -0
  106. /package/dist/templates/cc-native/_cc-native/agents/{TRADEOFF-STAKEHOLDERS.md → plan-review/TRADEOFF-STAKEHOLDERS.md} +0 -0
  107. /package/dist/templates/cc-native/_cc-native/agents/{VERIFY-COVERAGE.md → plan-review/VERIFY-COVERAGE.md} +0 -0
  108. /package/dist/templates/cc-native/_cc-native/agents/{VERIFY-STRENGTH.md → plan-review/VERIFY-STRENGTH.md} +0 -0
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Plan quality guidance for context emission.
3
+ *
4
+ * Provides prompt text that guides the main agent to review plans before
5
+ * presenting them to the user. Emitted via emitContext() — NOT appended to plan files.
6
+ *
7
+ * Used by both SubagentStop hook (Plan agents) and PostToolUse:Write hook (direct writes).
8
+ */
9
+
10
+ /**
11
+ * Returns the plan quality review prompt to emit as context after a plan is written.
12
+ * This guides the main agent to review the plan before calling ExitPlanMode.
13
+ *
14
+ * Design principles:
15
+ * - No hardcoded skill names — agent discovers relevant skills from system-reminders
16
+ * - Documentation focuses on WHY (preserve decisions) not WHERE (file paths)
17
+ * - Concise — every token in emitted context costs attention budget
18
+ * - Trusts the agent's judgment — guidance, not mandate
19
+ */
20
+ export function getPlanQualityReviewContext(): string {
21
+ return `## Plan Quality Review
22
+
23
+ Before presenting this plan, review it from the perspective of an agent with zero conversation history.
24
+
25
+ ### Self-Check
26
+ - File paths are absolute and verified (not "the auth file" or "as discussed")
27
+ - Function and class names are exact references (not "the handler" or "it")
28
+ - Each step is specific enough to execute without this conversation's context
29
+ - Verification steps are binary-testable (pass/fail in one check)
30
+
31
+ ### Skills Integration
32
+ Review the skills listed in your system-reminder messages. Where a step would benefit from a specific skill, reference it inline (e.g., "Use \`SkillName\` skill for [specific purpose]"). Only reference skills relevant to this plan's domain.
33
+
34
+ ### Documentation Reasoning
35
+ Evaluate whether the plan captures decisions that would be lost when this session ends. The implementation agent should understand:
36
+ - What was decided and why alternatives were rejected
37
+ - What constraints exist that aren't obvious from the code
38
+ - What would break if assumptions change
39
+
40
+ If the plan has gaps, address them before presenting to the user.`;
41
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Plan question generation — runs a fresh-context agent to identify
3
+ * questions, assumptions, and ambiguities in a plan before review.
4
+ * See cc-native-plan-review.ts for integration point (questions gate).
5
+ */
6
+
7
+ import * as path from "node:path";
8
+ import { logInfo, logWarn, logError } from "../../_shared/lib-ts/base/logger.js";
9
+ import { aggregateAgents } from "./aggregate-agents.js";
10
+ import { runAgentReview } from "./reviewers/index.js";
11
+ import { QUESTIONS_SCHEMA } from "./reviewers/schemas.js";
12
+ import type { AgentConfig, ModelsConfig } from "./types.js";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Types
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export interface PlanQuestionsResult {
19
+ questions: string[];
20
+ assumptions: string[];
21
+ ambiguities: string[];
22
+ }
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Provider assignment (local copy — avoids circular import from hook)
26
+ // ---------------------------------------------------------------------------
27
+
28
+ import { findExecutable } from "../../_shared/lib-ts/base/subprocess-utils.js";
29
+
30
+ function assignProvider(agent: AgentConfig): AgentConfig {
31
+ // Default to claude provider with the agent's configured model
32
+ const found = findExecutable("claude");
33
+ if (found) {
34
+ return { ...agent, provider: "claude" };
35
+ }
36
+ logWarn("plan-questions", "Claude CLI not found, using agent defaults");
37
+ return agent;
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Main
42
+ // ---------------------------------------------------------------------------
43
+
44
+ const HOOK = "plan-questions";
45
+
46
+ /**
47
+ * Run the plan-questions agent to generate questions about a plan.
48
+ * Returns structured questions/assumptions/ambiguities, or null on failure.
49
+ *
50
+ * The agent runs in a fresh context (no codebase, no session history)
51
+ * and uses QUESTIONS_SCHEMA instead of REVIEW_SCHEMA — the agent runner
52
+ * is schema-agnostic.
53
+ */
54
+ export async function runPlanQuestions(
55
+ plan: string,
56
+ aiwcliDir: string,
57
+ timeout: number,
58
+ contextPath?: string,
59
+ sessionName?: string,
60
+ ): Promise<PlanQuestionsResult | null> {
61
+ // Load the plan-questions agent from agents/plan-questions/
62
+ const questionsAgentDir = path.join(aiwcliDir, "_cc-native", "agents", "plan-questions");
63
+ const agents = aggregateAgents(questionsAgentDir);
64
+
65
+ if (agents.length === 0) {
66
+ logWarn(HOOK, `No agents found in ${questionsAgentDir}`);
67
+ return null;
68
+ }
69
+
70
+ // Use the first agent (PLAN-QUESTIONER)
71
+ let agent = agents[0]!;
72
+ logInfo(HOOK, `Using plan-questions agent: ${agent.name}`);
73
+
74
+ // Assign provider
75
+ agent = assignProvider(agent);
76
+
77
+ // Run the agent with QUESTIONS_SCHEMA
78
+ const result = await runAgentReview(
79
+ plan,
80
+ agent,
81
+ QUESTIONS_SCHEMA,
82
+ timeout,
83
+ contextPath,
84
+ sessionName ?? "unknown",
85
+ );
86
+
87
+ if (!result.ok) {
88
+ logError(HOOK, `Plan-questions agent failed: ${result.err}`);
89
+ return null;
90
+ }
91
+
92
+ // Extract structured data
93
+ const data = result.data ?? {};
94
+ const questions = Array.isArray(data.questions) ? (data.questions as string[]) : [];
95
+ const assumptions = Array.isArray(data.assumptions) ? (data.assumptions as string[]) : [];
96
+ const ambiguities = Array.isArray(data.ambiguities) ? (data.ambiguities as string[]) : [];
97
+
98
+ logInfo(HOOK, `Plan-questions result: ${questions.length} questions, ${assumptions.length} assumptions, ${ambiguities.length} ambiguities`);
99
+
100
+ return { questions, assumptions, ambiguities };
101
+ }
@@ -1,21 +1,16 @@
1
1
  /**
2
2
  * Agent-based plan reviewer with multi-provider support.
3
- * Routes to Claude or Codex CLI based on agent.provider field.
3
+ * Routes to provider-specific implementations (Claude, Codex, Gemini).
4
4
  * See cc-native-plan-review-spec.md §4.10
5
5
  */
6
6
 
7
- import * as fs from "node:fs";
8
- import * as os from "node:os";
9
- import * as path from "node:path";
10
- import { logDebug, logInfo, logWarn, logError } from "../../../_shared/lib-ts/base/logger.js";
11
- import { getInternalSubprocessEnv, findExecutable, execFileAsync } from "../../../_shared/lib-ts/base/subprocess-utils.js";
12
- import { parseCliOutput } from "../cli-output-parser.js";
13
- import { parseJsonMaybe, coerceToReview } from "../json-parser.js";
14
- import { debugLog, debugRaw } from "../debug.js";
7
+ import { logWarn } from "../../../_shared/lib-ts/base/logger.js";
15
8
  import type { AgentConfig, ReviewerResult, ReviewOptions } from "../types.js";
16
- import { AGENT_REVIEW_PROMPT_PREFIX } from "../types.js";
17
9
  import { makeResult } from "./types.js";
18
10
  import type { Reviewer } from "./types.js";
11
+ import { ClaudeAgent } from "./providers/claude-agent.js";
12
+ import { CodexAgent } from "./providers/codex-agent.js";
13
+ import { GeminiAgent } from "./providers/gemini-agent.js";
19
14
 
20
15
  /**
21
16
  * Agent reviewer — runs a CLI instance with a custom persona.
@@ -52,224 +47,25 @@ export async function runAgentReview(
52
47
  contextPath?: string,
53
48
  sessionName = "unknown",
54
49
  ): Promise<ReviewerResult> {
55
- if (agent.provider === "codex") {
56
- return runAgentReviewCodex(plan, agent, schema, timeout, contextPath, sessionName);
57
- }
58
- // Default: Claude (existing implementation)
59
- return runAgentReviewClaude(plan, agent, schema, timeout, contextPath, sessionName);
60
- }
61
-
62
- /**
63
- * Run a single Claude Code agent to review the plan.
64
- */
65
- async function runAgentReviewClaude(
66
- plan: string,
67
- agent: AgentConfig,
68
- schema: Record<string, unknown>,
69
- timeout: number,
70
- contextPath?: string,
71
- sessionName = "unknown",
72
- ): Promise<ReviewerResult> {
73
- const claudePath = findExecutable("claude");
74
- if (!claudePath) {
75
- logWarn(agent.name, "Claude CLI not found on PATH");
76
- return makeResult(agent.name, false, "skip", {}, "", "claude CLI not found on PATH");
77
- }
78
-
79
- logDebug(agent.name, `Found Claude CLI at: ${claudePath}`);
80
-
81
- const prompt = `IMMEDIATELY call StructuredOutput with your review of the plan below.
82
- Do NOT output any text before calling StructuredOutput.
83
-
84
- PLAN:
85
- <<<
86
- ${plan}
87
- >>>
88
- `;
89
-
90
- const schemaJson = JSON.stringify(schema);
91
- const cmdArgs = [
92
- "--model", agent.model,
93
- "--output-format", "json",
94
- "--json-schema", schemaJson,
95
- "--max-turns", "3",
96
- "--setting-sources", "",
97
- "-p",
98
- ];
99
-
100
- if (agent.system_prompt) {
101
- const fullPrompt = AGENT_REVIEW_PROMPT_PREFIX + "\n\n---\n\n" + agent.system_prompt;
102
- cmdArgs.push("--system-prompt", fullPrompt);
103
- }
104
-
105
- logInfo(agent.name, `Running Claude with model: ${agent.model}, timeout: ${timeout}s`);
106
-
107
- const env = getInternalSubprocessEnv();
108
-
109
- const result = await execFileAsync(claudePath, cmdArgs, {
110
- input: prompt,
111
- timeout: timeout * 1000,
112
- env: env as Record<string, string>,
113
- maxBuffer: 10 * 1024 * 1024,
114
- shell: process.platform === "win32",
115
- });
116
-
117
- if (result.killed || result.signal === "SIGTERM") {
118
- logWarn(agent.name, `Claude TIMEOUT after ${timeout}s`);
119
- return makeResult(agent.name, false, "error", {}, "", `${agent.name} timed out after ${timeout}s`);
120
- }
121
-
122
- const raw = result.stdout.trim();
123
- const err = result.stderr.trim();
124
-
125
- if (!raw && !err && result.exitCode !== 0) {
126
- logError(agent.name, `Process exited with code ${result.exitCode} and no output`);
127
- return makeResult(agent.name, false, "error", {}, "", `${agent.name} failed to run (exit ${result.exitCode})`);
128
- }
129
-
130
- logDebug(agent.name, `Exit code: ${result.exitCode}`);
131
- logDebug(agent.name, `stdout length: ${raw.length} chars`);
132
- if (err) logDebug(agent.name, `stderr: ${err.slice(0, 500)}`);
133
-
134
- // Debug logging
135
- if (contextPath) {
136
- debugRaw(contextPath, sessionName, `agent:${agent.name}`, "stdout", raw);
137
- if (err) {
138
- debugRaw(contextPath, sessionName, `agent:${agent.name}`, "stderr", err);
139
- }
140
- debugLog(contextPath, sessionName, `agent:${agent.name}`, "subprocess_info", {
141
- exit_code: result.exitCode,
142
- stdout_len: raw.length,
143
- stderr_len: err.length,
144
- model: agent.model,
145
- provider: "claude",
146
- timeout,
147
- });
148
- }
149
-
150
- if (raw) logDebug(agent.name, `stdout preview: ${raw.slice(0, 500)}`);
151
-
152
- const obj = parseCliOutput(raw, ["verdict", "summary"]);
153
-
154
- if (contextPath) {
155
- debugLog(contextPath, sessionName, `agent:${agent.name}`, "parsed_result", {
156
- parsed_keys: obj ? Object.keys(obj) : null,
157
- verdict: obj?.verdict ?? null,
158
- has_summary: obj ? Boolean(obj.summary) : false,
159
- issues_count: obj && Array.isArray(obj.issues) ? (obj.issues as unknown[]).length : 0,
160
- });
161
- }
162
-
163
- if (obj) {
164
- logInfo(agent.name, `Parsed JSON successfully, verdict: ${obj.verdict ?? "N/A"}`);
165
- } else {
166
- logWarn(agent.name, "Failed to parse JSON from output");
167
- }
168
-
169
- const [ok, verdict, norm] = coerceToReview(
170
- obj as Record<string, unknown> | null,
171
- "Retry or check agent configuration.",
172
- );
173
-
174
- return makeResult(agent.name, ok, verdict, norm, raw, err);
175
- }
176
-
177
- /**
178
- * Run a single Codex CLI agent to review the plan.
179
- * Adapts the codex.ts reviewer pattern for agent-based review with persona.
180
- */
181
- async function runAgentReviewCodex(
182
- plan: string,
183
- agent: AgentConfig,
184
- schema: Record<string, unknown>,
185
- timeout: number,
186
- contextPath?: string,
187
- sessionName = "unknown",
188
- ): Promise<ReviewerResult> {
189
- const codexPath = findExecutable("codex");
190
- if (!codexPath) {
191
- logWarn(agent.name, "Codex CLI not found on PATH, skipping");
192
- return makeResult(agent.name, false, "skip", {}, "", "codex CLI not found on PATH");
193
- }
194
-
195
- // Codex has no --system-prompt flag, so we prepend the agent persona to stdin.
196
- const fullPrompt = [
197
- AGENT_REVIEW_PROMPT_PREFIX,
198
- "---",
199
- agent.system_prompt || "",
200
- "---",
201
- `Return ONLY a JSON object matching this schema:\n${JSON.stringify(schema)}`,
202
- "",
203
- "PLAN:",
204
- "<<<",
205
- plan,
206
- ">>>",
207
- ].join("\n\n");
208
-
209
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `codex-agent-${agent.name}-`));
210
-
211
50
  try {
212
- const schemaPath = path.join(tmpDir, "schema.json");
213
- const outPath = path.join(tmpDir, "output.json");
214
- fs.writeFileSync(schemaPath, JSON.stringify(schema, null, 2), "utf-8");
215
-
216
- const cmdArgs = ["exec", "--sandbox", "read-only"];
217
- if (agent.model) cmdArgs.push("--model", agent.model);
218
- cmdArgs.push("--output-schema", schemaPath, "-o", outPath, "-");
219
-
220
- logInfo(agent.name, `Running Codex with model: ${agent.model}, timeout: ${timeout}s`);
221
-
222
- const result = await execFileAsync(codexPath, cmdArgs, {
223
- input: fullPrompt,
224
- timeout: timeout * 1000,
225
- maxBuffer: 10 * 1024 * 1024,
226
- shell: process.platform === "win32",
227
- });
228
-
229
- if (result.killed || result.signal === "SIGTERM") {
230
- logWarn(agent.name, `Codex TIMEOUT after ${timeout}s`);
231
- return makeResult(agent.name, false, "error", {}, "", `${agent.name} (codex) timed out after ${timeout}s`);
232
- }
233
-
234
- if (!result.stdout && !result.stderr && !fs.existsSync(outPath) && result.exitCode !== 0) {
235
- logError(agent.name, `Codex exited with code ${result.exitCode} and no output`);
236
- return makeResult(agent.name, false, "error", {}, "", `${agent.name} (codex) failed (exit ${result.exitCode})`);
237
- }
238
-
239
- // Read output: prefer temp file, fallback to stdout
240
- let raw = "";
241
- if (fs.existsSync(outPath)) {
242
- raw = fs.readFileSync(outPath, "utf-8");
243
- }
244
- const err = result.stderr.trim();
245
-
246
- // Debug logging
247
- if (contextPath) {
248
- debugRaw(contextPath, sessionName, `agent:${agent.name}`, "stdout", raw || result.stdout);
249
- if (err) debugRaw(contextPath, sessionName, `agent:${agent.name}`, "stderr", err);
250
- debugLog(contextPath, sessionName, `agent:${agent.name}`, "subprocess_info", {
251
- exit_code: result.exitCode,
252
- stdout_len: (raw || result.stdout).length,
253
- stderr_len: err.length,
254
- model: agent.model,
255
- provider: "codex",
256
- timeout,
257
- });
51
+ let reviewer;
52
+
53
+ switch (agent.provider) {
54
+ case "codex":
55
+ reviewer = new CodexAgent(agent, schema, timeout, contextPath, sessionName);
56
+ break;
57
+ case "gemini":
58
+ reviewer = new GeminiAgent(agent, schema, timeout, contextPath, sessionName);
59
+ break;
60
+ case "claude":
61
+ default:
62
+ reviewer = new ClaudeAgent(agent, schema, timeout, contextPath, sessionName);
63
+ break;
258
64
  }
259
65
 
260
- // Parse output
261
- const obj = parseJsonMaybe(raw) ?? parseJsonMaybe(result.stdout);
262
- const [ok, verdict, norm] = coerceToReview(
263
- obj,
264
- "Retry or check Codex CLI auth/config.",
265
- );
266
-
267
- return makeResult(agent.name, ok, verdict, norm, raw || result.stdout, err);
268
- } finally {
269
- try {
270
- fs.rmSync(tmpDir, { recursive: true, force: true });
271
- } catch (e) {
272
- logDebug(agent.name, `Failed to cleanup temp dir ${tmpDir}: ${e}`);
273
- }
66
+ return await reviewer.review(plan);
67
+ } catch (e) {
68
+ logWarn(agent.name, `Unexpected error creating reviewer: ${e}`);
69
+ return makeResult(agent.name, false, "error", {}, "", `Failed: ${e}`);
274
70
  }
275
71
  }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Abstract base class for CLI-based agent reviewers.
3
+ * Implements template method pattern for subprocess execution flow.
4
+ * Provider-specific implementations (Claude, Codex, Gemini) extend this class.
5
+ */
6
+
7
+ import { logDebug, logInfo, logWarn, logError } from "../../../../_shared/lib-ts/base/logger.js";
8
+ import { getInternalSubprocessEnv, findExecutable, execFileAsync } from "../../../../_shared/lib-ts/base/subprocess-utils.js";
9
+ import { debugLog, debugRaw } from "../../debug.js";
10
+ import type { AgentConfig } from "../../types.js";
11
+
12
+ /** Result from execFileAsync */
13
+ export interface ExecResult {
14
+ stdout: string;
15
+ stderr: string;
16
+ exitCode: number | null;
17
+ signal: string | null;
18
+ killed: boolean;
19
+ }
20
+
21
+ /**
22
+ * Abstract base class for all CLI agent subprocess invocations.
23
+ * Parameterized over return type T — ReviewerResult for reviewers,
24
+ * OrchestratorResult for the orchestrator.
25
+ * Subclasses implement provider-specific details.
26
+ */
27
+ export abstract class BaseCliAgent<T> {
28
+ protected agent: AgentConfig;
29
+ protected schema: Record<string, unknown>;
30
+ protected timeout: number;
31
+ protected contextPath?: string;
32
+ protected sessionName: string;
33
+
34
+ constructor(
35
+ agent: AgentConfig,
36
+ schema: Record<string, unknown>,
37
+ timeout: number,
38
+ contextPath?: string,
39
+ sessionName = "unknown",
40
+ ) {
41
+ this.agent = agent;
42
+ this.schema = schema;
43
+ this.timeout = timeout;
44
+ this.contextPath = contextPath;
45
+ this.sessionName = sessionName;
46
+ }
47
+
48
+ /**
49
+ * Template method - orchestrates the review flow.
50
+ * Subclasses override abstract methods to customize behavior.
51
+ */
52
+ async review(plan: string): Promise<T> {
53
+ // 1. Find CLI executable
54
+ const cliPath = this.findCli();
55
+ if (!cliPath) {
56
+ return this.makeSkipResult(`${this.getCliName()} CLI not found on PATH`);
57
+ }
58
+
59
+ logDebug(this.agent.name, `Found ${this.getCliName()} CLI at: ${cliPath}`);
60
+
61
+ // 2. Build prompt and args (provider-specific)
62
+ const prompt = this.buildPrompt(plan);
63
+ const args = this.buildCliArgs();
64
+
65
+ logInfo(this.agent.name, `Running ${this.getCliName()} with model: ${this.agent.model}, timeout: ${this.timeout}s`);
66
+
67
+ // 3. Execute subprocess
68
+ const env = getInternalSubprocessEnv();
69
+ const result = await execFileAsync(cliPath, args, {
70
+ input: prompt,
71
+ timeout: this.timeout * 1000,
72
+ env: env as Record<string, string>,
73
+ maxBuffer: 10 * 1024 * 1024,
74
+ shell: process.platform === "win32",
75
+ });
76
+
77
+ // 4. Handle timeout
78
+ if (result.killed || result.signal === "SIGTERM") {
79
+ return this.handleTimeout();
80
+ }
81
+
82
+ // 5. Extract output (provider-specific)
83
+ const { raw, err } = this.extractOutput(result);
84
+
85
+ // 6. Handle exit errors
86
+ if (!raw && !err && result.exitCode !== 0) {
87
+ return this.handleExitError(result);
88
+ }
89
+
90
+ // 7. Log subprocess results
91
+ this.logSubprocessResult(result, raw, err);
92
+
93
+ // 8. Parse JSON output (provider-specific)
94
+ const obj = this.parseOutput(raw, result);
95
+
96
+ // 9. Log parsed result
97
+ this.logParsedResult(obj);
98
+
99
+ // 10. Coerce to result type T (provider-specific)
100
+ const coerced = this.coerceResult(obj, raw, err);
101
+
102
+ // 11. Cleanup (optional override)
103
+ await this.cleanup();
104
+
105
+ return coerced;
106
+ }
107
+
108
+ // ─── Abstract Methods (Subclass Implements) ────────────────────────────
109
+
110
+ /** Get the CLI executable name (e.g., "claude", "codex") */
111
+ protected abstract getCliName(): string;
112
+
113
+ /** Build the stdin prompt for the CLI */
114
+ protected abstract buildPrompt(plan: string): string;
115
+
116
+ /** Build the command-line arguments for the CLI */
117
+ protected abstract buildCliArgs(): string[];
118
+
119
+ /** Parse JSON from CLI output */
120
+ protected abstract parseOutput(raw: string, result: ExecResult): Record<string, unknown> | null;
121
+
122
+ /** Coerce parsed JSON into the result type T */
123
+ protected abstract coerceResult(obj: Record<string, unknown> | null, raw: string, err: string): T;
124
+
125
+ // ─── Template Methods (Subclass Can Override) ──────────────────────────
126
+
127
+ /** Find the CLI executable. Override for custom search logic. */
128
+ protected findCli(): string | null {
129
+ return findExecutable(this.getCliName());
130
+ }
131
+
132
+ /** Extract stdout/stderr from subprocess result. Override for file-based output (Codex). */
133
+ protected extractOutput(result: ExecResult): { raw: string; err: string } {
134
+ return {
135
+ raw: result.stdout.trim(),
136
+ err: result.stderr.trim(),
137
+ };
138
+ }
139
+
140
+ /** Get default error message for coerceToReview */
141
+ protected getDefaultErrorMessage(): string {
142
+ return `Retry or check ${this.getCliName()} configuration.`;
143
+ }
144
+
145
+ /** Optional cleanup after subprocess execution */
146
+ protected async cleanup(): Promise<void> {
147
+ // Default: no-op. Subclasses override if needed (e.g., Codex temp files).
148
+ }
149
+
150
+ // ─── Shared Infrastructure ──────────────────────────────────────────────
151
+
152
+ /** Create skip result when CLI not found */
153
+ protected makeSkipResult(reason: string): T {
154
+ logWarn(this.agent.name, reason);
155
+ return this.makeErrorResult("skip", reason);
156
+ }
157
+
158
+ /** Handle timeout scenario */
159
+ protected handleTimeout(): T {
160
+ const msg = `${this.getCliName()} TIMEOUT after ${this.timeout}s`;
161
+ logWarn(this.agent.name, msg);
162
+ return this.makeErrorResult("error", `${this.agent.name} timed out after ${this.timeout}s`);
163
+ }
164
+
165
+ /** Handle non-zero exit with no output */
166
+ protected handleExitError(result: ExecResult): T {
167
+ const msg = `${this.agent.name} failed to run (exit ${result.exitCode})`;
168
+ logError(this.agent.name, `Process exited with code ${result.exitCode} and no output`);
169
+ return this.makeErrorResult("error", msg);
170
+ }
171
+
172
+ /** Construct a T for error/skip/timeout scenarios. Subclasses define shape. */
173
+ protected abstract makeErrorResult(type: "skip" | "error", message: string): T;
174
+
175
+ /** Log subprocess execution results */
176
+ protected logSubprocessResult(result: ExecResult, raw: string, err: string): void {
177
+ logDebug(this.agent.name, `Exit code: ${result.exitCode}`);
178
+ logDebug(this.agent.name, `stdout length: ${raw.length} chars`);
179
+ if (err) logDebug(this.agent.name, `stderr: ${err.slice(0, 500)}`);
180
+
181
+ // Debug logging
182
+ if (this.contextPath) {
183
+ debugRaw(this.contextPath, this.sessionName, `agent:${this.agent.name}`, "stdout", raw);
184
+ if (err) {
185
+ debugRaw(this.contextPath, this.sessionName, `agent:${this.agent.name}`, "stderr", err);
186
+ }
187
+ debugLog(this.contextPath, this.sessionName, `agent:${this.agent.name}`, "subprocess_info", {
188
+ exit_code: result.exitCode,
189
+ stdout_len: raw.length,
190
+ stderr_len: err.length,
191
+ model: this.agent.model,
192
+ provider: this.agent.provider,
193
+ timeout: this.timeout,
194
+ });
195
+ }
196
+
197
+ if (raw) logDebug(this.agent.name, `stdout preview: ${raw.slice(0, 500)}`);
198
+ }
199
+
200
+ /** Log parsed JSON result */
201
+ protected logParsedResult(obj: Record<string, unknown> | null): void {
202
+ if (this.contextPath && obj) {
203
+ debugLog(this.contextPath, this.sessionName, `agent:${this.agent.name}`, "parsed_result", {
204
+ parsed_keys: Object.keys(obj),
205
+ verdict: obj.verdict ?? null,
206
+ has_summary: Boolean(obj.summary),
207
+ issues_count: Array.isArray(obj.issues) ? (obj.issues as unknown[]).length : 0,
208
+ });
209
+ }
210
+
211
+ if (obj) {
212
+ logInfo(this.agent.name, `Parsed JSON successfully, verdict: ${obj.verdict ?? "N/A"}`);
213
+ } else {
214
+ logWarn(this.agent.name, "Failed to parse JSON from output");
215
+ }
216
+ }
217
+ }
@@ -3,8 +3,10 @@
3
3
  * See cc-native-plan-review-spec.md §4.9
4
4
  */
5
5
 
6
- export { AgentReviewer, runAgentReview } from "./agent.js";
7
- export { CodexReviewer, runCodexReview } from "./codex.js";
8
- export { GeminiReviewer, runGeminiReview } from "./gemini.js";
9
6
  export type { Reviewer, ReviewerResult, ReviewOptions } from "./types.js";
10
7
  export { makeResult } from "./types.js";
8
+ export { AgentReviewer, runAgentReview } from "./agent.js";
9
+ export { BaseCliAgent } from "./base/base-agent.js";
10
+ export { ClaudeAgent } from "./providers/claude-agent.js";
11
+ export { CodexAgent } from "./providers/codex-agent.js";
12
+ export { GeminiAgent } from "./providers/gemini-agent.js";