stonecut 1.4.0 → 1.4.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/package.json +1 -3
- package/src/execute.ts +2 -3
- package/src/logger.ts +4 -3
- package/src/runners/claude.ts +136 -73
- package/src/runners/codex.ts +104 -42
- package/src/types.ts +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stonecut",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "CLI that drives PRD-driven development with agentic coding CLIs",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -32,9 +32,7 @@
|
|
|
32
32
|
"prepare": "husky"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@anthropic-ai/claude-agent-sdk": "^0.2.92",
|
|
36
35
|
"@clack/prompts": "^0.10.0",
|
|
37
|
-
"@openai/codex-sdk": "^0.118.0",
|
|
38
36
|
"commander": "^13.1.0"
|
|
39
37
|
},
|
|
40
38
|
"lint-staged": {
|
package/src/execute.ts
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
|
|
12
12
|
import { checkoutOrCreateBranch, createPr, pushBranch } from "./git";
|
|
13
13
|
import { LocalSource } from "./local";
|
|
14
|
-
import { slugifyBranchComponent } from "./naming";
|
|
15
14
|
import { renderLocal } from "./prompt";
|
|
16
15
|
import { Logger } from "./logger";
|
|
17
16
|
import { defaultGitOps, formatTokens, runAfkLoop } from "./runner";
|
|
@@ -118,13 +117,13 @@ export async function executeLocal(
|
|
|
118
117
|
runnerName: string,
|
|
119
118
|
): Promise<void> {
|
|
120
119
|
const source = new LocalSource(name);
|
|
121
|
-
const
|
|
122
|
-
const logger = new Logger(prdIdentifier);
|
|
120
|
+
const logger = new Logger();
|
|
123
121
|
const runner = getRunner(runnerName, logger);
|
|
124
122
|
|
|
125
123
|
const session: Session = { logger, git: defaultGitOps, runner, runnerName };
|
|
126
124
|
|
|
127
125
|
try {
|
|
126
|
+
logger.logFile(`PRD: ${name}`);
|
|
128
127
|
checkoutOrCreateBranch(branch);
|
|
129
128
|
console.log("");
|
|
130
129
|
|
package/src/logger.ts
CHANGED
|
@@ -14,10 +14,11 @@ export class Logger implements LogWriter {
|
|
|
14
14
|
private warnedRecreated = false;
|
|
15
15
|
private warnedUnavailable = false;
|
|
16
16
|
|
|
17
|
-
constructor(
|
|
18
|
-
const
|
|
17
|
+
constructor() {
|
|
18
|
+
const now = new Date();
|
|
19
|
+
const timestamp = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}T${String(now.getHours()).padStart(2, "0")}-${String(now.getMinutes()).padStart(2, "0")}-${String(now.getSeconds()).padStart(2, "0")}`;
|
|
19
20
|
const logDir = resolve(".stonecut", "logs");
|
|
20
|
-
this.filePath = join(logDir, `${
|
|
21
|
+
this.filePath = join(logDir, `${timestamp}.log`);
|
|
21
22
|
try {
|
|
22
23
|
mkdirSync(logDir, { recursive: true });
|
|
23
24
|
appendFileSync(this.filePath, "");
|
package/src/runners/claude.ts
CHANGED
|
@@ -1,36 +1,79 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ClaudeRunner — adapter for Claude Code via
|
|
2
|
+
* ClaudeRunner — adapter for Claude Code via CLI subprocess.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* (tool calls,
|
|
7
|
-
*
|
|
4
|
+
* Spawns `claude --print --output-format stream-json --verbose` and
|
|
5
|
+
* consumes stdout as newline-delimited JSON. Console gets concise
|
|
6
|
+
* progress (tool calls, errors); the log file captures the full
|
|
7
|
+
* event stream for debugging.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
10
|
+
import type { ChildResult, LogWriter, Runner, RunResult, SpawnFn } from "../types.js";
|
|
11
|
+
|
|
12
|
+
const CLI_ARGS = [
|
|
13
|
+
"claude",
|
|
14
|
+
"--print",
|
|
15
|
+
"--output-format",
|
|
16
|
+
"stream-json",
|
|
17
|
+
"--verbose",
|
|
18
|
+
"--allowedTools",
|
|
19
|
+
"Bash,Edit,Read,Write,Glob,Grep",
|
|
20
|
+
"--permission-mode",
|
|
21
|
+
"acceptEdits",
|
|
22
|
+
"--no-session-persistence",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Subprocess spawning (injectable for tests)
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
async function* readLines(stream: ReadableStream<Uint8Array>): AsyncGenerator<string> {
|
|
30
|
+
const reader = stream.getReader();
|
|
31
|
+
const decoder = new TextDecoder();
|
|
32
|
+
let buffer = "";
|
|
33
|
+
try {
|
|
34
|
+
while (true) {
|
|
35
|
+
const { done, value } = await reader.read();
|
|
36
|
+
if (done) break;
|
|
37
|
+
buffer += decoder.decode(value, { stream: true });
|
|
38
|
+
const lines = buffer.split("\n");
|
|
39
|
+
buffer = lines.pop()!;
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
if (line.trim()) yield line;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (buffer.trim()) yield buffer;
|
|
45
|
+
} finally {
|
|
46
|
+
reader.releaseLock();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function defaultSpawn(cmd: string[], stdin: string): ChildResult {
|
|
51
|
+
const proc = Bun.spawn(cmd, {
|
|
52
|
+
stdin: new Blob([stdin]),
|
|
53
|
+
stdout: "pipe",
|
|
54
|
+
stderr: "ignore",
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
lines: readLines(proc.stdout),
|
|
58
|
+
exitCode: proc.exited,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let spawnFn: SpawnFn = defaultSpawn;
|
|
63
|
+
|
|
64
|
+
/** @internal Replace the spawn function. Returns a restore function. */
|
|
65
|
+
export function _setSpawnFn(fn: SpawnFn): () => void {
|
|
66
|
+
const prev = spawnFn;
|
|
67
|
+
spawnFn = fn;
|
|
29
68
|
return () => {
|
|
30
|
-
|
|
69
|
+
spawnFn = prev;
|
|
31
70
|
};
|
|
32
71
|
}
|
|
33
72
|
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Error subtype mapping
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
34
77
|
const ERROR_MESSAGES: Record<string, string> = {
|
|
35
78
|
error_max_turns: "max turns exceeded",
|
|
36
79
|
error_max_budget_usd: "max budget exceeded",
|
|
@@ -38,34 +81,23 @@ const ERROR_MESSAGES: Record<string, string> = {
|
|
|
38
81
|
error_max_structured_output_retries: "max structured output retries exceeded",
|
|
39
82
|
};
|
|
40
83
|
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Runner implementation
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
41
88
|
export class ClaudeRunner implements Runner {
|
|
42
89
|
private logger: LogWriter;
|
|
43
90
|
|
|
44
91
|
constructor(logger: LogWriter) {
|
|
45
92
|
this.logger = logger;
|
|
46
|
-
const configDir = process.env.CLAUDE_CONFIG_DIR || "~/.claude (default)";
|
|
47
|
-
logger.log(`Claude config: ${configDir}`);
|
|
48
93
|
}
|
|
49
94
|
|
|
50
95
|
async run(prompt: string): Promise<RunResult> {
|
|
51
96
|
const start = performance.now();
|
|
52
|
-
let
|
|
97
|
+
let child: ChildResult;
|
|
53
98
|
|
|
54
99
|
try {
|
|
55
|
-
|
|
56
|
-
prompt,
|
|
57
|
-
options: {
|
|
58
|
-
allowedTools: ALLOWED_TOOLS,
|
|
59
|
-
permissionMode: "acceptEdits",
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
for await (const message of stream) {
|
|
64
|
-
this.handleMessage(message);
|
|
65
|
-
if (message.type === "result") {
|
|
66
|
-
result = message;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
100
|
+
child = spawnFn(CLI_ARGS, prompt);
|
|
69
101
|
} catch (error: unknown) {
|
|
70
102
|
const durationSeconds = (performance.now() - start) / 1000;
|
|
71
103
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -76,16 +108,46 @@ export class ClaudeRunner implements Runner {
|
|
|
76
108
|
return { success: false, durationSeconds, error: msg };
|
|
77
109
|
}
|
|
78
110
|
|
|
111
|
+
let result: Record<string, unknown> | undefined;
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
for await (const line of child.lines) {
|
|
115
|
+
let parsed: Record<string, unknown>;
|
|
116
|
+
try {
|
|
117
|
+
parsed = JSON.parse(line);
|
|
118
|
+
} catch {
|
|
119
|
+
this.logger.logFile(`[claude] Malformed JSON line: ${line}`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.handleMessage(parsed);
|
|
124
|
+
|
|
125
|
+
if (parsed.type === "result") {
|
|
126
|
+
result = parsed;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} catch (error: unknown) {
|
|
130
|
+
const durationSeconds = (performance.now() - start) / 1000;
|
|
131
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
132
|
+
return { success: false, durationSeconds, error: msg };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const exitCode = await child.exitCode;
|
|
79
136
|
const durationSeconds = (performance.now() - start) / 1000;
|
|
80
137
|
|
|
138
|
+
if (exitCode !== 0) {
|
|
139
|
+
return { success: false, durationSeconds, error: `claude exited with code ${exitCode}` };
|
|
140
|
+
}
|
|
141
|
+
|
|
81
142
|
if (!result) {
|
|
82
143
|
return { success: false, durationSeconds, error: "no result message received" };
|
|
83
144
|
}
|
|
84
145
|
|
|
85
|
-
const sessionId = result.session_id;
|
|
86
|
-
const turns = result.num_turns;
|
|
87
|
-
const
|
|
88
|
-
const
|
|
146
|
+
const sessionId = result.session_id as string | undefined;
|
|
147
|
+
const turns = result.num_turns as number | undefined;
|
|
148
|
+
const usage = result.usage as { input_tokens?: number; output_tokens?: number } | undefined;
|
|
149
|
+
const inputTokens = usage?.input_tokens;
|
|
150
|
+
const outputTokens = usage?.output_tokens;
|
|
89
151
|
|
|
90
152
|
this.logger.logFile(
|
|
91
153
|
`[claude] Result: ${result.subtype}, turns=${turns}, input=${inputTokens}, output=${outputTokens}`,
|
|
@@ -95,7 +157,7 @@ export class ClaudeRunner implements Runner {
|
|
|
95
157
|
return {
|
|
96
158
|
success: true,
|
|
97
159
|
durationSeconds,
|
|
98
|
-
output: result.result,
|
|
160
|
+
output: result.result as string | undefined,
|
|
99
161
|
turns,
|
|
100
162
|
inputTokens,
|
|
101
163
|
outputTokens,
|
|
@@ -103,8 +165,10 @@ export class ClaudeRunner implements Runner {
|
|
|
103
165
|
};
|
|
104
166
|
}
|
|
105
167
|
|
|
106
|
-
const
|
|
107
|
-
const
|
|
168
|
+
const subtype = result.subtype as string;
|
|
169
|
+
const error = ERROR_MESSAGES[subtype] ?? `failed (${subtype})`;
|
|
170
|
+
const errors = result.errors as string[] | undefined;
|
|
171
|
+
const detail = errors?.length ? `: ${errors.join("; ")}` : "";
|
|
108
172
|
return {
|
|
109
173
|
success: false,
|
|
110
174
|
durationSeconds,
|
|
@@ -116,13 +180,19 @@ export class ClaudeRunner implements Runner {
|
|
|
116
180
|
};
|
|
117
181
|
}
|
|
118
182
|
|
|
119
|
-
|
|
183
|
+
// -----------------------------------------------------------------------
|
|
184
|
+
// Message routing
|
|
185
|
+
// -----------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
private handleMessage(message: Record<string, unknown>): void {
|
|
120
188
|
switch (message.type) {
|
|
121
189
|
case "assistant":
|
|
122
190
|
this.logAssistant(message);
|
|
123
191
|
break;
|
|
124
|
-
case "
|
|
125
|
-
|
|
192
|
+
case "system":
|
|
193
|
+
case "user":
|
|
194
|
+
case "rate_limit_event":
|
|
195
|
+
this.logger.logFile(`[claude] ${message.type}: ${JSON.stringify(message)}`);
|
|
126
196
|
break;
|
|
127
197
|
case "result":
|
|
128
198
|
break;
|
|
@@ -132,32 +202,25 @@ export class ClaudeRunner implements Runner {
|
|
|
132
202
|
}
|
|
133
203
|
}
|
|
134
204
|
|
|
135
|
-
private logAssistant(message:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
205
|
+
private logAssistant(message: Record<string, unknown>): void {
|
|
206
|
+
const msgBody = message.message as { content?: unknown[] } | undefined;
|
|
207
|
+
const content = msgBody?.content;
|
|
139
208
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const target = this.toolTarget(block.name, block.input as Record<string, unknown>);
|
|
144
|
-
this.logger.log(`[claude] ${block.name}${target ? `: ${target}` : ""}`);
|
|
145
|
-
}
|
|
209
|
+
if (!Array.isArray(content)) {
|
|
210
|
+
this.logger.logFile(`[claude] Assistant message: ${JSON.stringify(message)}`);
|
|
211
|
+
return;
|
|
146
212
|
}
|
|
147
213
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const delta = event.delta;
|
|
155
|
-
if ("text" in delta && typeof delta.text === "string") {
|
|
156
|
-
this.logger.logFile(`[claude] text: ${delta.text}`);
|
|
157
|
-
} else if ("thinking" in delta && typeof delta.thinking === "string") {
|
|
158
|
-
this.logger.logFile(`[claude] thinking: ${delta.thinking}`);
|
|
214
|
+
for (const block of content) {
|
|
215
|
+
if (typeof block === "string" || block === null || typeof block !== "object") continue;
|
|
216
|
+
const b = block as Record<string, unknown>;
|
|
217
|
+
if (b.type === "tool_use") {
|
|
218
|
+
const target = this.toolTarget(b.name as string, b.input as Record<string, unknown>);
|
|
219
|
+
this.logger.log(`[claude] ${b.name}${target ? `: ${target}` : ""}`);
|
|
159
220
|
}
|
|
160
221
|
}
|
|
222
|
+
|
|
223
|
+
this.logger.logFile(`[claude] Assistant message: ${JSON.stringify(content)}`);
|
|
161
224
|
}
|
|
162
225
|
|
|
163
226
|
private toolTarget(name: string, input: Record<string, unknown>): string | undefined {
|
package/src/runners/codex.ts
CHANGED
|
@@ -1,27 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CodexRunner — adapter for
|
|
2
|
+
* CodexRunner — adapter for Codex via CLI subprocess.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* changes, turn boundaries, errors); the log
|
|
7
|
-
* event stream for debugging.
|
|
4
|
+
* Spawns `codex exec --json --dangerously-bypass-approvals-and-sandbox` and
|
|
5
|
+
* consumes stdout as newline-delimited JSON. Console gets concise
|
|
6
|
+
* progress (tool calls, file changes, turn boundaries, errors); the log
|
|
7
|
+
* file captures the full event stream for debugging.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import type { ChildResult, LogWriter, Runner, RunResult, SpawnFn } from "../types.js";
|
|
11
|
+
|
|
12
|
+
const CLI_ARGS = ["codex", "exec", "--json", "--dangerously-bypass-approvals-and-sandbox"];
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Subprocess spawning (injectable for tests)
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
async function* readLines(stream: ReadableStream<Uint8Array>): AsyncGenerator<string> {
|
|
19
|
+
const reader = stream.getReader();
|
|
20
|
+
const decoder = new TextDecoder();
|
|
21
|
+
let buffer = "";
|
|
22
|
+
try {
|
|
23
|
+
while (true) {
|
|
24
|
+
const { done, value } = await reader.read();
|
|
25
|
+
if (done) break;
|
|
26
|
+
buffer += decoder.decode(value, { stream: true });
|
|
27
|
+
const lines = buffer.split("\n");
|
|
28
|
+
buffer = lines.pop()!;
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
if (line.trim()) yield line;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (buffer.trim()) yield buffer;
|
|
34
|
+
} finally {
|
|
35
|
+
reader.releaseLock();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function defaultSpawn(cmd: string[], stdin: string): ChildResult {
|
|
40
|
+
const proc = Bun.spawn(cmd, {
|
|
41
|
+
stdin: new Blob([stdin]),
|
|
42
|
+
stdout: "pipe",
|
|
43
|
+
stderr: "ignore",
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
lines: readLines(proc.stdout),
|
|
47
|
+
exitCode: proc.exited,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
12
50
|
|
|
13
|
-
|
|
14
|
-
let createCodex: CodexFactory = (...args) => new Codex(...args);
|
|
51
|
+
let spawnFn: SpawnFn = defaultSpawn;
|
|
15
52
|
|
|
16
|
-
/** @internal Replace the
|
|
17
|
-
export function
|
|
18
|
-
const prev =
|
|
19
|
-
|
|
53
|
+
/** @internal Replace the spawn function. Returns a restore function. */
|
|
54
|
+
export function _setSpawnFn(fn: SpawnFn): () => void {
|
|
55
|
+
const prev = spawnFn;
|
|
56
|
+
spawnFn = fn;
|
|
20
57
|
return () => {
|
|
21
|
-
|
|
58
|
+
spawnFn = prev;
|
|
22
59
|
};
|
|
23
60
|
}
|
|
24
61
|
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Runner implementation
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
25
66
|
export class CodexRunner implements Runner {
|
|
26
67
|
private logger: LogWriter;
|
|
27
68
|
|
|
@@ -31,6 +72,19 @@ export class CodexRunner implements Runner {
|
|
|
31
72
|
|
|
32
73
|
async run(prompt: string): Promise<RunResult> {
|
|
33
74
|
const start = performance.now();
|
|
75
|
+
let child: ChildResult;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
child = spawnFn(CLI_ARGS, prompt);
|
|
79
|
+
} catch (error: unknown) {
|
|
80
|
+
const durationSeconds = (performance.now() - start) / 1000;
|
|
81
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
82
|
+
|
|
83
|
+
if (msg.includes("ENOENT") || (error as NodeJS.ErrnoException)?.code === "ENOENT") {
|
|
84
|
+
return { success: false, durationSeconds, error: "codex binary not found in PATH" };
|
|
85
|
+
}
|
|
86
|
+
return { success: false, durationSeconds, error: msg };
|
|
87
|
+
}
|
|
34
88
|
|
|
35
89
|
let threadId: string | undefined;
|
|
36
90
|
let turns = 0;
|
|
@@ -41,58 +95,68 @@ export class CodexRunner implements Runner {
|
|
|
41
95
|
let errorMessage: string | undefined;
|
|
42
96
|
|
|
43
97
|
try {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
98
|
+
for await (const line of child.lines) {
|
|
99
|
+
let parsed: Record<string, unknown>;
|
|
100
|
+
try {
|
|
101
|
+
parsed = JSON.parse(line);
|
|
102
|
+
} catch {
|
|
103
|
+
this.logger.logFile(`[codex] Malformed JSON line: ${line}`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
51
106
|
|
|
52
|
-
|
|
53
|
-
this.handleEvent(event);
|
|
107
|
+
this.logger.logFile(`[codex] ${parsed.type}: ${JSON.stringify(parsed)}`);
|
|
54
108
|
|
|
55
|
-
switch (
|
|
109
|
+
switch (parsed.type) {
|
|
56
110
|
case "thread.started":
|
|
57
|
-
threadId =
|
|
111
|
+
threadId = parsed.thread_id as string;
|
|
58
112
|
break;
|
|
59
113
|
case "turn.started":
|
|
60
114
|
turns++;
|
|
115
|
+
completed = false;
|
|
61
116
|
break;
|
|
62
117
|
case "turn.completed":
|
|
63
118
|
completed = true;
|
|
64
119
|
this.logger.log(`[codex] Turn ${turns} completed`);
|
|
65
|
-
|
|
66
|
-
|
|
120
|
+
{
|
|
121
|
+
const usage = parsed.usage as
|
|
122
|
+
| { input_tokens?: number; output_tokens?: number }
|
|
123
|
+
| undefined;
|
|
124
|
+
inputTokens += usage?.input_tokens ?? 0;
|
|
125
|
+
outputTokens += usage?.output_tokens ?? 0;
|
|
126
|
+
}
|
|
67
127
|
break;
|
|
68
128
|
case "turn.failed":
|
|
69
129
|
failed = true;
|
|
70
|
-
|
|
130
|
+
{
|
|
131
|
+
const err = parsed.error as { message?: string } | undefined;
|
|
132
|
+
errorMessage = err?.message ?? "codex turn failed";
|
|
133
|
+
}
|
|
71
134
|
this.logger.log(`[codex] Error: ${errorMessage}`);
|
|
72
135
|
break;
|
|
73
136
|
case "error":
|
|
74
137
|
failed = true;
|
|
75
|
-
errorMessage =
|
|
138
|
+
errorMessage = parsed.message as string;
|
|
76
139
|
this.logger.log(`[codex] Error: ${errorMessage}`);
|
|
77
140
|
break;
|
|
78
141
|
case "item.started":
|
|
79
142
|
case "item.completed":
|
|
80
|
-
this.logItem(
|
|
143
|
+
this.logItem(parsed.type, parsed.item as Record<string, unknown>);
|
|
81
144
|
break;
|
|
82
145
|
}
|
|
83
146
|
}
|
|
84
147
|
} catch (error: unknown) {
|
|
85
148
|
const durationSeconds = (performance.now() - start) / 1000;
|
|
86
149
|
const msg = error instanceof Error ? error.message : String(error);
|
|
87
|
-
|
|
88
|
-
if (msg.includes("ENOENT") || msg.includes("not found") || msg.includes("spawn")) {
|
|
89
|
-
return { success: false, durationSeconds, error: "codex binary not found in PATH" };
|
|
90
|
-
}
|
|
91
150
|
return { success: false, durationSeconds, error: msg };
|
|
92
151
|
}
|
|
93
152
|
|
|
153
|
+
const exitCode = await child.exitCode;
|
|
94
154
|
const durationSeconds = (performance.now() - start) / 1000;
|
|
95
155
|
|
|
156
|
+
if (exitCode !== 0) {
|
|
157
|
+
return { success: false, durationSeconds, error: `codex exited with code ${exitCode}` };
|
|
158
|
+
}
|
|
159
|
+
|
|
96
160
|
const status = failed ? "failed" : completed ? "success" : "incomplete";
|
|
97
161
|
this.logger.logFile(
|
|
98
162
|
`[codex] Result: ${status}, turns=${turns}, input=${inputTokens}, output=${outputTokens}`,
|
|
@@ -132,21 +196,19 @@ export class CodexRunner implements Runner {
|
|
|
132
196
|
};
|
|
133
197
|
}
|
|
134
198
|
|
|
135
|
-
private
|
|
136
|
-
this.logger.logFile(`[codex] ${event.type}: ${JSON.stringify(event)}`);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
private logItem(phase: "item.started" | "item.completed", item: ThreadItem): void {
|
|
199
|
+
private logItem(phase: string, item: Record<string, unknown>): void {
|
|
140
200
|
switch (item.type) {
|
|
141
201
|
case "command_execution":
|
|
142
|
-
if (phase === "item.started") {
|
|
202
|
+
if (phase === "item.started" && typeof item.command === "string") {
|
|
143
203
|
this.logger.log(`[codex] Bash: ${item.command.slice(0, 80)}`);
|
|
144
204
|
}
|
|
145
205
|
break;
|
|
146
206
|
case "file_change":
|
|
147
|
-
if (phase === "item.completed") {
|
|
207
|
+
if (phase === "item.completed" && Array.isArray(item.changes)) {
|
|
148
208
|
for (const change of item.changes) {
|
|
149
|
-
|
|
209
|
+
if (typeof change?.path === "string") {
|
|
210
|
+
this.logger.log(`[codex] Edit: ${change.path}`);
|
|
211
|
+
}
|
|
150
212
|
}
|
|
151
213
|
}
|
|
152
214
|
break;
|
package/src/types.ts
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
* Core types and interfaces shared across the Stonecut CLI.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
/** Return type from SpawnFn — stdout line stream and process exit code. */
|
|
6
|
+
export interface ChildResult {
|
|
7
|
+
lines: AsyncIterable<string>;
|
|
8
|
+
exitCode: Promise<number>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Injectable subprocess factory for runner testing. */
|
|
12
|
+
export type SpawnFn = (cmd: string[], stdin: string) => ChildResult;
|
|
13
|
+
|
|
5
14
|
/** Structured result from a single runner execution. */
|
|
6
15
|
export interface RunResult {
|
|
7
16
|
success: boolean;
|