aiwcli 0.13.8 → 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 (47) hide show
  1. package/README.md +11 -1
  2. package/dist/commands/launch.d.ts +8 -0
  3. package/dist/commands/launch.js +96 -5
  4. package/dist/templates/_shared/.claude/skills/codex/SKILL.md +42 -0
  5. package/dist/templates/_shared/.claude/skills/codex/prompt.md +30 -0
  6. package/dist/templates/_shared/lib-ts/agent-exec/backends/headless.ts +33 -0
  7. package/dist/templates/_shared/lib-ts/agent-exec/backends/index.ts +6 -0
  8. package/dist/templates/_shared/lib-ts/agent-exec/backends/tmux.ts +145 -0
  9. package/dist/templates/_shared/lib-ts/agent-exec/base-agent.ts +229 -0
  10. package/dist/templates/_shared/lib-ts/agent-exec/execution-backend.ts +50 -0
  11. package/dist/templates/_shared/lib-ts/agent-exec/index.ts +6 -0
  12. package/dist/templates/_shared/lib-ts/agent-exec/structured-output.ts +166 -0
  13. package/dist/templates/_shared/lib-ts/base/cli-args.ts +287 -0
  14. package/dist/templates/_shared/lib-ts/base/inference.ts +53 -47
  15. package/dist/templates/_shared/lib-ts/base/models.ts +16 -0
  16. package/dist/templates/_shared/lib-ts/base/preflight.ts +98 -0
  17. package/dist/templates/_shared/lib-ts/base/state-io.ts +1 -1
  18. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +4 -3
  19. package/dist/templates/_shared/lib-ts/base/tmux-driver.ts +381 -0
  20. package/dist/templates/_shared/lib-ts/base/utils.ts +8 -0
  21. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +35 -11
  22. package/dist/templates/_shared/lib-ts/context/context-store.ts +3 -0
  23. package/dist/templates/_shared/lib-ts/types.ts +17 -0
  24. package/dist/templates/_shared/scripts/status_line.ts +93 -47
  25. package/dist/templates/_shared/skills/prompt-codex/CLAUDE.md +71 -0
  26. package/dist/templates/_shared/skills/prompt-codex/scripts/launch-codex.ts +387 -0
  27. package/dist/templates/_shared/skills/prompt-codex/scripts/watch-codex.ts +257 -0
  28. package/dist/templates/cc-native/.claude/settings.json +121 -1
  29. package/dist/templates/cc-native/_cc-native/CLAUDE.md +73 -0
  30. package/dist/templates/cc-native/_cc-native/lib-ts/CLAUDE.md +70 -0
  31. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +9 -133
  32. package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +120 -43
  33. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +1 -0
  34. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +66 -12
  35. package/dist/templates/cc-native/_cc-native/plan-review/lib/agent-selection.ts +5 -4
  36. package/dist/templates/cc-native/_cc-native/plan-review/lib/orchestrator.ts +4 -4
  37. package/dist/templates/cc-native/_cc-native/plan-review/lib/preflight.ts +14 -80
  38. package/dist/templates/cc-native/_cc-native/plan-review/lib/review-pipeline.ts +16 -13
  39. package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/agent.ts +19 -7
  40. package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/base/base-agent.ts +4 -215
  41. package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/index.ts +1 -1
  42. package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/providers/claude-agent.ts +9 -39
  43. package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/providers/codex-agent.ts +19 -22
  44. package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/providers/gemini-agent.ts +2 -1
  45. package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/providers/orchestrator-claude-agent.ts +65 -36
  46. package/oclif.manifest.json +21 -3
  47. package/package.json +1 -1
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Execution backend interfaces for CLI agent subprocess invocations.
3
+ * Decouples agent logic (prompt building, output parsing) from execution
4
+ * strategy (headless subprocess vs tmux pane).
5
+ */
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Execution Request / Result
9
+ // ---------------------------------------------------------------------------
10
+
11
+ /** Request to execute a CLI subprocess. */
12
+ export interface ExecutionRequest {
13
+ cliPath: string;
14
+ args: string[];
15
+ input: string;
16
+ env: Record<string, string | undefined>;
17
+ timeoutMs: number;
18
+ /** If set, read output from this file instead of stdout (Codex pattern). */
19
+ outputFilePath?: string;
20
+ maxBuffer?: number;
21
+ shell?: boolean;
22
+ }
23
+
24
+ /** Result from a CLI subprocess execution. */
25
+ export interface ExecutionResult {
26
+ stdout: string;
27
+ stderr: string;
28
+ exitCode: number;
29
+ killed: boolean;
30
+ signal: string | null;
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Execution Backend
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /** Strategy interface for running CLI agent subprocesses. */
38
+ export interface ExecutionBackend {
39
+ execute(request: ExecutionRequest): Promise<ExecutionResult>;
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Debug Logger
44
+ // ---------------------------------------------------------------------------
45
+
46
+ /** Injectable debug logger for agents running in _shared context. */
47
+ export interface AgentDebugLogger {
48
+ log(contextPath: string, sessionName: string, component: string, message: string, data?: unknown): void;
49
+ raw(contextPath: string, sessionName: string, component: string, label: string, raw: string): void;
50
+ }
@@ -0,0 +1,6 @@
1
+ export { BaseCliAgent, type AgentExecutionConfig } from "./base-agent.js";
2
+ export type { ExecutionBackend, ExecutionRequest, ExecutionResult, AgentDebugLogger } from "./execution-backend.js";
3
+ export { HeadlessBackend } from "./backends/headless.js";
4
+ export { TmuxBackend } from "./backends/tmux.js";
5
+ export { parseJsonObjectMaybe, parseStructuredOutput } from "./structured-output.js";
6
+ export type { StructuredOutputParseOptions } from "./structured-output.js";
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Shared structured output parsing utilities for CLI-based agents.
3
+ * Supports Claude/Codex-style envelopes and heuristic JSON extraction.
4
+ */
5
+
6
+ import { logDebug, logError, logWarn } from "../base/logger.js";
7
+
8
+ export interface StructuredOutputParseOptions {
9
+ requireFields?: string[];
10
+ loggerTag?: string;
11
+ }
12
+
13
+ const DEFAULT_LOG_TAG = "structured_output";
14
+
15
+ function getTag(options?: StructuredOutputParseOptions): string {
16
+ return options?.loggerTag ?? DEFAULT_LOG_TAG;
17
+ }
18
+
19
+ function validateRequiredFields(
20
+ obj: Record<string, unknown>,
21
+ parseMethod: "strict" | "heuristic",
22
+ options?: StructuredOutputParseOptions,
23
+ ): Record<string, unknown> | null {
24
+ const required = options?.requireFields;
25
+ if (!required || required.length === 0) return obj;
26
+
27
+ const missing = required.filter((field) => !(field in obj) || obj[field] === undefined || obj[field] === null);
28
+ if (missing.length === 0) return obj;
29
+
30
+ const tag = getTag(options);
31
+ logWarn(tag, `Parsed JSON (${parseMethod}) missing required fields: ${JSON.stringify(missing)}`);
32
+ logDebug(tag, `Parsed keys: ${JSON.stringify(Object.keys(obj))}`);
33
+
34
+ // Heuristic extraction often grabs the wrong JSON blob. Reject in that case.
35
+ if (parseMethod === "heuristic") {
36
+ return null;
37
+ }
38
+ return obj;
39
+ }
40
+
41
+ /**
42
+ * Parse a JSON object from text using strict parse first, then heuristic
43
+ * extraction of the first object-like block.
44
+ */
45
+ export function parseJsonObjectMaybe(
46
+ text: string,
47
+ options?: StructuredOutputParseOptions,
48
+ ): Record<string, unknown> | null {
49
+ const tag = getTag(options);
50
+ const trimmed = text.trim();
51
+ if (!trimmed) return null;
52
+
53
+ // Strict parse first.
54
+ try {
55
+ const parsed: unknown = JSON.parse(trimmed);
56
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
57
+ return validateRequiredFields(parsed as Record<string, unknown>, "strict", options);
58
+ }
59
+ } catch {
60
+ // Fall through to heuristic extraction.
61
+ }
62
+
63
+ // Heuristic parse: extract the first object-like block.
64
+ const start = trimmed.indexOf("{");
65
+ const end = trimmed.lastIndexOf("}");
66
+ if (start === -1 || end === -1 || end <= start) return null;
67
+
68
+ const candidate = trimmed.slice(start, end + 1);
69
+ try {
70
+ const parsed: unknown = JSON.parse(candidate);
71
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
72
+ logDebug(tag, `Used heuristic JSON extraction (chars ${start}-${end})`);
73
+ return validateRequiredFields(parsed as Record<string, unknown>, "heuristic", options);
74
+ }
75
+ } catch {
76
+ logDebug(tag, `Heuristic JSON extraction failed (chars ${start}-${end})`);
77
+ }
78
+
79
+ return null;
80
+ }
81
+
82
+ function parseAssistantEnvelope(
83
+ envelope: Record<string, unknown>,
84
+ options?: StructuredOutputParseOptions,
85
+ ): Record<string, unknown> | null {
86
+ const tag = getTag(options);
87
+ const message = envelope.message;
88
+ if (!message || typeof message !== "object") return null;
89
+
90
+ const content = (message as Record<string, unknown>).content;
91
+ if (!Array.isArray(content)) return null;
92
+
93
+ for (const item of content) {
94
+ if (!item || typeof item !== "object") continue;
95
+ const toolUse = item as Record<string, unknown>;
96
+ if (toolUse.name !== "StructuredOutput") continue;
97
+ if (toolUse.input && typeof toolUse.input === "object" && !Array.isArray(toolUse.input)) {
98
+ logDebug(tag, "Found StructuredOutput in assistant envelope");
99
+ return toolUse.input as Record<string, unknown>;
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+
105
+ /**
106
+ * Parse structured output across known CLI envelope formats.
107
+ * Falls back to generic JSON extraction when no recognized envelope exists.
108
+ */
109
+ export function parseStructuredOutput(
110
+ raw: string,
111
+ options?: StructuredOutputParseOptions,
112
+ ): Record<string, unknown> | null {
113
+ const tag = getTag(options);
114
+
115
+ try {
116
+ const parsed: unknown = JSON.parse(raw);
117
+
118
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
119
+ const obj = parsed as Record<string, unknown>;
120
+
121
+ if (obj.structured_output && typeof obj.structured_output === "object" && !Array.isArray(obj.structured_output)) {
122
+ logDebug(tag, "Found structured_output in root object");
123
+ return validateRequiredFields(obj.structured_output as Record<string, unknown>, "strict", options);
124
+ }
125
+
126
+ const assistantResult = parseAssistantEnvelope(obj, options);
127
+ if (assistantResult) return assistantResult;
128
+
129
+ // Session result envelope (no structured output tool call).
130
+ if (obj.type === "result" || ("duration_ms" in obj && "session_id" in obj)) {
131
+ if (obj.is_error === true || (Array.isArray(obj.errors) && obj.errors.length > 0)) {
132
+ logWarn(tag, `CLI returned error envelope: ${JSON.stringify(obj.errors ?? "is_error=true")}`);
133
+ return null;
134
+ }
135
+
136
+ if (typeof obj.result === "string" && obj.result.trim().length > 0) {
137
+ logDebug(tag, "Found text result in session envelope, attempting JSON extraction");
138
+ const extracted = parseJsonObjectMaybe(obj.result, options);
139
+ if (extracted) return extracted;
140
+ logWarn(tag, "Session envelope result contained no extractable JSON object");
141
+ }
142
+ return null;
143
+ }
144
+ } else if (Array.isArray(parsed)) {
145
+ for (let i = 0; i < parsed.length; i++) {
146
+ const event = parsed[i];
147
+ if (!event || typeof event !== "object") continue;
148
+ const eventObj = event as Record<string, unknown>;
149
+ const assistantResult = parseAssistantEnvelope(eventObj, options);
150
+ if (assistantResult) {
151
+ logDebug(tag, `Found StructuredOutput in event[${i}]`);
152
+ return assistantResult;
153
+ }
154
+ }
155
+ }
156
+ } catch (error: unknown) {
157
+ if (error instanceof SyntaxError) {
158
+ logWarn(tag, `JSON decode error: ${error.message}`);
159
+ } else {
160
+ logError(tag, `Unexpected parse error: ${error}`);
161
+ }
162
+ }
163
+
164
+ logDebug(tag, "No structured envelope found, falling back to generic JSON extraction");
165
+ return parseJsonObjectMaybe(raw, options);
166
+ }
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Centralized CLI argument construction for agent subprocesses.
3
+ * Single source of truth for Claude CLI and Codex CLI flag patterns,
4
+ * platform quoting, model tier resolution, and env setup.
5
+ */
6
+
7
+ import type { PreflightCommandConfig } from "./preflight.js";
8
+ import { getInternalSubprocessEnv, shellQuoteWin } from "./subprocess-utils.js";
9
+ import { CLAUDE_MODELS, CODEX_MODELS } from "./models.js";
10
+
11
+ export { CLAUDE_MODELS, CODEX_MODELS };
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Types
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export type InvocationMode = "structured" | "print" | "preflight";
18
+ export type CliProvider = "claude" | "codex";
19
+ export type ModelTier = "fast" | "standard" | "smart";
20
+
21
+ const VALID_SANDBOXES = ["read-only", "workspace-write", "danger-full-access"] as const;
22
+ export type CodexSandbox = (typeof VALID_SANDBOXES)[number];
23
+
24
+ export function isCodexSandbox(value: string): value is CodexSandbox {
25
+ return (VALID_SANDBOXES as readonly string[]).includes(value);
26
+ }
27
+
28
+ export interface CliArgSpec {
29
+ provider: CliProvider;
30
+ model: string | ModelTier;
31
+ mode: InvocationMode;
32
+ jsonSchema?: Record<string, unknown>;
33
+ maxTurns?: number;
34
+ systemPrompt?: string;
35
+ sandbox?: CodexSandbox;
36
+ outputSchemaPath?: string;
37
+ outputFilePath?: string;
38
+ extraArgs?: string[];
39
+ }
40
+
41
+ /** Codex REPL spec — model optional (Codex uses its default when omitted). */
42
+ export interface CodexReplSpec {
43
+ provider: "codex";
44
+ mode: "repl";
45
+ model?: string | ModelTier;
46
+ sandbox?: CodexSandbox;
47
+ yolo?: boolean;
48
+ extraArgs?: string[];
49
+ }
50
+
51
+ export interface CliInvocation {
52
+ cliName: string;
53
+ args: string[];
54
+ needsShell: boolean;
55
+ env: Record<string, string | undefined>;
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Model Tier Resolution
60
+ // ---------------------------------------------------------------------------
61
+
62
+ export const MODEL_TIERS: Record<ModelTier, string> = {
63
+ fast: CLAUDE_MODELS.haiku,
64
+ standard: CLAUDE_MODELS.sonnet,
65
+ smart: CLAUDE_MODELS.opus,
66
+ };
67
+
68
+ export const CODEX_MODEL_TIERS: Record<ModelTier, string> = {
69
+ fast: CODEX_MODELS.spark,
70
+ standard: CODEX_MODELS.codex,
71
+ smart: CODEX_MODELS.codex,
72
+ };
73
+
74
+ export const TIER_TIMEOUTS: Record<ModelTier, number> = {
75
+ fast: 15,
76
+ standard: 30,
77
+ smart: 90,
78
+ };
79
+
80
+ export function isModelTier(value: string): value is ModelTier {
81
+ return value in MODEL_TIERS;
82
+ }
83
+
84
+ export function resolveModel(model: string | ModelTier): string {
85
+ if (isModelTier(model)) return MODEL_TIERS[model];
86
+ return model;
87
+ }
88
+
89
+ export function resolveModelForProvider(
90
+ model: string | ModelTier,
91
+ provider: CliProvider,
92
+ ): string {
93
+ if (!isModelTier(model)) return model;
94
+ return provider === "codex" ? CODEX_MODEL_TIERS[model] : MODEL_TIERS[model];
95
+ }
96
+
97
+ export function getTierTimeout(tier: ModelTier): number {
98
+ return TIER_TIMEOUTS[tier];
99
+ }
100
+
101
+ /** Resolve a Codex model: tier resolution + pass-through. No aliases (those are skill-specific). */
102
+ export function resolveCodexModel(input: string): string {
103
+ if (isModelTier(input)) return CODEX_MODEL_TIERS[input as ModelTier];
104
+ return input;
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Core Builder
109
+ // ---------------------------------------------------------------------------
110
+
111
+ export function buildCliInvocation(spec: CliArgSpec | CodexReplSpec): CliInvocation {
112
+ const env = getInternalSubprocessEnv();
113
+ delete env.ANTHROPIC_API_KEY;
114
+
115
+ if (spec.mode === "repl") {
116
+ const resolvedModel = spec.model ? resolveModelForProvider(spec.model, spec.provider) : undefined;
117
+ return buildCodexReplInvocation(spec, resolvedModel, env);
118
+ }
119
+
120
+ const resolvedModel = resolveModelForProvider((spec as CliArgSpec).model, spec.provider);
121
+ const isWin = process.platform === "win32";
122
+ const empty = isWin ? '""' : "";
123
+
124
+ if (spec.provider === "claude") {
125
+ return buildClaudeInvocation(spec as CliArgSpec, resolvedModel, isWin, empty, env);
126
+ }
127
+
128
+ return buildCodexInvocation(spec as CliArgSpec, resolvedModel, env);
129
+ }
130
+
131
+ function buildClaudeInvocation(
132
+ spec: CliArgSpec,
133
+ model: string,
134
+ isWin: boolean,
135
+ empty: string,
136
+ env: Record<string, string | undefined>,
137
+ ): CliInvocation {
138
+ const args: string[] = [];
139
+
140
+ args.push("--model", model);
141
+
142
+ if (spec.mode === "print") {
143
+ args.push("--print");
144
+ } else {
145
+ // structured and preflight both use json output
146
+ args.push("--output-format", "json");
147
+
148
+ if (spec.jsonSchema) {
149
+ args.push("--json-schema", shellQuoteWin(JSON.stringify(spec.jsonSchema)));
150
+ }
151
+
152
+ const maxTurns = spec.mode === "preflight" ? 1 : (spec.maxTurns ?? 3);
153
+ args.push("--max-turns", String(maxTurns));
154
+ }
155
+
156
+ args.push("--setting-sources", empty);
157
+ args.push("-p");
158
+ args.push("--no-session-persistence");
159
+
160
+ if (spec.systemPrompt) {
161
+ args.push("--system-prompt", shellQuoteWin(spec.systemPrompt));
162
+ }
163
+
164
+ if (spec.extraArgs) {
165
+ args.push(...spec.extraArgs);
166
+ }
167
+
168
+ return { cliName: "claude", args, needsShell: isWin, env };
169
+ }
170
+
171
+ function buildCodexInvocation(
172
+ spec: CliArgSpec,
173
+ model: string,
174
+ env: Record<string, string | undefined>,
175
+ ): CliInvocation {
176
+ const args: string[] = ["exec"];
177
+
178
+ if (spec.sandbox) {
179
+ args.push("--sandbox", spec.sandbox);
180
+ }
181
+
182
+ args.push("--model", model);
183
+
184
+ if (spec.outputSchemaPath) {
185
+ args.push("--output-schema", spec.outputSchemaPath);
186
+ }
187
+
188
+ if (spec.outputFilePath) {
189
+ args.push("-o", spec.outputFilePath);
190
+ }
191
+
192
+ args.push("-");
193
+
194
+ if (spec.extraArgs) {
195
+ args.push(...spec.extraArgs);
196
+ }
197
+
198
+ return { cliName: "codex", args, needsShell: false, env };
199
+ }
200
+
201
+ function buildCodexReplInvocation(
202
+ spec: CodexReplSpec,
203
+ model: string | undefined,
204
+ env: Record<string, string | undefined>,
205
+ ): CliInvocation {
206
+ const args: string[] = [];
207
+ if (spec.yolo) args.push("--dangerously-bypass-approvals-and-sandbox");
208
+ if (spec.sandbox) args.push("--sandbox", spec.sandbox);
209
+ if (model) args.push("--model", model);
210
+ if (spec.extraArgs) args.push(...spec.extraArgs);
211
+ return { cliName: "codex", args, needsShell: false, env };
212
+ }
213
+
214
+ // ---------------------------------------------------------------------------
215
+ // Convenience Presets
216
+ // ---------------------------------------------------------------------------
217
+
218
+ export function preflightSpec(provider: CliProvider, model: string): CliArgSpec {
219
+ if (provider === "codex") {
220
+ return {
221
+ provider: "codex",
222
+ model,
223
+ mode: "preflight",
224
+ sandbox: "read-only",
225
+ };
226
+ }
227
+ return {
228
+ provider: "claude",
229
+ model,
230
+ mode: "preflight",
231
+ };
232
+ }
233
+
234
+ export function inferenceSpec(model: string | ModelTier): CliArgSpec {
235
+ return {
236
+ provider: "claude",
237
+ model,
238
+ mode: "print",
239
+ };
240
+ }
241
+
242
+ export function reviewSpec(
243
+ provider: CliProvider,
244
+ model: string,
245
+ schema: Record<string, unknown>,
246
+ systemPrompt?: string,
247
+ ): CliArgSpec {
248
+ if (provider === "codex") {
249
+ return {
250
+ provider: "codex",
251
+ model,
252
+ mode: "structured",
253
+ sandbox: "read-only",
254
+ };
255
+ }
256
+ return {
257
+ provider: "claude",
258
+ model,
259
+ mode: "structured",
260
+ jsonSchema: schema,
261
+ systemPrompt,
262
+ };
263
+ }
264
+
265
+ export function codexReplSpec(
266
+ model?: string,
267
+ sandbox?: CodexSandbox,
268
+ yolo?: boolean,
269
+ ): CodexReplSpec {
270
+ return {
271
+ provider: "codex",
272
+ mode: "repl",
273
+ model,
274
+ sandbox,
275
+ yolo,
276
+ };
277
+ }
278
+
279
+ export function preflightCommandConfig(provider: CliProvider): PreflightCommandConfig {
280
+ const input = "Respond with exactly: ok";
281
+
282
+ return {
283
+ cliName: provider === "claude" ? "claude" : "codex",
284
+ buildArgs: (model: string) => buildCliInvocation(preflightSpec(provider, model)).args,
285
+ input,
286
+ };
287
+ }
@@ -9,20 +9,18 @@ import { execFileSync } from "node:child_process";
9
9
  import { logDebug, logWarn } from "./logger.js";
10
10
  import { STOP_WORDS } from "./stop-words.js";
11
11
  import type { InferenceResult } from "../types.js";
12
- import { execFileAsync, getInternalSubprocessEnv, shellQuoteWin } from "./subprocess-utils.js";
13
-
14
- // Model configurations §6.1
15
- const MODELS: Record<string, string> = {
16
- fast: "claude-haiku-4-5-20251001",
17
- standard: "claude-sonnet-4-6",
18
- smart: "claude-opus-4-6",
19
- };
20
-
21
- const TIMEOUTS: Record<string, number> = {
22
- fast: 15,
23
- standard: 30,
24
- smart: 90,
25
- };
12
+ import { execFileAsync } from "./subprocess-utils.js";
13
+ import {
14
+ buildCliInvocation,
15
+ inferenceSpec,
16
+ isModelTier,
17
+ resolveModel,
18
+ getTierTimeout,
19
+ TIER_TIMEOUTS,
20
+ } from "./cli-args.js";
21
+ import { CODEX_MODELS } from "./models.js";
22
+
23
+ const CONTEXT_ID_PRIMARY_MODEL = CODEX_MODELS.spark;
26
24
 
27
25
  /**
28
26
  * Run inference using the claude CLI.
@@ -33,38 +31,33 @@ export function inference(
33
31
  userPrompt: string,
34
32
  level = "fast",
35
33
  timeout?: number,
34
+ options?: { model?: string },
36
35
  ): InferenceResult {
37
36
  const startTime = Date.now();
38
- const model = MODELS[level] ?? MODELS.fast;
39
- const timeoutSec = timeout ?? TIMEOUTS[level] ?? TIMEOUTS.fast;
37
+ const modelInput = options?.model ?? level;
38
+ const model = resolveModel(modelInput);
39
+ const timeoutSec = timeout ?? (isModelTier(modelInput) ? getTierTimeout(modelInput) : TIER_TIMEOUTS.fast);
40
40
  const fullPrompt = `${systemPrompt}\n\n${userPrompt}`;
41
41
 
42
- // Remove ANTHROPIC_API_KEY to force subscription auth
43
- const env = { ...process.env };
44
- delete env.ANTHROPIC_API_KEY;
42
+ const invocation = buildCliInvocation(inferenceSpec(modelInput));
43
+ const isWin = invocation.needsShell;
45
44
 
46
- try {
47
- const isWin = process.platform === "win32";
48
-
49
- // On Windows with shell:true, Node.js sets windowsVerbatimArguments
50
- // args are joined with spaces, NOT individually quoted. We must manually
51
- // wrap multi-word/special-char args in "..." for cmd.exe parsing.
52
- // Inside double quotes: "" = literal ", and |&<> are safe.
53
- const empty = isWin ? '""' : "";
54
- let promptArg = fullPrompt;
55
- if (isWin) {
56
- promptArg = '"' + fullPrompt.replaceAll(/\r?\n/g, " ").replaceAll('"', '""') + '"';
57
- }
45
+ // Prompt arg needs Windows quoting when using shell mode
46
+ let promptArg = fullPrompt;
47
+ if (isWin) {
48
+ promptArg = '"' + fullPrompt.replaceAll(/\r?\n/g, " ").replaceAll('"', '""') + '"';
49
+ }
58
50
 
51
+ try {
59
52
  const stdout = execFileSync(
60
- "claude",
61
- ["--model", model, "--print", "--setting-sources", empty, "-p", "--no-session-persistence", promptArg],
53
+ invocation.cliName,
54
+ [...invocation.args, promptArg],
62
55
  {
63
56
  timeout: timeoutSec * 1000,
64
- env,
57
+ env: invocation.env,
65
58
  encoding: "utf-8",
66
59
  stdio: ["pipe", "pipe", "pipe"],
67
- shell: isWin, // Windows needs shell for .cmd resolution
60
+ shell: isWin,
68
61
  },
69
62
  );
70
63
 
@@ -189,7 +182,7 @@ Respond with ONLY a JSON object: {"slug": "your 8-12 word phrase here"}`;
189
182
 
190
183
  /**
191
184
  * Generate a 5-12 word context ID slug from a user prompt.
192
- * Uses Haiku (fast tier) for low latency.
185
+ * Uses 5.3 Codex Spark first, then falls back to current fast tier for resilience.
193
186
  * See SPEC.md §6.3
194
187
  */
195
188
  export function generateContextIdSlug(
@@ -198,7 +191,20 @@ export function generateContextIdSlug(
198
191
  ): string | null {
199
192
  const truncated = prompt.slice(0, 500);
200
193
 
201
- const result = inference(CONTEXT_ID_SLUG_PROMPT, truncated, "fast", timeout);
194
+ const sparkResult = inference(
195
+ CONTEXT_ID_SLUG_PROMPT,
196
+ truncated,
197
+ "fast",
198
+ timeout,
199
+ { model: CONTEXT_ID_PRIMARY_MODEL },
200
+ );
201
+ if (!sparkResult.success || !sparkResult.output) {
202
+ logWarn(
203
+ "inference",
204
+ `Context ID slug Spark (${CONTEXT_ID_PRIMARY_MODEL}) failed or returned empty output. Falling back to ${resolveModel("fast")}`,
205
+ );
206
+ }
207
+ const result = sparkResult.success && sparkResult.output ? sparkResult : inference(CONTEXT_ID_SLUG_PROMPT, truncated, "fast", timeout);
202
208
 
203
209
  if (!result.success || !result.output) {
204
210
  logWarn("inference", `Context ID slug inference failed: ${result.error}`);
@@ -250,26 +256,26 @@ export async function inferenceAsync(
250
256
  userPrompt: string,
251
257
  level = "fast",
252
258
  timeout?: number,
259
+ options?: { model?: string },
253
260
  ): Promise<InferenceResult> {
254
261
  const startTime = Date.now();
255
- const model = (level in MODELS ? MODELS[level] : undefined) ?? MODELS.fast;
256
- const timeoutSec = timeout ?? (level in TIMEOUTS ? TIMEOUTS[level] : undefined) ?? TIMEOUTS.fast;
262
+ const modelInput = options?.model ?? level;
263
+ const timeoutSec = timeout ?? (isModelTier(modelInput) ? getTierTimeout(modelInput) : TIER_TIMEOUTS.fast);
257
264
  const timeoutMs = timeoutSec * 1000;
258
265
  const fullPrompt = `${systemPrompt}\n\n${userPrompt}`;
259
266
 
260
- const env = getInternalSubprocessEnv();
261
- delete env.ANTHROPIC_API_KEY;
267
+ const invocation = buildCliInvocation(inferenceSpec(modelInput));
268
+ const isWin = invocation.needsShell;
262
269
 
263
- const isWin = process.platform === "win32";
264
- const empty = isWin ? '""' : "";
270
+ // Prompt arg needs Windows quoting when using shell mode
265
271
  const promptArg = isWin
266
- ? shellQuoteWin(fullPrompt.replaceAll(/\r?\n/g, " "))
272
+ ? ('"' + fullPrompt.replaceAll(/\r?\n/g, " ").replaceAll('"', '""') + '"')
267
273
  : fullPrompt;
268
274
 
269
275
  const result = await execFileAsync(
270
- "claude",
271
- ["--model", model, "--print", "--setting-sources", empty, "-p", "--no-session-persistence", promptArg],
272
- { timeout: timeoutMs, env, shell: isWin },
276
+ invocation.cliName,
277
+ [...invocation.args, promptArg],
278
+ { timeout: timeoutMs, env: invocation.env, shell: isWin },
273
279
  );
274
280
 
275
281
  const latencyMs = Date.now() - startTime;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Canonical model ID constants — single source of truth.
3
+ * All model IDs used across the system should reference these constants.
4
+ */
5
+
6
+ export const CLAUDE_MODELS = {
7
+ haiku: "claude-haiku-4-5-20251001",
8
+ sonnet: "claude-sonnet-4-6",
9
+ opus: "claude-opus-4-6",
10
+ } as const;
11
+
12
+ export const CODEX_MODELS = {
13
+ spark: "gpt-5.3-codex-spark",
14
+ codex: "gpt-5.3-codex",
15
+ gpt: "gpt-5.2",
16
+ } as const;