aiwcli 0.14.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. package/dist/templates/_shared/.claude/skills/codex/prompt.md +25 -5
  2. package/dist/templates/_shared/lib-ts/agent-exec/index.ts +2 -0
  3. package/dist/templates/_shared/lib-ts/agent-exec/structured-output.ts +166 -0
  4. package/dist/templates/_shared/lib-ts/base/cli-args.ts +4 -0
  5. package/dist/templates/_shared/lib-ts/base/state-io.ts +1 -1
  6. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +4 -3
  7. package/dist/templates/_shared/lib-ts/context/context-store.ts +3 -0
  8. package/dist/templates/_shared/scripts/status_line.ts +36 -19
  9. package/dist/templates/_shared/skills/prompt-codex/CLAUDE.md +29 -4
  10. package/dist/templates/_shared/skills/prompt-codex/scripts/launch-codex.ts +140 -7
  11. package/dist/templates/_shared/skills/prompt-codex/scripts/watch-codex.ts +257 -0
  12. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +9 -133
  13. package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +118 -42
  14. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +1 -0
  15. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +61 -0
  16. package/dist/templates/cc-native/_cc-native/plan-review/lib/agent-selection.ts +5 -4
  17. package/dist/templates/cc-native/_cc-native/plan-review/lib/orchestrator.ts +4 -4
  18. package/dist/templates/cc-native/_cc-native/plan-review/lib/review-pipeline.ts +16 -13
  19. package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/providers/orchestrator-claude-agent.ts +54 -23
  20. package/oclif.manifest.json +1 -1
  21. package/package.json +1 -1
@@ -11,8 +11,12 @@ import { loadConfig, getDisplaySettings } from "./config.js";
11
11
  import { DEFAULT_REVIEW_ITERATIONS } from "./state.js";
12
12
  import type {
13
13
  AgentConfig,
14
- ProviderConfig,
14
+ AgentReviewSettings,
15
+ AgentSelectionConfig,
16
+ LoadedSettings,
15
17
  ModelsConfig,
18
+ PlanReviewSettings,
19
+ ProviderConfig,
16
20
  } from "./types.js";
17
21
  import { DEFAULT_DISPLAY, DEFAULT_SANITIZATION } from "./types.js";
18
22
  import { logInfo } from "../../_shared/lib-ts/base/logger.js";
@@ -57,10 +61,10 @@ export const DEFAULT_AGENTS: Array<{ name: string; model: string; provider: stri
57
61
  { ...AGENT_DEFAULTS, name: "constraint-validator", focus: "constraint identification and satisfaction", categories: ALL_CATEGORIES },
58
62
  ];
59
63
 
60
- export const DEFAULT_ORCHESTRATOR: { enabled: boolean; model: string; timeout: number } = { enabled: true, model: "opus", timeout: 60 };
64
+ export const DEFAULT_ORCHESTRATOR = { enabled: true, model: CODEX_MODELS.codex, provider: "codex", timeout: 60 } as const;
61
65
  export const DEFAULT_AGENT_MODEL = "sonnet";
62
66
 
