aiwcli 0.13.6 → 0.13.8

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.
@@ -80,6 +80,8 @@
80
80
  "allow": [],
81
81
  "deny": []
82
82
  },
83
- "env": {},
83
+ "env": {
84
+ "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
85
+ },
84
86
  "enabledPlugins": {}
85
87
  }
@@ -7,7 +7,8 @@
7
7
  },
8
8
  "codex": {
9
9
  "enabled": true,
10
- "models": ["codex-mini-latest"]
10
+ "models": ["gpt-5.3-codex"],
11
+ "reasoningEffort": "low"
11
12
  }
12
13
  }
13
14
  },
@@ -29,6 +30,10 @@
29
30
  "model": "opus",
30
31
  "timeout": 60
31
32
  },
33
+ "preflight": {
34
+ "enabled": true,
35
+ "timeoutMs": 15000
36
+ },
32
37
  "legacyMode": false,
33
38
  "mandatoryAgents": {
34
39
  "always": ["handoff-readiness", "clarity-auditor", "skeptic"],
@@ -70,8 +70,8 @@ export const DEFAULT_COMPLEXITY_CATEGORIES = ["code", "infrastructure", "documen
70
70
 
71
71
  export const DEFAULT_MODELS_CONFIG: ModelsConfig = {
72
72
  providers: {
73
- claude: { enabled: true, models: ["sonnet"] },
74
- codex: { enabled: true, models: ["codex-mini-latest"] },
73
+ claude: { enabled: false, models: ["sonnet"] },
74
+ codex: { enabled: true, models: ["gpt-5.2"] },
75
75
  },
76
76
  };
77
77
 
@@ -149,6 +149,7 @@ export function loadModelsConfig(settings: Record<string, unknown>): ModelsConfi
149
149
  providers[name] = {
150
150
  enabled: c.enabled !== false,
151
151
  models: Array.isArray(c.models) ? (c.models as string[]).filter(Boolean) : [],
152
+ ...(typeof c.reasoningEffort === "string" && { reasoningEffort: c.reasoningEffort }),
152
153
  };
153
154
  }
154
155
  return { providers };
@@ -127,6 +127,7 @@ export interface AgentConfig {
127
127
  categories: string[];
128
128
  description: string;
129
129
  system_prompt: string; // Markdown body content for --system-prompt
130
+ reasoningEffort?: string; // e.g. "low", "medium", "high" — passed to codex -c model_reasoning_effort
130
131
  }
131
132
 
132
133
  /** Configuration for the plan orchestrator */
@@ -140,6 +141,7 @@ export interface OrchestratorConfig {
140
141
  export interface ProviderConfig {
141
142
  enabled: boolean;
142
143
  models: string[];
144
+ reasoningEffort?: string; // e.g. "low", "medium", "high" — codex model_reasoning_effort
143
145
  }
144
146
 
145
147
  /** Model provider pool configuration */
@@ -327,3 +329,24 @@ export interface IterationAdvancement {
327
329
  updatedState: IterationState;
328
330
  newGraduates: string[];
329
331
  }
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // Preflight Types
335
+ // ---------------------------------------------------------------------------
336
+
337
+ /** Result from a single provider+model preflight check */
338
+ export interface PreflightCheckResult {
339
+ provider: string;
340
+ model: string;
341
+ available: boolean;
342
+ latencyMs: number;
343
+ error?: string;
344
+ }
345
+
346
+ /** Aggregated preflight report across all provider+model combos */
347
+ export interface PreflightReport {
348
+ checks: PreflightCheckResult[];
349
+ available: Map<string, Set<string>>; // provider → set of working models
350
+ allFailed: boolean;
351
+ totalMs: number;
352
+ }
@@ -47,38 +47,70 @@ export function resolveMandatoryAgents(
47
47
  // Model Assignment
48
48
  // ---------------------------------------------------------------------------
49
49
 
50
+ /** Provider priority order: codex first (cheaper/faster), claude as fallback */
51
+ const PROVIDER_PRIORITY = ["codex", "claude"];
52
+
50
53
  /**
51
- * Randomly assign enabled providers and models to agents.
52
- * Filters to providers whose CLI is available on PATH.
54
+ * Assign providers and models to agents.
55
+ * When preflightAvailable is provided, filters to only models that passed preflight.
56
+ * Providers are ordered by PROVIDER_PRIORITY (codex first, claude fallback).
57
+ * All agents get the first available provider; random model within that provider.
53
58
  */
54
59
  export function assignModelsToAgents(
55
60
  agents: AgentConfig[],
56
61
  modelsConfig: ModelsConfig,
62
+ preflightAvailable?: Map<string, Set<string>>,
57
63
  ): AgentConfig[] {
58
- const enabledProviders = Object.entries(modelsConfig.providers)
64
+ let enabledProviders = Object.entries(modelsConfig.providers)
59
65
  .filter(([name, config]) => {
60
66
  if (!config.enabled || config.models.length === 0) return false;
61
67
  const cliName = name === "claude" ? "claude" : name;
62
68
  const found = findExecutable(cliName);
63
69
  if (!found) {
64
70
  logWarn(HOOK, `Provider '${name}' enabled but CLI '${cliName}' not found on PATH — skipping`);
71
+ return false;
72
+ }
73
+ return true;
74
+ })
75
+ .map(([name, config]) => {
76
+ // Filter models by preflight results when available
77
+ if (preflightAvailable) {
78
+ const passedModels = preflightAvailable.get(name);
79
+ if (!passedModels || passedModels.size === 0) {
80
+ logWarn(HOOK, `Provider '${name}' has no models that passed preflight — skipping`);
81
+ return null;
82
+ }
83
+ const filteredModels = config.models.filter(m => passedModels.has(m));
84
+ if (filteredModels.length === 0) {
85
+ logWarn(HOOK, `Provider '${name}': none of its configured models passed preflight — skipping`);
86
+ return null;
87
+ }
88
+ return [name, { ...config, models: filteredModels }] as [string, typeof config];
65
89
  }
66
- return Boolean(found);
67
- });
90
+ return [name, config] as [string, typeof config];
91
+ })
92
+ .filter((entry): entry is [string, { enabled: boolean; models: string[]; reasoningEffort?: string }] => entry !== null);
93
+
94
+ // Sort by provider priority (codex first)
95
+ enabledProviders.sort((a, b) => {
96
+ const aIdx = PROVIDER_PRIORITY.indexOf(a[0]);
97
+ const bIdx = PROVIDER_PRIORITY.indexOf(b[0]);
98
+ return (aIdx === -1 ? 999 : aIdx) - (bIdx === -1 ? 999 : bIdx);
99
+ });
68
100
 
69
101
  if (enabledProviders.length === 0) {
70
102
  logWarn(HOOK, "No providers with available CLI found, falling back to Claude with agent defaults");
71
103
  return agents.map(a => ({ ...a, provider: "claude" }));
72
104
  }
73
105
 
106
+ // Assign all agents to the first (highest-priority) available provider
107
+ const [providerName, providerConfig] = enabledProviders[0]!;
108
+ logInfo(HOOK, `Using provider: ${providerName} (models: ${providerConfig.models.join(", ")})`);
109
+
74
110
  return agents.map(agent => {
75
- const idx = Math.floor(Math.random() * enabledProviders.length);
76
- const entry = enabledProviders[idx];
77
- if (!entry) return { ...agent, provider: "claude" };
78
- const [providerName, providerConfig] = entry;
79
111
  const modelIdx = Math.floor(Math.random() * providerConfig.models.length);
80
112
  const model = providerConfig.models[modelIdx] ?? providerConfig.models[0] ?? agent.model;
81
- return { ...agent, provider: providerName, model };
113
+ return { ...agent, provider: providerName, model, reasoningEffort: providerConfig.reasoningEffort };
82
114
  });
83
115
  }
84
116
 
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Preflight health checks for plan review agents.
3
+ * Validates provider+model combos work before committing agents to them.
4
+ * Runs minimal "ping" requests in parallel per unique provider:model combo.
5
+ */
6
+
7
+ import { logInfo, logWarn, logDebug } from "../../../_shared/lib-ts/base/logger.js";
8
+ import { findExecutable, execFileAsync, getInternalSubprocessEnv } from "../../../_shared/lib-ts/base/subprocess-utils.js";
9
+ import type { ModelsConfig, PreflightCheckResult, PreflightReport } from "../../lib-ts/types.js";
10
+ import { claudePreflightArgs, CLAUDE_PREFLIGHT_INPUT } from "./reviewers/providers/claude-agent.js";
11
+ import { codexPreflightArgs, CODEX_PREFLIGHT_INPUT } from "./reviewers/providers/codex-agent.js";
12
+
13
+ const HOOK = "preflight";
14
+ const DEFAULT_TIMEOUT_MS = 15000;
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Provider Registry
18
+ // ---------------------------------------------------------------------------
19
+
20
+ interface PreflightCommandConfig {
21
+ cliName: string;
22
+ buildArgs: (model: string) => string[];
23
+ input: string;
24
+ }
25
+
26
+ const PREFLIGHT_COMMANDS: Record<string, PreflightCommandConfig> = {
27
+ claude: { cliName: "claude", buildArgs: claudePreflightArgs, input: CLAUDE_PREFLIGHT_INPUT },
28
+ codex: { cliName: "codex", buildArgs: codexPreflightArgs, input: CODEX_PREFLIGHT_INPUT },
29
+ };
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Error Classification
33
+ // ---------------------------------------------------------------------------
34
+
35
+ function classifyError(stderr: string, exitCode: number | null, killed: boolean, signal: string | null): string {
36
+ if (killed || signal === "SIGTERM") return "Preflight timed out";
37
+ if (/model.*not found|not available/i.test(stderr)) return "Model not available for this account";
38
+ if (/rate limit|429/i.test(stderr)) return "Rate limited";
39
+ if (/auth|api key|401/i.test(stderr)) return "Authentication failed";
40
+ if (/quota|billing/i.test(stderr)) return "Quota/billing issue";
41
+ return `Exit code ${exitCode}`;
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Single Check
46
+ // ---------------------------------------------------------------------------
47
+
48
+ async function checkProviderModel(
49
+ provider: string,
50
+ model: string,
51
+ timeoutMs: number,
52
+ ): Promise<PreflightCheckResult> {
53
+ const config = PREFLIGHT_COMMANDS[provider];
54
+ if (!config) {
55
+ return { provider, model, available: false, latencyMs: 0, error: `Unknown provider: ${provider}` };
56
+ }
57
+
58
+ const cliPath = findExecutable(config.cliName);
59
+ if (!cliPath) {
60
+ return { provider, model, available: false, latencyMs: 0, error: `CLI '${config.cliName}' not found on PATH` };
61
+ }
62
+
63
+ const start = Date.now();
64
+ try {
65
+ const env = getInternalSubprocessEnv();
66
+ const result = await execFileAsync(cliPath, config.buildArgs(model), {
67
+ input: config.input,
68
+ timeout: timeoutMs,
69
+ env: env as Record<string, string>,
70
+ maxBuffer: 1 * 1024 * 1024,
71
+ shell: process.platform === "win32",
72
+ });
73
+
74
+ const latencyMs = Date.now() - start;
75
+
76
+ if (result.killed || result.signal === "SIGTERM") {
77
+ return { provider, model, available: false, latencyMs, error: "Preflight timed out" };
78
+ }
79
+
80
+ if (result.exitCode !== 0) {
81
+ const error = classifyError(result.stderr, result.exitCode, result.killed, result.signal);
82
+ logWarn(HOOK, `${provider}:${model} failed: ${error} (stderr: ${result.stderr.slice(-200)})`);
83
+ return { provider, model, available: false, latencyMs, error };
84
+ }
85
+
86
+ logDebug(HOOK, `${provider}:${model} passed (${latencyMs}ms)`);
87
+ return { provider, model, available: true, latencyMs };
88
+ } catch (err) {
89
+ const latencyMs = Date.now() - start;
90
+ const error = err instanceof Error ? err.message : String(err);
91
+ logWarn(HOOK, `${provider}:${model} exception: ${error}`);
92
+ return { provider, model, available: false, latencyMs, error };
93
+ }
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Run All Checks
98
+ // ---------------------------------------------------------------------------
99
+
100
+ export async function runPreflight(
101
+ modelsConfig: ModelsConfig,
102
+ timeoutMs?: number,
103
+ ): Promise<PreflightReport> {
104
+ const effectiveTimeout = timeoutMs ?? DEFAULT_TIMEOUT_MS;
105
+ const start = Date.now();
106
+
107
+ // Collect unique provider:model combos from enabled providers
108
+ const checks: Array<{ provider: string; model: string }> = [];
109
+ const seen = new Set<string>();
110
+
111
+ for (const [provider, config] of Object.entries(modelsConfig.providers)) {
112
+ if (!config.enabled || config.models.length === 0) continue;
113
+ if (!PREFLIGHT_COMMANDS[provider]) {
114
+ logWarn(HOOK, `No preflight command for provider '${provider}', skipping`);
115
+ continue;
116
+ }
117
+ for (const model of config.models) {
118
+ const key = `${provider}:${model}`;
119
+ if (!seen.has(key)) {
120
+ seen.add(key);
121
+ checks.push({ provider, model });
122
+ }
123
+ }
124
+ }
125
+
126
+ if (checks.length === 0) {
127
+ logWarn(HOOK, "No provider:model combos to check");
128
+ return { checks: [], available: new Map(), allFailed: true, totalMs: Date.now() - start };
129
+ }
130
+
131
+ logInfo(HOOK, `Checking ${checks.length} provider:model combo(s): ${checks.map(c => `${c.provider}:${c.model}`).join(", ")}`);
132
+
133
+ // Run all checks in parallel
134
+ const results = await Promise.all(
135
+ checks.map(({ provider, model }) => checkProviderModel(provider, model, effectiveTimeout)),
136
+ );
137
+
138
+ // Build available map
139
+ const available = new Map<string, Set<string>>();
140
+ for (const r of results) {
141
+ if (r.available) {
142
+ if (!available.has(r.provider)) available.set(r.provider, new Set());
143
+ available.get(r.provider)!.add(r.model);
144
+ }
145
+ }
146
+
147
+ const allFailed = available.size === 0;
148
+ const totalMs = Date.now() - start;
149
+
150
+ // Log summary
151
+ const passed = results.filter(r => r.available).length;
152
+ const failed = results.filter(r => !r.available).length;
153
+ logInfo(HOOK, `Preflight complete: ${passed} passed, ${failed} failed (${totalMs}ms)`);
154
+
155
+ for (const r of results) {
156
+ if (!r.available) {
157
+ logWarn(HOOK, ` FAIL ${r.provider}:${r.model} — ${r.error}`);
158
+ }
159
+ }
160
+
161
+ return { checks: results, available, allFailed, totalMs };
162
+ }
@@ -8,6 +8,7 @@ import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
9
 
10
10
  import { resolveMandatoryAgents, assignModelsToAgents, selectAgents } from "./agent-selection.js";
11
+ import { runPreflight } from "./preflight.js";
11
12
  import { computeCorroboratedDecision } from "./corroboration.js";
12
13
  import { computePassEligible, extractTopIssuesForTracker, advanceIterationState } from "./graduation.js";
13
14
  import { runOrchestrator } from "./orchestrator.js";
@@ -225,6 +226,28 @@ export async function runReviewPipeline(input: PipelineInput): Promise<PipelineR
225
226
  const agentResults: Record<string, ReviewerResult> = {};
226
227
  let detectedComplexity = "medium";
227
228
 
229
+ // Preflight: validate provider+model combos before committing agents or orchestrator
230
+ const preflightEnabled = (agentSettings.preflight as Record<string, unknown>)?.enabled ?? true;
231
+ let preflightAvailable: Map<string, Set<string>> | undefined;
232
+
233
+ if (preflightEnabled && agentReviewEnabled) {
234
+ logInfo(HOOK, "=== PREFLIGHT: Checking provider availability ===");
235
+ const preflightTimeoutMs = (agentSettings.preflight as Record<string, unknown>)?.timeoutMs as number | undefined;
236
+ const modelsConfig = loadModelsConfig(settings);
237
+ const preflightReport = await runPreflight(modelsConfig, preflightTimeoutMs);
238
+
239
+ if (preflightReport.allFailed) {
240
+ logWarn(HOOK, "All providers failed preflight checks");
241
+ // Preflight failures skip review rather than block because an unavailable
242
+ // reviewer is worse than no reviewer. A permanently broken config will
243
+ // silently pass all plans — mitigated by the log warnings above.
244
+ eprint("[plan-review] All AI providers unavailable. Skipping review.");
245
+ return { action: "skip", reason: "No AI providers passed preflight. Check CLI, API keys, model access, and quota." };
246
+ }
247
+
248
+ preflightAvailable = preflightReport.available;
249
+ }
250
+
228
251
  // 7. PHASE 1: Orchestrator
229
252
  const graduatedSet = new Set(iterationState.graduated);
230
253
  if (graduatedSet.size > 0) {
@@ -254,10 +277,20 @@ export async function runReviewPipeline(input: PipelineInput): Promise<PipelineR
254
277
  const phase1Promises: Array<{ name: string; promise: Promise<ReviewerResult | OrchestratorResult> }> = [];
255
278
 
256
279
  if (orchestratorConfig.enabled && enabledAgents.length > 0 && !legacyMode) {
257
- phase1Promises.push({
258
- name: "orchestrator",
259
- promise: runOrchestrator(plan, enabledAgents, orchestratorConfig, agentSettings, alwaysMandatory),
260
- });
280
+ // Guard orchestrator against preflight failures (always uses claude provider)
281
+ const orchProvider = "claude";
282
+ const orchModel = orchestratorConfig.model;
283
+ const orchPassed = !preflightAvailable ||
284
+ (preflightAvailable.has(orchProvider) && preflightAvailable.get(orchProvider)!.has(orchModel));
285
+
286
+ if (!orchPassed) {
287
+ logWarn(HOOK, `Orchestrator model ${orchProvider}:${orchModel} failed preflight, skipping`);
288
+ } else {
289
+ phase1Promises.push({
290
+ name: "orchestrator",
291
+ promise: runOrchestrator(plan, enabledAgents, orchestratorConfig, agentSettings, alwaysMandatory),
292
+ });
293
+ }
261
294
  }
262
295
 
263
296
  logInfo(HOOK, `=== PHASE 1: Running ${phase1Promises.length} tasks in parallel ===`);
@@ -318,9 +351,9 @@ export async function runReviewPipeline(input: PipelineInput): Promise<PipelineR
318
351
  iterationState.max = reviewIterations[detectedComplexity] ?? iterationState.max;
319
352
  logDebug(HOOK, `Iteration state: ${iterationState.current}/${iterationState.max} (${detectedComplexity})`);
320
353
 
321
- // Assign random providers + models
354
+ // Assign providers + models (filtered by preflight results if available)
322
355
  const modelsConfig = loadModelsConfig(settings);
323
- selectedAgents = assignModelsToAgents(selectedAgents, modelsConfig);
356
+ selectedAgents = assignModelsToAgents(selectedAgents, modelsConfig, preflightAvailable);
324
357
  logInfo(HOOK, `Model assignments: ${selectedAgents.map(a => `${a.name}→${a.provider}:${a.model}`).join(", ")}`);
325
358
 
326
359
  // 9. PHASE 3: Run agents
@@ -15,6 +15,27 @@ import { makeResult } from "../types.js";
15
15
  * Claude CLI-based agent reviewer.
16
16
  * Extends BaseCliAgent with Claude-specific prompt and argument handling.
17
17
  */
18
+ // ---------------------------------------------------------------------------
19
+ // Preflight (standalone — no instance needed)
20
+ // ---------------------------------------------------------------------------
21
+
22
+ export const CLAUDE_PREFLIGHT_INPUT = "Respond with exactly: ok";
23
+
24
+ export function claudePreflightArgs(model: string): string[] {
25
+ return [
26
+ "--model", model,
27
+ "--max-turns", "1",
28
+ "--output-format", "json",
29
+ "--setting-sources", process.platform === "win32" ? '""' : "",
30
+ "-p",
31
+ "--no-session-persistence",
32
+ ];
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Agent Class
37
+ // ---------------------------------------------------------------------------
38
+
18
39
  export class ClaudeAgent extends BaseCliAgent<ReviewerResult> {
19
40
  protected buildCliArgs(): string[] {
20
41
  const schemaJson = JSON.stringify(this.schema);
@@ -16,6 +16,20 @@ import { BaseCliAgent, type ExecResult } from "../base/base-agent.js";
16
16
  import { AGENT_REVIEW_PROMPT_PREFIX } from "../schemas.js";
17
17
  import { makeResult } from "../types.js";
18
18
 
19
+ // ---------------------------------------------------------------------------
20
+ // Preflight (standalone — no instance needed)
21
+ // ---------------------------------------------------------------------------
22
+
23
+ export const CODEX_PREFLIGHT_INPUT = "Respond with exactly: ok";
24
+
25
+ export function codexPreflightArgs(model: string): string[] {
26
+ return ["exec", "--sandbox", "read-only", "--model", model, "-"];
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Agent Class
31
+ // ---------------------------------------------------------------------------
32
+
19
33
  /** Temp directory for Codex schema/output files */
20
34
  const _tmpDir: string | null = null;
21
35
 
@@ -40,6 +54,7 @@ export class CodexAgent extends BaseCliAgent<ReviewerResult> {
40
54
 
41
55
  const cmdArgs = ["exec", "--sandbox", "read-only"];
42
56
  if (this.agent.model) cmdArgs.push("--model", this.agent.model);
57
+ if (this.agent.reasoningEffort) cmdArgs.push("-c", `model_reasoning_effort="${this.agent.reasoningEffort}"`);
43
58
  cmdArgs.push("--output-schema", normalizedSchema, "-o", normalizedOut, "-");
44
59
 
45
60
  return cmdArgs;
@@ -416,5 +416,5 @@
416
416
  ]
417
417
  }
418
418
  },
419
- "version": "0.13.6"
419
+ "version": "0.13.8"
420
420
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aiwcli",
3
3
  "description": "AI Workflow CLI - Command-line interface for AI-powered workflows",
4
- "version": "0.13.6",
4
+ "version": "0.13.8",
5
5
  "author": "jofu-tofu",
6
6
  "bin": {
7
7
  "aiw": "bin/run.js"