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.
- package/README.md +11 -1
- package/dist/commands/launch.d.ts +8 -0
- package/dist/commands/launch.js +96 -5
- package/dist/templates/_shared/.claude/skills/codex/SKILL.md +42 -0
- package/dist/templates/_shared/.claude/skills/codex/prompt.md +30 -0
- package/dist/templates/_shared/lib-ts/agent-exec/backends/headless.ts +33 -0
- package/dist/templates/_shared/lib-ts/agent-exec/backends/index.ts +6 -0
- package/dist/templates/_shared/lib-ts/agent-exec/backends/tmux.ts +145 -0
- package/dist/templates/_shared/lib-ts/agent-exec/base-agent.ts +229 -0
- package/dist/templates/_shared/lib-ts/agent-exec/execution-backend.ts +50 -0
- package/dist/templates/_shared/lib-ts/agent-exec/index.ts +6 -0
- package/dist/templates/_shared/lib-ts/agent-exec/structured-output.ts +166 -0
- package/dist/templates/_shared/lib-ts/base/cli-args.ts +287 -0
- package/dist/templates/_shared/lib-ts/base/inference.ts +53 -47
- package/dist/templates/_shared/lib-ts/base/models.ts +16 -0
- package/dist/templates/_shared/lib-ts/base/preflight.ts +98 -0
- package/dist/templates/_shared/lib-ts/base/state-io.ts +1 -1
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +4 -3
- package/dist/templates/_shared/lib-ts/base/tmux-driver.ts +381 -0
- package/dist/templates/_shared/lib-ts/base/utils.ts +8 -0
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +35 -11
- package/dist/templates/_shared/lib-ts/context/context-store.ts +3 -0
- package/dist/templates/_shared/lib-ts/types.ts +17 -0
- package/dist/templates/_shared/scripts/status_line.ts +93 -47
- package/dist/templates/_shared/skills/prompt-codex/CLAUDE.md +71 -0
- package/dist/templates/_shared/skills/prompt-codex/scripts/launch-codex.ts +387 -0
- package/dist/templates/_shared/skills/prompt-codex/scripts/watch-codex.ts +257 -0
- package/dist/templates/cc-native/.claude/settings.json +121 -1
- package/dist/templates/cc-native/_cc-native/CLAUDE.md +73 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/CLAUDE.md +70 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +9 -133
- package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +120 -43
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +1 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +66 -12
- package/dist/templates/cc-native/_cc-native/plan-review/lib/agent-selection.ts +5 -4
- package/dist/templates/cc-native/_cc-native/plan-review/lib/orchestrator.ts +4 -4
- package/dist/templates/cc-native/_cc-native/plan-review/lib/preflight.ts +14 -80
- package/dist/templates/cc-native/_cc-native/plan-review/lib/review-pipeline.ts +16 -13
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/agent.ts +19 -7
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/base/base-agent.ts +4 -215
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/index.ts +1 -1
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/providers/claude-agent.ts +9 -39
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/providers/codex-agent.ts +19 -22
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/providers/gemini-agent.ts +2 -1
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/providers/orchestrator-claude-agent.ts +65 -36
- package/oclif.manifest.json +21 -3
- 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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
39
|
-
const
|
|
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
|
-
|
|
43
|
-
const
|
|
44
|
-
delete env.ANTHROPIC_API_KEY;
|
|
42
|
+
const invocation = buildCliInvocation(inferenceSpec(modelInput));
|
|
43
|
+
const isWin = invocation.needsShell;
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
61
|
-
[
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
256
|
-
const timeoutSec = timeout ?? (
|
|
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
|
|
261
|
-
|
|
267
|
+
const invocation = buildCliInvocation(inferenceSpec(modelInput));
|
|
268
|
+
const isWin = invocation.needsShell;
|
|
262
269
|
|
|
263
|
-
|
|
264
|
-
const empty = isWin ? '""' : "";
|
|
270
|
+
// Prompt arg needs Windows quoting when using shell mode
|
|
265
271
|
const promptArg = isWin
|
|
266
|
-
?
|
|
272
|
+
? ('"' + fullPrompt.replaceAll(/\r?\n/g, " ").replaceAll('"', '""') + '"')
|
|
267
273
|
: fullPrompt;
|
|
268
274
|
|
|
269
275
|
const result = await execFileAsync(
|
|
270
|
-
|
|
271
|
-
[
|
|
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;
|