63
- export const DEFAULT_AGENT_SELECTION: Record<string, unknown> = {
67
+ export const DEFAULT_AGENT_SELECTION: AgentSelectionConfig = {
64
68
  simple: { min: 3, max: 3 },
65
69
  medium: { min: 5, max: 5 },
66
70
  high: { min: 7, max: 7 },
@@ -80,58 +84,131 @@ export const DEFAULT_MODELS_CONFIG: ModelsConfig = {
80
84
  // Settings Loading
81
85
  // ---------------------------------------------------------------------------
82
86
 
83
- export function loadSettings(projDir: string): Record<string, unknown> {
84
- const defaults: Record<string, unknown> = {
85
- planReview: {
86
- enabled: true,
87
- reviewers: {
88
- codex: { enabled: true, model: "", timeout: 120 },
89
- gemini: { enabled: false, model: "", timeout: 120 },
90
- },
91
- display: { ...DEFAULT_DISPLAY },
92
- },
93
- agentReview: {
94
- enabled: true,
95
- orchestrator: { ...DEFAULT_ORCHESTRATOR },
96
- timeout: 180,
97
- highIssueThreshold: 3,
98
- legacyMode: false,
99
- display: { ...DEFAULT_DISPLAY },
100
- agentSelection: { ...DEFAULT_AGENT_SELECTION },
101
- agentDefaults: { model: DEFAULT_AGENT_MODEL },
102
- complexityCategories: [...DEFAULT_COMPLEXITY_CATEGORIES],
103
- sanitization: { ...DEFAULT_SANITIZATION },
87
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
88
+ return value && typeof value === "object" && !Array.isArray(value)
89
+ ? value as Record<string, unknown>
90
+ : undefined;
91
+ }
92
+
93
+ function asStringArray(value: unknown): string[] | undefined {
94
+ if (!Array.isArray(value)) return undefined;
95
+ const filtered = value.filter((item): item is string => typeof item === "string" && item.trim().length > 0);
96
+ return filtered.length > 0 ? filtered : undefined;
97
+ }
98
+
99
+ export function loadSettings(projDir: string): LoadedSettings {
100
+ const defaultPlan: PlanReviewSettings = {
101
+ enabled: true,
102
+ reviewers: {
103
+ codex: { enabled: true, model: "", timeout: 120 },
104
+ gemini: { enabled: false, model: "", timeout: 120 },
104
105
  },
106
+ display: { ...DEFAULT_DISPLAY },
107
+ };
108
+
109
+ const defaultAgent: AgentReviewSettings = {
110
+ enabled: true,
111
+ orchestrator: { ...DEFAULT_ORCHESTRATOR },
112
+ timeout: 180,
113
+ highIssueThreshold: 3,
114
+ legacyMode: false,
115
+ display: { ...DEFAULT_DISPLAY },
116
+ agentSelection: { ...DEFAULT_AGENT_SELECTION },
117
+ agentDefaults: { model: DEFAULT_AGENT_MODEL },
118
+ complexityCategories: [...DEFAULT_COMPLEXITY_CATEGORIES],
119
+ sanitization: { ...DEFAULT_SANITIZATION },
105
120
  };
106
121
 
107
122
  const config = loadConfig(projDir);
108
- if (!config || Object.keys(config).length === 0) return { ...defaults, models: {} };
123
+ if (!config || Object.keys(config).length === 0) {
124
+ return { planReview: defaultPlan, agentReview: defaultAgent, models: {} };
125
+ }
126
+
127
+ // Cast raw config to access arbitrary keys from JSON
128
+ const raw = config as Record<string, unknown>;
109
129
 
110
130
  // Merge planReview
111
- const planReview = config.planReview ?? {};
112
- const mergedPlan = { ...defaults.planReview, ...planReview };
113
- if (planReview.reviewers) {
114
- mergedPlan.reviewers = { ...defaults.planReview.reviewers, ...planReview.reviewers };
131
+ const planReviewRaw = (asRecord(raw.planReview) ?? {}) as Partial<PlanReviewSettings>;
132
+ const mergedPlan: PlanReviewSettings = { ...defaultPlan, ...planReviewRaw };
133
+ if (planReviewRaw.reviewers) {
134
+ mergedPlan.reviewers = { ...defaultPlan.reviewers, ...planReviewRaw.reviewers };
115
135
  }
116
136
  mergedPlan.display = getDisplaySettings(config, "planReview");
117
137
 
118
138
  // Merge agentReview
119
- const agentReview = (config as Record<string, unknown>).agentReview ?? {};
120
- const mergedAgent = { ...defaults.agentReview, ...agentReview };
139
+ const agentReviewRawRecord = asRecord(raw.agentReview);
140
+ const agentReviewRaw = (agentReviewRawRecord ?? {}) as Partial<AgentReviewSettings>;
141
+ const mergedAgent: AgentReviewSettings = { ...defaultAgent, ...agentReviewRaw };
121
142
  if (!mergedAgent.orchestrator || typeof mergedAgent.orchestrator !== "object") {
122
143
  mergedAgent.orchestrator = { ...DEFAULT_ORCHESTRATOR };
123
144
  } else {
124
145
  mergedAgent.orchestrator = { ...DEFAULT_ORCHESTRATOR, ...mergedAgent.orchestrator };
125
146
  }
126
147
  mergedAgent.display = getDisplaySettings(config, "agentReview");
127
- const configRecord = config as Record<string, unknown>;
128
- mergedAgent.agentSelection = { ...DEFAULT_AGENT_SELECTION, ...(configRecord.agentSelection as Record<string, unknown>) };
129
- mergedAgent.agentDefaults = { model: DEFAULT_AGENT_MODEL, ...(configRecord.agentDefaults as Record<string, unknown>) };
130
- mergedAgent.complexityCategories = (configRecord.complexityCategories as string[]) ?? [...DEFAULT_COMPLEXITY_CATEGORIES];
131
- mergedAgent.sanitization = { ...DEFAULT_SANITIZATION, ...(configRecord.sanitization as Record<string, unknown>) };
132
- mergedAgent.reviewIterations = { ...DEFAULT_REVIEW_ITERATIONS, ...agentReview.reviewIterations };
133
-
134
- const modelsRaw = (config as Record<string, unknown>).models ?? {};
148
+
149
+ const nestedAgentSelection = asRecord(agentReviewRawRecord?.agentSelection) as AgentSelectionConfig | undefined;
150
+ const topLevelAgentSelection = asRecord(raw.agentSelection) as AgentSelectionConfig | undefined;
151
+ mergedAgent.agentSelection = {
152
+ ...DEFAULT_AGENT_SELECTION,
153
+ ...nestedAgentSelection,
154
+ ...topLevelAgentSelection,
155
+ };
156
+
157
+ const nestedAgentDefaults = asRecord(agentReviewRawRecord?.agentDefaults) as { model?: string } | undefined;
158
+ const topLevelAgentDefaults = asRecord(raw.agentDefaults) as { model?: string } | undefined;
159
+ mergedAgent.agentDefaults = {
160
+ model: DEFAULT_AGENT_MODEL,
161
+ ...nestedAgentDefaults,
162
+ ...topLevelAgentDefaults,
163
+ };
164
+
165
+ const nestedComplexityCategories = asStringArray(agentReviewRawRecord?.complexityCategories);
166
+ const topLevelComplexityCategories = asStringArray(raw.complexityCategories);
167
+ mergedAgent.complexityCategories = topLevelComplexityCategories
168
+ ?? nestedComplexityCategories
169
+ ?? [...DEFAULT_COMPLEXITY_CATEGORIES];
170
+
171
+ const nestedSanitization = asRecord(agentReviewRawRecord?.sanitization);
172
+ const topLevelSanitization = asRecord(raw.sanitization);
173
+ mergedAgent.sanitization = {
174
+ ...DEFAULT_SANITIZATION,
175
+ ...nestedSanitization,
176
+ ...topLevelSanitization,
177
+ };
178
+
179
+ const nestedFallbackByComplexity = asRecord(agentReviewRawRecord?.fallbackByComplexity) as Record<string, number> | undefined;
180
+ const topLevelFallbackByComplexity = asRecord(raw.fallbackByComplexity) as Record<string, number> | undefined;
181
+ if (nestedFallbackByComplexity || topLevelFallbackByComplexity) {
182
+ mergedAgent.fallbackByComplexity = {
183
+ ...(nestedFallbackByComplexity ?? {}),
184
+ ...(topLevelFallbackByComplexity ?? {}),
185
+ };
186
+ }
187
+
188
+ const nestedMandatoryAgents = agentReviewRawRecord?.mandatoryAgents as AgentReviewSettings["mandatoryAgents"] | undefined;
189
+ const topLevelMandatoryAgents = raw.mandatoryAgents as AgentReviewSettings["mandatoryAgents"] | undefined;
190
+ if (nestedMandatoryAgents !== undefined || topLevelMandatoryAgents !== undefined) {
191
+ mergedAgent.mandatoryAgents = topLevelMandatoryAgents ?? nestedMandatoryAgents;
192
+ }
193
+
194
+ const nestedPreflight = asRecord(agentReviewRawRecord?.preflight);
195
+ const topLevelPreflight = asRecord(raw.preflight);
196
+ if (nestedPreflight || topLevelPreflight) {
197
+ mergedAgent.preflight = {
198
+ ...(nestedPreflight ?? {}),
199
+ ...(topLevelPreflight ?? {}),
200
+ };
201
+ }
202
+
203
+ const nestedReviewIterations = asRecord(agentReviewRawRecord?.reviewIterations) as Record<string, number> | undefined;
204
+ const topLevelReviewIterations = asRecord(raw.reviewIterations) as Record<string, number> | undefined;
205
+ mergedAgent.reviewIterations = {
206
+ ...DEFAULT_REVIEW_ITERATIONS,
207
+ ...(nestedReviewIterations ?? {}),
208
+ ...(topLevelReviewIterations ?? {}),
209
+ };
210
+
211
+ const modelsRaw = (raw.models ?? {}) as Record<string, unknown>;
135
212
  return { planReview: mergedPlan, agentReview: mergedAgent, models: modelsRaw };
136
213
  }
137
214
 
@@ -139,7 +216,7 @@ export function loadSettings(projDir: string): Record<string, unknown> {
139
216
  // Models Config
140
217
  // ---------------------------------------------------------------------------
141
218
 
142
- export function loadModelsConfig(settings: Record<string, unknown>): ModelsConfig {
219
+ export function loadModelsConfig(settings: LoadedSettings): ModelsConfig {
143
220
  const raw = settings.models as Record<string, unknown> | undefined;
144
221
  if (!raw?.providers || typeof raw.providers !== "object") {
145
222
  return DEFAULT_MODELS_CONFIG;
@@ -150,7 +227,6 @@ export function loadModelsConfig(settings: Record<string, unknown>): ModelsConfi
150
227
  providers[name] = {
151
228
  enabled: c.enabled !== false,
152
229
  models: Array.isArray(c.models) ? (c.models as string[]).filter(Boolean) : [],
153
- ...(typeof c.reasoningEffort === "string" && { reasoningEffort: c.reasoningEffort }),
154
230
  };
155
231
  }
156
232
  return { providers };
@@ -162,7 +238,7 @@ export function loadModelsConfig(settings: Record<string, unknown>): ModelsConfi
162
238
 
163
239
  export function loadAgentLibrary(
164
240
  projDir: string,
165
- settings?: Record<string, unknown>,
241
+ settings?: AgentReviewSettings,
166
242
  ): AgentConfig[] {
167
243
  const agentsData = aggregateAgents(path.join(projDir, "_cc-native", "plan-review", "agents", "plan-review"));
168
244
  const defaultModel = settings?.agentDefaults?.model ?? DEFAULT_AGENT_MODEL;
@@ -178,6 +178,7 @@ export function getIterationState(
178
178
  passStreaks: {},
179
179
  lastPlanHash: "",
180
180
  lastPlanPath: "",
181
+ sessionId: "",
181
182
  };
182
183
  }
183
184
 
@@ -128,6 +128,7 @@ export interface ReviewDecisionResult {
128
128
  export interface OrchestratorConfig {
129
129
  enabled: boolean;
130
130
  model: string;
131
+ provider?: string;
131
132
  timeout: number;
132
133
  }
133
134
 
@@ -142,6 +143,66 @@ export interface ModelsConfig {
142
143
  providers: Record<string, ProviderConfig>;
143
144
  }
144
145
 
146
+ // ---------------------------------------------------------------------------
147
+ // Settings Interfaces (typed output of loadSettings())
148
+ // ---------------------------------------------------------------------------
149
+
150
+ /** Agent selection count range for a single complexity tier */
151
+ export interface AgentSelectionRange {
152
+ min: number;
153
+ max: number;
154
+ }
155
+
156
+ /** Agent selection configuration (per-tier ranges + fallback) */
157
+ export interface AgentSelectionConfig {
158
+ simple?: AgentSelectionRange;
159
+ medium?: AgentSelectionRange;
160
+ high?: AgentSelectionRange;
161
+ fallbackCount?: number;
162
+ }
163
+
164
+ /** Preflight health-check configuration */
165
+ export interface PreflightSettings {
166
+ enabled?: boolean;
167
+ timeoutMs?: number;
168
+ }
169
+
170
+ /** Plan review section of merged settings (the "planReview" key) */
171
+ export interface PlanReviewSettings {
172
+ enabled?: boolean;
173
+ reviewers?: {
174
+ codex?: { enabled?: boolean; model?: string; timeout?: number };
175
+ gemini?: { enabled?: boolean; model?: string; timeout?: number };
176
+ };
177
+ display?: Partial<DisplaySettings>;
178
+ }
179
+
180
+ /** Agent review section of merged settings (the "agentReview" key) */
181
+ export interface AgentReviewSettings {
182
+ enabled?: boolean;
183
+ timeout?: number;
184
+ orchestrator?: OrchestratorConfig;
185
+ legacyMode?: boolean;
186
+ highIssueThreshold?: number;
187
+ maxIssuesPerAgent?: number;
188
+ mandatoryAgents?: string[] | Record<string, string[]>;
189
+ agentSelection?: AgentSelectionConfig;
190
+ agentDefaults?: { model?: string };
191
+ complexityCategories?: string[];
192
+ sanitization?: { maxSessionIdLength?: number; maxTitleLength?: number };
193
+ reviewIterations?: Record<string, number>;
194
+ display?: Partial<DisplaySettings>;
195
+ preflight?: PreflightSettings;
196
+ fallbackByComplexity?: Record<string, number>;
197
+ }
198
+
199
+ /** Top-level settings object returned by loadSettings() */
200
+ export interface LoadedSettings {
201
+ planReview: PlanReviewSettings;
202
+ agentReview: AgentReviewSettings;
203
+ models: Record<string, unknown>;
204
+ }
205
+
145
206
  // ---------------------------------------------------------------------------
146
207
  // State Interfaces
147
208
  // ---------------------------------------------------------------------------
@@ -7,6 +7,7 @@ import { logDebug, logInfo, logWarn } from "../../../_shared/lib-ts/base/logger.
7
7
  import { findExecutable } from "../../../_shared/lib-ts/base/subprocess-utils.js";
8
8
  import type {
9
9
  AgentConfig,
10
+ AgentReviewSettings,
10
11
  ModelsConfig,
11
12
  OrchestratorResult,
12
13
  AgentSelectionResult,
@@ -89,7 +90,7 @@ export function assignModelsToAgents(
89
90
  }
90
91
  return [name, config] as [string, typeof config];
91
92
  })
92
- .filter((entry): entry is [string, { enabled: boolean; models: string[]; reasoningEffort?: string }] => entry !== null);
93
+ .filter((entry): entry is [string, { enabled: boolean; models: string[] }] => entry !== null);
93
94
 
94
95
  // Sort by provider priority (codex first)
95
96
  enabledProviders.sort((a, b) => {
@@ -110,7 +111,7 @@ export function assignModelsToAgents(
110
111
  return agents.map(agent => {
111
112
  const modelIdx = Math.floor(Math.random() * providerConfig.models.length);
112
113
  const model = providerConfig.models[modelIdx] ?? providerConfig.models[0] ?? agent.model;
113
- return { ...agent, provider: providerName, model, reasoningEffort: providerConfig.reasoningEffort };
114
+ return { ...agent, provider: providerName, model };
114
115
  });
115
116
  }
116
117
 
@@ -122,7 +123,7 @@ export interface AgentSelectionInput {
122
123
  enabledAgents: AgentConfig[];
123
124
  orchResult: OrchestratorResult | null;
124
125
  mandatoryConfig: unknown;
125
- agentSettings: Record<string, unknown>;
126
+ agentSettings: AgentReviewSettings;
126
127
  legacyMode: boolean;
127
128
  }
128
129
 
@@ -168,7 +169,7 @@ export function selectAgents(input: AgentSelectionInput): AgentSelectionResult {
168
169
  }
169
170
 
170
171
  // Enforce minimum agent count
171
- const fallbackByComplexity = agentSettings.fallbackByComplexity ?? { simple: 0, medium: 2, high: 4 };
172
+ const fallbackByComplexity: Record<string, number> = agentSettings.fallbackByComplexity ?? { simple: 0, medium: 2, high: 4 };
172
173
  const minAdditional = fallbackByComplexity[detectedComplexity] ?? 5;
173
174
  if (orchSelected.length < minAdditional && nonMandatory.length > 0) {
174
175
  const remaining = nonMandatory.filter(a => !orchSelected.includes(a));
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { OrchestratorClaudeAgent } from "./reviewers/providers/orchestrator-claude-agent.js";
8
8
  import { logInfo, logWarn } from "../../../_shared/lib-ts/base/logger.js";
9
- import type { AgentConfig, OrchestratorConfig, OrchestratorResult } from "../../lib-ts/types.js";
9
+ import type { AgentConfig, AgentReviewSettings, OrchestratorConfig, OrchestratorResult } from "../../lib-ts/types.js";
10
10
 
11
11
  // Re-export for backward compatibility (moved to reviewers/schemas.ts)
12
12
  export { buildOrchestratorSchema } from "./reviewers/schemas.js";
@@ -23,7 +23,7 @@ export async function runOrchestrator(
23
23
  plan: string,
24
24
  agentLibrary: AgentConfig[],
25
25
  config: OrchestratorConfig,
26
- settings: Record<string, unknown>,
26
+ settings: AgentReviewSettings,
27
27
  mandatoryNames?: Set<string>,
28
28
  ): Promise<OrchestratorResult> {
29
29
  logInfo("orchestrator", "Starting plan analysis...");
@@ -34,7 +34,7 @@ export async function runOrchestrator(
34
34
  const orchestratorAgent: AgentConfig = {
35
35
  name: "orchestrator",
36
36
  model: config.model,
37
- provider: "claude",
37
+ provider: config.provider ?? "claude",
38
38
  focus: "plan analysis and agent selection",
39
39
  categories: [],
40
40
  description: "Plan orchestrator",
@@ -58,7 +58,7 @@ export async function runOrchestrator(
58
58
  } catch (error) {
59
59
  logWarn("orchestrator", `Unexpected error: ${error}`);
60
60
  const nonMandatory = agentLibrary.filter((a) => !mandatory.has(a.name));
61
- const fallbackCount = ((settings.agentSelection as Record<string, unknown>)?.fallbackCount as number) ?? 2;
61
+ const fallbackCount = settings.agentSelection?.fallbackCount ?? 2;
62
62
  return {
63
63
  complexity: "medium",
64
64
  category: "code",
@@ -43,7 +43,10 @@ import { loadSettings, loadModelsConfig, loadAgentLibrary, DEFAULT_ORCHESTRATOR
43
43
  import { DEFAULT_REVIEW_ITERATIONS, loadIterationState, saveIterationState } from "../../lib-ts/state.js";
44
44
  import type {
45
45
  AgentConfig,
46
+ AgentReviewSettings,
47
+ LoadedSettings,
46
48
  OrchestratorConfig,
49
+ PlanReviewSettings,
47
50
  ReviewerResult,
48
51
  CombinedReviewResult,
49
52
  OrchestratorResult,
@@ -66,7 +69,7 @@ function getActiveContextForReview(sessionId: string, projectRoot: string): Cont
66
69
  return ctx;
67
70
  }
68
71
  const allActive = getAllContexts("active", projectRoot);
69
- const planning = allActive.filter(c => c.mode === "active" || c.mode === "has_plan");
72
+ const planning = allActive.filter(c => c.mode === "active" || c.mode === "has_staged_work");
70
73
  if (planning.length === 1) {
71
74
  logInfo(HOOK, `Found single planning context: ${planning[0]!.id}`);
72
75
  return planning[0]!;
@@ -89,9 +92,9 @@ export async function runReviewPipeline(input: PipelineInput): Promise<PipelineR
89
92
  const { sessionId, base, aiwcliDir, transcriptPath, payload } = input;
90
93
 
91
94
  // 1. Load settings
92
- const settings = loadSettings(aiwcliDir);
93
- const planSettings = settings.planReview ?? {};
94
- const agentSettings = settings.agentReview ?? {};
95
+ const settings: LoadedSettings = loadSettings(aiwcliDir);
96
+ const planSettings: PlanReviewSettings = settings.planReview;
97
+ const agentSettings: AgentReviewSettings = settings.agentReview;
95
98
 
96
99
  const planReviewEnabled = planSettings.enabled ?? true;
97
100
  const agentReviewEnabled = agentSettings.enabled ?? true;
@@ -162,8 +165,8 @@ export async function runReviewPipeline(input: PipelineInput): Promise<PipelineR
162
165
  // 5. Questions gate
163
166
  if (!wasPlanQuestionsAgentAsked(sessionId, base)) {
164
167
  logInfo(HOOK, "Questions gate: plan-questions agent has not run yet, running now");
165
- const timeout = typeof agentSettings.timeout === "number" ? agentSettings.timeout : 120;
166
- const questionsResult = await runPlanQuestions(plan, aiwcliDir, timeout, undefined, sessionId);
168
+ const questionsTimeout = agentSettings.timeout ?? 120;
169
+ const questionsResult = await runPlanQuestions(plan, aiwcliDir, questionsTimeout, undefined, sessionId);
167
170
 
168
171
  markQuestionsAsked(sessionId, base, "agent");
169
172
 
@@ -227,12 +230,12 @@ export async function runReviewPipeline(input: PipelineInput): Promise<PipelineR
227
230
  let detectedComplexity = "medium";
228
231
 
229
232
  // Preflight: validate provider+model combos before committing agents or orchestrator
230
- const preflightEnabled = (agentSettings.preflight as Record<string, unknown>)?.enabled ?? true;
233
+ const preflightEnabled = agentSettings.preflight?.enabled ?? true;
231
234
  let preflightAvailable: Map<string, Set<string>> | undefined;
232
235
 
233
236
  if (preflightEnabled && agentReviewEnabled) {
234
237
  logInfo(HOOK, "=== PREFLIGHT: Checking provider availability ===");
235
- const preflightTimeoutMs = (agentSettings.preflight as Record<string, unknown>)?.timeoutMs as number | undefined;
238
+ const preflightTimeoutMs = agentSettings.preflight?.timeoutMs;
236
239
  const modelsConfig = loadModelsConfig(settings);
237
240
  const preflightReport = await runPreflight(modelsConfig, preflightTimeoutMs);
238
241
 
@@ -257,13 +260,14 @@ export async function runReviewPipeline(input: PipelineInput): Promise<PipelineR
257
260
  const agentLibrary = agentReviewEnabled ? loadAgentLibrary(aiwcliDir, agentSettings) : [];
258
261
  const originalAgentCount = agentLibrary.length;
259
262
  const enabledAgents = agentLibrary.filter(a => !graduatedSet.has(a.name));
260
- const timeout = typeof agentSettings.timeout === "number" ? agentSettings.timeout : 120;
263
+ const timeout = agentSettings.timeout ?? 120;
261
264
  const legacyMode = agentSettings.legacyMode === true;
262
265
 
263
266
  const orchSettings = agentSettings.orchestrator ?? DEFAULT_ORCHESTRATOR;
264
267
  const orchestratorConfig: OrchestratorConfig = {
265
268
  enabled: (orchSettings.enabled ?? true) && agentReviewEnabled,
266
269
  model: orchSettings.model ?? "haiku",
270
+ provider: orchSettings.provider,
267
271
  timeout: orchSettings.timeout ?? 30,
268
272
  };
269
273
 
@@ -278,7 +282,7 @@ export async function runReviewPipeline(input: PipelineInput): Promise<PipelineR
278
282
 
279
283
  if (orchestratorConfig.enabled && enabledAgents.length > 0 && !legacyMode) {
280
284
  // Guard orchestrator against preflight failures (always uses claude provider)
281
- const orchProvider = "claude";
285
+ const orchProvider = orchestratorConfig.provider ?? "claude";
282
286
  const orchModel = orchestratorConfig.model;
283
287
  const orchPassed = !preflightAvailable ||
284
288
  (preflightAvailable.has(orchProvider) && preflightAvailable.get(orchProvider)!.has(orchModel));
@@ -395,8 +399,7 @@ export async function runReviewPipeline(input: PipelineInput): Promise<PipelineR
395
399
  }
396
400
 
397
401
  // 10. Issue truncation + verdict override
398
- const maxIssuesPerAgent = typeof agentSettings.maxIssuesPerAgent === "number"
399
- ? agentSettings.maxIssuesPerAgent : 3;
402
+ const maxIssuesPerAgent = agentSettings.maxIssuesPerAgent ?? 3;
400
403
  truncateAgentIssues(agentResults, maxIssuesPerAgent);
401
404
 
402
405
  const passEligible = computePassEligible(agentResults);
@@ -404,7 +407,7 @@ export async function runReviewPipeline(input: PipelineInput): Promise<PipelineR
404
407
  logInfo(HOOK, `Pass-eligible agents this iteration: ${passEligible.join(", ")}`);
405
408
  }
406
409
 
407
- const highIssueThreshold = typeof agentSettings.highIssueThreshold === "number" ? agentSettings.highIssueThreshold : 3;
410
+ const highIssueThreshold = agentSettings.highIssueThreshold ?? 3;
408
411
  overrideVerdictsByThreshold(agentResults, highIssueThreshold);
409
412
 
410
413
  // PHASE 4: Generate Output
@@ -3,13 +3,13 @@
3
3
  * Analyzes plan complexity and selects reviewer agents via Claude CLI.
4
4
  */
5
5
 
6
- import { buildCliInvocation, reviewSpec } from "../../../../../_shared/lib-ts/base/cli-args.js";
6
+ import { buildCliInvocation, reviewSpec, type CliProvider } from "../../../../../_shared/lib-ts/base/cli-args.js";
7
7
  import type { ExecutionBackend } from "../../../../../_shared/lib-ts/agent-exec/execution-backend.js";
8
8
  import type { ExecutionResult } from "../../../../../_shared/lib-ts/agent-exec/execution-backend.js";
9
+ import { parseStructuredOutput } from "../../../../../_shared/lib-ts/agent-exec/structured-output.js";
9
10
  import { logDebug } from "../../../../../_shared/lib-ts/base/logger.js";
10
11
  import { debugLog, debugRaw } from "../../../../lib-ts/debug.js";
11
- import { parseCliOutput } from "../../../../lib-ts/cli-output-parser.js";
12
- import type { AgentConfig, OrchestratorResult, ComplexityCategory } from "../../../../lib-ts/types.js";
12
+ import type { AgentConfig, AgentReviewSettings, AgentSelectionConfig, OrchestratorResult, ComplexityCategory } from "../../../../lib-ts/types.js";
13
13
  import { BaseCliAgent } from "../base/base-agent.js";
14
14
  import { buildOrchestratorSchema, ORCHESTRATOR_SCHEMA } from "../schemas.js";
15
15
 
@@ -27,7 +27,7 @@ const DEFAULT_COMPLEXITY_CATEGORIES = [
27
27
  "research",
28
28
  ];
29
29
 
30
- const DEFAULT_AGENT_SELECTION: Record<string, unknown> = {
30
+ const DEFAULT_AGENT_SELECTION: AgentSelectionConfig = {
31
31
  simple: { min: 3, max: 3 },
32
32
  medium: { min: 8, max: 8 },
33
33
  high: { min: 12, max: 12 },
@@ -43,14 +43,14 @@ export class OrchestratorClaudeAgent extends BaseCliAgent<OrchestratorResult> {
43
43
  private fallbackCount: number;
44
44
  private mandatoryCount: number;
45
45
  private nonMandatory: AgentConfig[];
46
- private settings: Record<string, unknown>;
46
+ private settings: AgentReviewSettings;
47
47
  private validNames: string[];
48
48
 
49
49
  constructor(
50
50
  agent: AgentConfig,
51
51
  agentLibrary: AgentConfig[],
52
52
  mandatoryNames: Set<string>,
53
- settings: Record<string, unknown>,
53
+ settings: AgentReviewSettings,
54
54
  timeout: number,
55
55
  contextPath?: string,
56
56
  sessionName?: string,
@@ -61,7 +61,7 @@ export class OrchestratorClaudeAgent extends BaseCliAgent<OrchestratorResult> {
61
61
  (a) => !mandatoryNames.has(a.name),
62
62
  );
63
63
  const validNames = nonMandatory.map((a) => a.name);
64
- const categories = (settings.complexityCategories as string[]) ?? DEFAULT_COMPLEXITY_CATEGORIES;
64
+ const categories = settings.complexityCategories ?? DEFAULT_COMPLEXITY_CATEGORIES;
65
65
 
66
66
  const schema = validNames.length > 0
67
67
  ? buildOrchestratorSchema(validNames, categories)
@@ -77,8 +77,8 @@ export class OrchestratorClaudeAgent extends BaseCliAgent<OrchestratorResult> {
77
77
  this.categories = categories;
78
78
  this.settings = settings;
79
79
 
80
- const selection = (settings.agentSelection as Record<string, unknown>) ?? DEFAULT_AGENT_SELECTION;
81
- this.fallbackCount = (selection.fallbackCount as number) ?? 2;
80
+ const selection = settings.agentSelection ?? DEFAULT_AGENT_SELECTION;
81
+ this.fallbackCount = selection.fallbackCount ?? DEFAULT_AGENT_SELECTION.fallbackCount;
82
82
  this.mandatoryCount = agentLibrary.filter((a) => mandatoryNames.has(a.name)).length;
83
83
 
84
84
  logDebug("orchestrator", `Mandatory agents (always run): ${[...mandatoryNames].sort().join(", ")}`);
@@ -97,12 +97,12 @@ When selecting agents:
97
97
  - Fewer agents for simple plans, more for complex plans`;
98
98
 
99
99
  return buildCliInvocation(
100
- reviewSpec("claude", this.agent.model, this.schema, systemPrompt),
100
+ reviewSpec((this.agent.provider ?? "claude") as CliProvider, this.agent.model, this.schema, systemPrompt),
101
101
  ).args;
102
102
  }
103
103
 
104
104
  protected buildPrompt(plan: string): string {
105
- const selection = (this.settings.agentSelection as Record<string, unknown>) ?? DEFAULT_AGENT_SELECTION;
105
+ const selection = this.settings.agentSelection ?? DEFAULT_AGENT_SELECTION;
106
106
 
107
107
  const agentList = this.nonMandatory
108
108
  .map(
@@ -112,9 +112,9 @@ When selecting agents:
112
112
  .join("\n");
113
113
  const categoryList = this.categories.join("/");
114
114
 
115
- const simpleAdditional = Math.max(0, ((selection.simple as Record<string, number> | undefined)?.max ?? 3) - this.mandatoryCount);
116
- const mediumAdditional = Math.max(0, ((selection.medium as Record<string, number> | undefined)?.max ?? 8) - this.mandatoryCount);
117
- const highAdditional = Math.max(0, ((selection.high as Record<string, number> | undefined)?.max ?? 12) - this.mandatoryCount);
115
+ const simpleAdditional = Math.max(0, (selection.simple?.max ?? 3) - this.mandatoryCount);
116
+ const mediumAdditional = Math.max(0, (selection.medium?.max ?? 8) - this.mandatoryCount);
117
+ const highAdditional = Math.max(0, (selection.high?.max ?? 12) - this.mandatoryCount);
118
118
 
119
119
  return `Analyze this plan and select appropriate reviewer agents.
120
120
 
@@ -149,26 +149,54 @@ Call StructuredOutput now with: complexity, category, selectedAgents, reasoning`
149
149
  ? rawComplexity
150
150
  : "medium";
151
151
 
152
- let category = (obj.category as string) ?? "code";
153
- if (!this.categories.includes(category)) category = "code";
152
+ const defaultCategory = this.categories.includes("code")
153
+ ? "code"
154
+ : (this.categories[0] ?? "code");
155
+ let category = defaultCategory;
156
+ if (typeof obj.category === "string" && this.categories.includes(obj.category)) {
157
+ category = obj.category;
158
+ }
159
+
160
+ const rawSelected = Array.isArray(obj.selectedAgents)
161
+ ? obj.selectedAgents
162
+ : (Array.isArray(obj.selected_agents) ? obj.selected_agents : []);
163
+
164
+ const validNames = new Set(this.validNames);
165
+ const selectedAgents: string[] = [];
166
+ const unknownNames: string[] = [];
167
+ const seen = new Set<string>();
168
+ for (const value of rawSelected) {
169
+ if (typeof value !== "string") continue;
170
+ if (!validNames.has(value)) {
171
+ unknownNames.push(value);
172
+ continue;
173
+ }
174
+ if (!seen.has(value)) {
175
+ selectedAgents.push(value);
176
+ seen.add(value);
177
+ }
178
+ }
154
179
 
155
- let {selectedAgents} = obj;
156
- if (!Array.isArray(selectedAgents)) selectedAgents = [];
180
+ if (unknownNames.length > 0) {
181
+ logDebug("orchestrator", `Ignoring unknown orchestrator selections: ${unknownNames.join(", ")}`);
182
+ }
157
183
 
158
184
  const reasoning = String(obj.reasoning ?? "").trim() || "No reasoning provided";
159
- const skipReason = obj.skipReason as string | undefined;
185
+ const skipReasonRaw = typeof obj.skipReason === "string"
186
+ ? obj.skipReason
187
+ : (typeof obj.skip_reason === "string" ? obj.skip_reason : undefined);
160
188
 
161
189
  return {
162
190
  complexity,
163
191
  category,
164
- selected_agents: selectedAgents as string[],
192
+ selected_agents: selectedAgents,
165
193
  reasoning,
166
- skip_reason: skipReason || undefined,
194
+ skip_reason: skipReasonRaw || undefined,
167
195
  };
168
196
  }
169
197
 
170
198
  protected getCliName(): string {
171
- return "claude";
199
+ return this.agent.provider ?? "claude";
172
200
  }
173
201
 
174
202
  protected makeErrorResult(type: "skip" | "error", message: string): OrchestratorResult {
@@ -179,7 +207,10 @@ Call StructuredOutput now with: complexity, category, selectedAgents, reasoning`
179
207
  }
180
208
 
181
209
  protected parseOutput(raw: string, _result: ExecutionResult): Record<string, unknown> | null {
182
- return parseCliOutput(raw);
210
+ return parseStructuredOutput(raw, {
211
+ requireFields: ["complexity", "category", "reasoning"],
212
+ loggerTag: "orchestrator_parser",
213
+ });
183
214
  }
184
215
 
185
216
  private makeFallback(reasoning: string, error: string): OrchestratorResult {
@@ -434,5 +434,5 @@
434
434
  ]
435
435
  }
436
436
  },
437
- "version": "0.14.0"
437
+ "version": "0.15.1"
438
438
  }