claude2codex 0.1.0 → 0.3.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/PATTERNS.md +139 -0
- package/PATTERNS.zh-CN.md +138 -0
- package/README.md +163 -43
- package/README.zh-CN.md +161 -41
- package/dist/server.js +565 -75
- package/package.json +7 -3
package/dist/server.js
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
// src/server.ts
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { stat } from "fs/promises";
|
|
7
|
+
import { resolve } from "path";
|
|
6
8
|
import { z } from "zod";
|
|
7
9
|
|
|
8
10
|
// src/contracts.ts
|
|
9
|
-
import {
|
|
10
|
-
import { join } from "path";
|
|
11
|
+
import { statSync } from "fs";
|
|
11
12
|
var PROTOCOL_INSTRUCTIONS = `## Protocol Instructions
|
|
12
13
|
|
|
13
14
|
### Execution Rules
|
|
@@ -39,25 +40,29 @@ End every task with exactly these sections:
|
|
|
39
40
|
|
|
40
41
|
### Rework
|
|
41
42
|
For a Delta Contract, fix only its review findings and failed Success Conditions. Do not revisit work that passed review. Repeat the full Self-Verification and Required Handoff.`;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
function renderContextFiles(contextFiles2) {
|
|
44
|
+
if (!contextFiles2?.length) return [];
|
|
45
|
+
return [
|
|
46
|
+
"### Context Files",
|
|
47
|
+
"Read these files before implementing. They are reference material, not necessarily files to modify.",
|
|
48
|
+
...contextFiles2.map((contextFile) => {
|
|
49
|
+
const directory = statSync(contextFile.path).isDirectory() ? " (directory)" : "";
|
|
50
|
+
const note = contextFile.note ? ` \u2014 ${contextFile.note}` : "";
|
|
51
|
+
return `- ${contextFile.path}${directory}${note}`;
|
|
52
|
+
}),
|
|
53
|
+
""
|
|
54
|
+
];
|
|
49
55
|
}
|
|
50
|
-
|
|
51
|
-
const agents = await optionalAgentsInstructions(cwd);
|
|
56
|
+
function renderGoalContract(c) {
|
|
52
57
|
const lines = [
|
|
53
58
|
PROTOCOL_INSTRUCTIONS,
|
|
54
|
-
...agents ? ["", "## Project Instructions", "", agents.trim()] : [],
|
|
55
59
|
"",
|
|
56
60
|
"## Goal Contract",
|
|
57
61
|
"",
|
|
58
62
|
"### Goal",
|
|
59
63
|
c.goal.trim(),
|
|
60
64
|
"",
|
|
65
|
+
...renderContextFiles(c.context_files),
|
|
61
66
|
"### Constraints",
|
|
62
67
|
...c.constraints.map((x) => `- ${x}`),
|
|
63
68
|
"",
|
|
@@ -75,6 +80,7 @@ function renderDeltaContract(d) {
|
|
|
75
80
|
"### Findings",
|
|
76
81
|
...d.findings.map((x) => `- ${x}`),
|
|
77
82
|
"",
|
|
83
|
+
...renderContextFiles(d.context_files),
|
|
78
84
|
"### Failed Success Conditions",
|
|
79
85
|
...d.failed_conditions.map((x) => `- [ ] ${x}`),
|
|
80
86
|
"",
|
|
@@ -84,10 +90,13 @@ function renderDeltaContract(d) {
|
|
|
84
90
|
];
|
|
85
91
|
return lines.join("\n");
|
|
86
92
|
}
|
|
87
|
-
function
|
|
93
|
+
function renderObjectiveDetails(c, maxLen) {
|
|
88
94
|
const conditions = c.success_conditions.map((x) => `(${x})`).join(" ");
|
|
89
95
|
const text = `Fulfill this Goal Contract: ${c.goal.trim()} The goal is achieved only when every Success Condition is verified: ${conditions} and a structured handoff (Changed Files / Validation / Success Conditions / Risks & Deviations) has been delivered.`;
|
|
90
|
-
return
|
|
96
|
+
return {
|
|
97
|
+
objective: text.length <= maxLen ? text : text.slice(0, maxLen - 1) + "\u2026",
|
|
98
|
+
objectiveChars: text.length
|
|
99
|
+
};
|
|
91
100
|
}
|
|
92
101
|
var HANDOFF_SECTIONS = [
|
|
93
102
|
"Changed Files",
|
|
@@ -126,6 +135,7 @@ var CodexAppServer = class {
|
|
|
126
135
|
pending = /* @__PURE__ */ new Map();
|
|
127
136
|
opts;
|
|
128
137
|
stderrTail = [];
|
|
138
|
+
exitError;
|
|
129
139
|
exited = false;
|
|
130
140
|
constructor(opts) {
|
|
131
141
|
this.opts = opts;
|
|
@@ -142,24 +152,26 @@ var CodexAppServer = class {
|
|
|
142
152
|
if (this.stderrTail.length > 50) this.stderrTail.shift();
|
|
143
153
|
opts.onLog?.(line);
|
|
144
154
|
});
|
|
145
|
-
this.proc.on("
|
|
146
|
-
|
|
147
|
-
for (const [, p] of this.pending) {
|
|
148
|
-
clearTimeout(p.timer);
|
|
149
|
-
p.reject(new Error(`codex app-server exited (code ${code}). stderr: ${this.stderrTail.slice(-10).join(" | ")}`));
|
|
150
|
-
}
|
|
151
|
-
this.pending.clear();
|
|
152
|
-
opts.onExit?.(code);
|
|
153
|
-
});
|
|
155
|
+
this.proc.on("spawn", () => opts.onSpawn?.(this.proc.pid));
|
|
156
|
+
this.proc.on("exit", (code) => this.handleExit(code));
|
|
154
157
|
this.proc.on("error", (err) => {
|
|
155
|
-
this.
|
|
156
|
-
for (const [, p] of this.pending) {
|
|
157
|
-
clearTimeout(p.timer);
|
|
158
|
-
p.reject(new Error(`failed to spawn ${opts.bin}: ${err.message}`));
|
|
159
|
-
}
|
|
160
|
-
this.pending.clear();
|
|
158
|
+
this.handleExit(null, new Error(`failed to spawn ${opts.bin}: ${err.message}`));
|
|
161
159
|
});
|
|
162
160
|
}
|
|
161
|
+
handleExit(code, error) {
|
|
162
|
+
if (this.exited) return;
|
|
163
|
+
this.exited = true;
|
|
164
|
+
const stderr = this.stderrTail.slice(-10).join(" | ");
|
|
165
|
+
this.exitError = error ?? new Error(
|
|
166
|
+
`codex app-server exited (code ${code})${stderr ? `. stderr: ${stderr}` : ""}`
|
|
167
|
+
);
|
|
168
|
+
for (const [, p] of this.pending) {
|
|
169
|
+
clearTimeout(p.timer);
|
|
170
|
+
p.reject(this.exitError);
|
|
171
|
+
}
|
|
172
|
+
this.pending.clear();
|
|
173
|
+
this.opts.onExit?.(code, this.exitError);
|
|
174
|
+
}
|
|
163
175
|
handleLine(line) {
|
|
164
176
|
const trimmed = line.trim();
|
|
165
177
|
if (!trimmed) return;
|
|
@@ -192,13 +204,16 @@ var CodexAppServer = class {
|
|
|
192
204
|
this.proc.stdin.write(JSON.stringify(obj) + "\n");
|
|
193
205
|
}
|
|
194
206
|
request(method, params, timeoutMs = 6e4) {
|
|
207
|
+
if (this.exited) {
|
|
208
|
+
return Promise.reject(this.exitError ?? new Error("codex app-server is not running"));
|
|
209
|
+
}
|
|
195
210
|
const id = this.nextId++;
|
|
196
|
-
return new Promise((
|
|
211
|
+
return new Promise((resolve2, reject) => {
|
|
197
212
|
const timer = setTimeout(() => {
|
|
198
213
|
this.pending.delete(id);
|
|
199
214
|
reject(new Error(`request ${method} timed out after ${timeoutMs}ms`));
|
|
200
215
|
}, timeoutMs);
|
|
201
|
-
this.pending.set(id, { resolve, reject, timer });
|
|
216
|
+
this.pending.set(id, { resolve: resolve2, reject, timer });
|
|
202
217
|
this.send({ id, method, ...params !== void 0 ? { params } : {} });
|
|
203
218
|
});
|
|
204
219
|
}
|
|
@@ -207,7 +222,7 @@ var CodexAppServer = class {
|
|
|
207
222
|
}
|
|
208
223
|
async initialize() {
|
|
209
224
|
const result = await this.request("initialize", {
|
|
210
|
-
clientInfo: { name: "claude2codex", title: "Claude\u2194Codex Contract Bridge", version: "0.1
|
|
225
|
+
clientInfo: { name: "claude2codex", title: "Claude\u2194Codex Contract Bridge", version: "0.3.1" },
|
|
211
226
|
capabilities: { experimentalApi: true }
|
|
212
227
|
});
|
|
213
228
|
this.notify("initialized", {});
|
|
@@ -218,25 +233,178 @@ var CodexAppServer = class {
|
|
|
218
233
|
}
|
|
219
234
|
};
|
|
220
235
|
|
|
236
|
+
// src/log.ts
|
|
237
|
+
var priorities = { error: 1, info: 2, debug: 3 };
|
|
238
|
+
function configuredLevel() {
|
|
239
|
+
const value = process.env.C2C_LOG_LEVEL;
|
|
240
|
+
return value === "silent" || value === "error" || value === "info" || value === "debug" ? value : "info";
|
|
241
|
+
}
|
|
242
|
+
function writeLog(level, event, fields = {}) {
|
|
243
|
+
const configured = configuredLevel();
|
|
244
|
+
if (configured === "silent" || priorities[level] > priorities[configured]) return;
|
|
245
|
+
process.stderr.write(`${JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level, event, ...fields })}
|
|
246
|
+
`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/store.ts
|
|
250
|
+
import { accessSync, constants, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
251
|
+
import { homedir } from "os";
|
|
252
|
+
import { join } from "path";
|
|
253
|
+
var MAX_JOBS = 50;
|
|
254
|
+
var warned = false;
|
|
255
|
+
function warnOnce(error) {
|
|
256
|
+
if (warned) return;
|
|
257
|
+
warned = true;
|
|
258
|
+
writeLog("error", "persistence_degraded", {
|
|
259
|
+
error: error instanceof Error ? error.message : String(error)
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function defaultStateDir() {
|
|
263
|
+
return process.env.C2C_STATE_DIR ?? join(homedir(), ".claude2codex", "jobs");
|
|
264
|
+
}
|
|
265
|
+
function createJobStore(dir = defaultStateDir()) {
|
|
266
|
+
const records = /* @__PURE__ */ new Map();
|
|
267
|
+
let enabled = true;
|
|
268
|
+
try {
|
|
269
|
+
mkdirSync(dir, { recursive: true });
|
|
270
|
+
accessSync(dir, constants.R_OK | constants.W_OK);
|
|
271
|
+
const interrupted = [];
|
|
272
|
+
for (const name of readdirSync(dir)) {
|
|
273
|
+
if (!name.endsWith(".json")) continue;
|
|
274
|
+
try {
|
|
275
|
+
const job = JSON.parse(readFileSync(join(dir, name), "utf8"));
|
|
276
|
+
if (!job?.id || !job?.startedAt || !Array.isArray(job.transcript)) continue;
|
|
277
|
+
if (job.state === "blocked" || job.state === "queued" || job.state === "starting" || job.state === "running") {
|
|
278
|
+
job.state = "error";
|
|
279
|
+
job.error = "interrupted by server restart";
|
|
280
|
+
job.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
281
|
+
interrupted.push(job);
|
|
282
|
+
}
|
|
283
|
+
records.set(job.id, job);
|
|
284
|
+
} catch {
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
for (const job of interrupted) atomicWrite(dir, job);
|
|
288
|
+
prune(dir, records);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
enabled = false;
|
|
291
|
+
warnOnce(error);
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
getJob: (id) => records.get(id),
|
|
295
|
+
listJobs: () => [...records.values()],
|
|
296
|
+
save(job) {
|
|
297
|
+
const record = { ...job, transcript: job.transcript.slice(-200) };
|
|
298
|
+
records.set(job.id, record);
|
|
299
|
+
if (!enabled) return;
|
|
300
|
+
try {
|
|
301
|
+
atomicWrite(dir, record);
|
|
302
|
+
prune(dir, records);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
enabled = false;
|
|
305
|
+
warnOnce(error);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function atomicWrite(dir, job) {
|
|
311
|
+
const target = join(dir, `${job.id}.json`);
|
|
312
|
+
const temp = join(dir, `.${job.id}.${process.pid}.${Date.now()}.tmp`);
|
|
313
|
+
writeFileSync(temp, JSON.stringify(job), { encoding: "utf8", mode: 384 });
|
|
314
|
+
renameSync(temp, target);
|
|
315
|
+
}
|
|
316
|
+
function prune(dir, records) {
|
|
317
|
+
const entries = [...records.values()].sort(
|
|
318
|
+
(a, b) => Date.parse(b.endedAt ?? b.startedAt) - Date.parse(a.endedAt ?? a.startedAt)
|
|
319
|
+
);
|
|
320
|
+
for (const job of entries.slice(MAX_JOBS)) {
|
|
321
|
+
records.delete(job.id);
|
|
322
|
+
try {
|
|
323
|
+
unlinkSync(join(dir, `${job.id}.json`));
|
|
324
|
+
} catch (error) {
|
|
325
|
+
if (error?.code !== "ENOENT") throw error;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
221
330
|
// src/jobs.ts
|
|
222
|
-
var
|
|
331
|
+
var store = createJobStore();
|
|
332
|
+
var jobs = new Map(store.listJobs().map((job) => [job.id, job]));
|
|
223
333
|
function getJob(id) {
|
|
224
|
-
return jobs.get(id);
|
|
334
|
+
return jobs.get(id) ?? store.getJob(id);
|
|
335
|
+
}
|
|
336
|
+
function listJobs() {
|
|
337
|
+
return [...jobs.values()];
|
|
338
|
+
}
|
|
339
|
+
var queue = [];
|
|
340
|
+
var blocked = /* @__PURE__ */ new Map();
|
|
341
|
+
function getQueuePosition(id) {
|
|
342
|
+
const index = queue.findIndex((entry) => entry.job.id === id);
|
|
343
|
+
return index === -1 ? void 0 : index + 1;
|
|
344
|
+
}
|
|
345
|
+
function activeJobCount() {
|
|
346
|
+
return [...jobs.values()].filter((job) => job.state === "starting" || job.state === "running").length;
|
|
347
|
+
}
|
|
348
|
+
function launch(job, opts) {
|
|
349
|
+
void runJob(job, opts).catch((err) => {
|
|
350
|
+
finish(job, "error", void 0, String(err?.message ?? err));
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
function drainQueue() {
|
|
354
|
+
while (queue.length > 0 && activeJobCount() < queue[0].opts.config.maxConcurrent) {
|
|
355
|
+
const { job, opts } = queue.shift();
|
|
356
|
+
job.state = "starting";
|
|
357
|
+
log(job, "queue", "dequeued; starting job");
|
|
358
|
+
store.save(job);
|
|
359
|
+
writeLog("info", "job_dequeued", { job_id: job.id, kind: job.kind });
|
|
360
|
+
opts.onProgress?.({ jobId: job.id, event: "dequeued", message: "job dequeued; starting" });
|
|
361
|
+
launch(job, opts);
|
|
362
|
+
}
|
|
225
363
|
}
|
|
226
364
|
function startJob(opts) {
|
|
365
|
+
const dependency = opts.dependsOn ? getJob(opts.dependsOn) : void 0;
|
|
366
|
+
if (opts.dependsOn && !dependency) throw new Error(`unknown depends_on job_id ${opts.dependsOn}`);
|
|
367
|
+
if (dependency?.state === "error" || dependency?.state === "timeout") {
|
|
368
|
+
throw new Error(dependencyFailureMessage(dependency));
|
|
369
|
+
}
|
|
370
|
+
const isBlocked = dependency !== void 0 && dependency.state !== "done";
|
|
371
|
+
const queued = !isBlocked && (queue.length > 0 || activeJobCount() >= opts.config.maxConcurrent);
|
|
372
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
227
373
|
const job = {
|
|
228
374
|
id: randomUUID().slice(0, 8),
|
|
229
375
|
kind: opts.kind,
|
|
230
|
-
state: "starting",
|
|
376
|
+
state: isBlocked ? "blocked" : queued ? "queued" : "starting",
|
|
231
377
|
goalSet: false,
|
|
378
|
+
usage: {},
|
|
379
|
+
attempts: 1,
|
|
232
380
|
turns: 0,
|
|
233
381
|
transcript: [],
|
|
234
|
-
|
|
382
|
+
...opts.contextFiles ? { contextFiles: opts.contextFiles } : {},
|
|
383
|
+
...opts.dependsOn ? { dependsOn: opts.dependsOn } : {},
|
|
384
|
+
startedAt,
|
|
385
|
+
lastActivityAt: startedAt
|
|
235
386
|
};
|
|
236
387
|
jobs.set(job.id, job);
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
388
|
+
if (isBlocked) {
|
|
389
|
+
const dependents = blocked.get(opts.dependsOn) ?? [];
|
|
390
|
+
dependents.push({ job, opts });
|
|
391
|
+
blocked.set(opts.dependsOn, dependents);
|
|
392
|
+
log(job, "blocked", `blocked on dependency ${opts.dependsOn}`);
|
|
393
|
+
} else if (queued) {
|
|
394
|
+
queue.push({ job, opts });
|
|
395
|
+
log(job, "queue", `enqueued at position ${queue.length}`);
|
|
396
|
+
}
|
|
397
|
+
store.save(job);
|
|
398
|
+
writeLog("info", "job_created", { job_id: job.id, kind: job.kind, state: job.state });
|
|
399
|
+
if (isBlocked) {
|
|
400
|
+
writeLog("info", "job_blocked", { job_id: job.id, kind: job.kind, depends_on: opts.dependsOn });
|
|
401
|
+
opts.onProgress?.({ jobId: job.id, event: "blocked", message: `blocked on dependency ${opts.dependsOn}` });
|
|
402
|
+
} else if (queued) {
|
|
403
|
+
writeLog("info", "job_enqueued", { job_id: job.id, kind: job.kind, queue_position: queue.length });
|
|
404
|
+
opts.onProgress?.({ jobId: job.id, event: "queued", message: `job queued at position ${queue.length}` });
|
|
405
|
+
} else {
|
|
406
|
+
launch(job, opts);
|
|
407
|
+
}
|
|
240
408
|
return job;
|
|
241
409
|
}
|
|
242
410
|
function log(job, kind, detail) {
|
|
@@ -245,11 +413,73 @@ function log(job, kind, detail) {
|
|
|
245
413
|
}
|
|
246
414
|
function finish(job, state, client, error) {
|
|
247
415
|
if (job.state === "done" || job.state === "error" || job.state === "timeout") return;
|
|
416
|
+
const previousState = job.state;
|
|
248
417
|
job.state = state;
|
|
249
418
|
job.error = error;
|
|
250
419
|
job.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
251
420
|
if (job.finalMessage) job.handoff = parseHandoff(job.finalMessage);
|
|
421
|
+
store.save(job);
|
|
422
|
+
writeLog(state === "error" || state === "timeout" ? "error" : "info", "job_state_changed", {
|
|
423
|
+
job_id: job.id,
|
|
424
|
+
from: previousState,
|
|
425
|
+
to: state,
|
|
426
|
+
...error ? { error } : {}
|
|
427
|
+
});
|
|
428
|
+
writeLog("info", "job_usage", {
|
|
429
|
+
job_id: job.id,
|
|
430
|
+
state,
|
|
431
|
+
usage: job.usage ?? {}
|
|
432
|
+
});
|
|
252
433
|
client?.kill();
|
|
434
|
+
resolveDependents(job);
|
|
435
|
+
drainQueue();
|
|
436
|
+
}
|
|
437
|
+
function dependencyFailureMessage(dependency) {
|
|
438
|
+
return `dependency ${dependency.id} failed: ${dependency.error ?? `state ${dependency.state}`}`;
|
|
439
|
+
}
|
|
440
|
+
function resolveDependents(dependency) {
|
|
441
|
+
const dependents = blocked.get(dependency.id);
|
|
442
|
+
if (!dependents?.length) return;
|
|
443
|
+
blocked.delete(dependency.id);
|
|
444
|
+
for (const { job, opts } of dependents) {
|
|
445
|
+
if (dependency.state === "done") {
|
|
446
|
+
log(job, "unblocked", `dependency ${dependency.id} completed; unblocked`);
|
|
447
|
+
store.save(job);
|
|
448
|
+
writeLog("info", "job_unblocked", { job_id: job.id, depends_on: dependency.id });
|
|
449
|
+
opts.onProgress?.({
|
|
450
|
+
jobId: job.id,
|
|
451
|
+
event: "unblocked",
|
|
452
|
+
message: `dependency ${dependency.id} completed; job unblocked`
|
|
453
|
+
});
|
|
454
|
+
enqueueOrLaunch(job, opts);
|
|
455
|
+
} else {
|
|
456
|
+
const error = dependencyFailureMessage(dependency);
|
|
457
|
+
log(job, "dependency_failed", error);
|
|
458
|
+
store.save(job);
|
|
459
|
+
writeLog("info", "job_dependency_failed", {
|
|
460
|
+
job_id: job.id,
|
|
461
|
+
depends_on: dependency.id,
|
|
462
|
+
error
|
|
463
|
+
});
|
|
464
|
+
opts.onProgress?.({ jobId: job.id, event: "dependency_failed", message: error });
|
|
465
|
+
finish(job, "error", void 0, error);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function enqueueOrLaunch(job, opts) {
|
|
470
|
+
if (queue.length > 0 || activeJobCount() >= opts.config.maxConcurrent) {
|
|
471
|
+
job.state = "queued";
|
|
472
|
+
queue.push({ job, opts });
|
|
473
|
+
log(job, "queue", `enqueued at position ${queue.length} after dependency completed`);
|
|
474
|
+
store.save(job);
|
|
475
|
+
writeLog("info", "job_enqueued", { job_id: job.id, kind: job.kind, queue_position: queue.length });
|
|
476
|
+
opts.onProgress?.({ jobId: job.id, event: "queued", message: `job queued at position ${queue.length}` });
|
|
477
|
+
drainQueue();
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
job.state = "starting";
|
|
481
|
+
store.save(job);
|
|
482
|
+
launch(job, opts);
|
|
253
483
|
}
|
|
254
484
|
function extractText(item) {
|
|
255
485
|
if (typeof item?.text === "string") return item.text;
|
|
@@ -270,23 +500,96 @@ function extractGoal(params) {
|
|
|
270
500
|
timeUsedSeconds: g.timeUsedSeconds ?? g.time_used_seconds
|
|
271
501
|
};
|
|
272
502
|
}
|
|
503
|
+
var usageAliases = {
|
|
504
|
+
input_tokens: "inputTokens",
|
|
505
|
+
input_token_count: "inputTokens",
|
|
506
|
+
output_tokens: "outputTokens",
|
|
507
|
+
output_token_count: "outputTokens",
|
|
508
|
+
total_tokens: "totalTokens",
|
|
509
|
+
total_token_count: "totalTokens",
|
|
510
|
+
tokensUsed: "totalTokens",
|
|
511
|
+
tokens_used: "totalTokens"
|
|
512
|
+
};
|
|
513
|
+
function extractUsage(params) {
|
|
514
|
+
const source = params?.turn?.usage ?? params?.usage ?? params?.turn?.tokenUsage ?? params?.tokenUsage;
|
|
515
|
+
if (!source || typeof source !== "object") return void 0;
|
|
516
|
+
const usage = {};
|
|
517
|
+
for (const [key, value] of Object.entries(source)) {
|
|
518
|
+
if (typeof value === "number" && Number.isFinite(value)) usage[usageAliases[key] ?? key] = value;
|
|
519
|
+
}
|
|
520
|
+
return Object.keys(usage).length ? usage : void 0;
|
|
521
|
+
}
|
|
522
|
+
function mergeUsage(job, usage, cumulative = false) {
|
|
523
|
+
if (!usage) return;
|
|
524
|
+
job.usage ??= {};
|
|
525
|
+
for (const [key, value] of Object.entries(usage)) {
|
|
526
|
+
job.usage[key] = cumulative ? value : (job.usage[key] ?? 0) + value;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function updateGoal(job, params) {
|
|
530
|
+
job.goal = extractGoal(params);
|
|
531
|
+
if (typeof job.goal.tokensUsed === "number") {
|
|
532
|
+
mergeUsage(job, { totalTokens: job.goal.tokensUsed }, true);
|
|
533
|
+
}
|
|
534
|
+
store.save(job);
|
|
535
|
+
}
|
|
273
536
|
var TERMINAL_GOAL_STATUSES = /* @__PURE__ */ new Set(["complete", "budget_limited", "budgetLimited"]);
|
|
274
537
|
async function runJob(job, opts) {
|
|
538
|
+
const deadline = Date.now() + opts.config.jobTimeoutMs;
|
|
539
|
+
const maxAttempts = (opts.config.retries ?? 1) + 1;
|
|
540
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
541
|
+
job.attempts = attempt;
|
|
542
|
+
log(job, "attempt", `attempt ${attempt} started`);
|
|
543
|
+
store.save(job);
|
|
544
|
+
writeLog("info", "job_attempt_started", { job_id: job.id, attempt });
|
|
545
|
+
const result = await runAttempt(job, opts, Math.max(0, deadline - Date.now()));
|
|
546
|
+
if (!result.error || job.state === "done" || job.state === "error" || job.state === "timeout") return;
|
|
547
|
+
const retryable = result.processFailure && job.state === "starting" && job.turns === 0;
|
|
548
|
+
if (!retryable || attempt === maxAttempts) {
|
|
549
|
+
finish(job, "error", void 0, result.error.message);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
log(job, "retry", `attempt ${attempt} failed before the first turn; retrying in 500ms`);
|
|
553
|
+
store.save(job);
|
|
554
|
+
writeLog("info", "job_retry_scheduled", {
|
|
555
|
+
job_id: job.id,
|
|
556
|
+
attempt,
|
|
557
|
+
next_attempt: attempt + 1,
|
|
558
|
+
delay_ms: 500,
|
|
559
|
+
error: result.error.message
|
|
560
|
+
});
|
|
561
|
+
await new Promise((resolve2) => setTimeout(resolve2, 500));
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
async function runAttempt(job, opts, timeoutMs) {
|
|
275
565
|
const cfg = opts.config;
|
|
276
566
|
let activeTurn = false;
|
|
567
|
+
let stalled = false;
|
|
568
|
+
let processError;
|
|
277
569
|
let quietTimer;
|
|
570
|
+
let stallTimer;
|
|
278
571
|
let settle;
|
|
279
572
|
const doneSignal = new Promise((res) => settle = res);
|
|
280
573
|
const progress = (event, message) => opts.onProgress?.({ jobId: job.id, event, message });
|
|
574
|
+
if (!opts.resumeThreadId) job.goalSet = false;
|
|
281
575
|
const client = new CodexAppServer({
|
|
282
576
|
bin: cfg.bin,
|
|
283
577
|
args: cfg.args,
|
|
284
578
|
cwd: cfg.cwd,
|
|
285
579
|
onLog: (line) => log(job, "log", line),
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
580
|
+
onSpawn: (pid) => writeLog("info", "codex_process_spawn", {
|
|
581
|
+
job_id: job.id,
|
|
582
|
+
attempt: job.attempts ?? 1,
|
|
583
|
+
...pid === void 0 ? {} : { pid }
|
|
584
|
+
}),
|
|
585
|
+
onExit: (code, error) => {
|
|
586
|
+
writeLog(job.state === "starting" || job.state === "running" ? "error" : "info", "codex_process_exit", {
|
|
587
|
+
job_id: job.id,
|
|
588
|
+
attempt: job.attempts ?? 1,
|
|
589
|
+
code,
|
|
590
|
+
error: error.message
|
|
591
|
+
});
|
|
592
|
+
if (job.state === "starting" || job.state === "running") processError = error;
|
|
290
593
|
settle();
|
|
291
594
|
},
|
|
292
595
|
onNotification: (method, params) => handleNotification(method, params)
|
|
@@ -295,18 +598,73 @@ async function runJob(job, opts) {
|
|
|
295
598
|
clearTimeout(quietTimer);
|
|
296
599
|
quietTimer = setTimeout(() => {
|
|
297
600
|
log(job, "quiet", `no goal continuation within ${cfg.quietMs}ms; treating turn as final`);
|
|
298
|
-
|
|
601
|
+
finalize("done");
|
|
299
602
|
settle();
|
|
300
603
|
}, cfg.quietMs);
|
|
301
604
|
}
|
|
605
|
+
function clearStallTimer() {
|
|
606
|
+
clearTimeout(stallTimer);
|
|
607
|
+
stallTimer = void 0;
|
|
608
|
+
}
|
|
609
|
+
function armStallTimer() {
|
|
610
|
+
clearStallTimer();
|
|
611
|
+
if (cfg.stallWarnMs === 0 || !activeTurn || stalled || !job.lastActivityAt) return;
|
|
612
|
+
const stalledForMs = Math.max(0, Date.now() - Date.parse(job.lastActivityAt));
|
|
613
|
+
stallTimer = setTimeout(checkForStall, Math.max(0, cfg.stallWarnMs - stalledForMs));
|
|
614
|
+
}
|
|
615
|
+
function checkForStall() {
|
|
616
|
+
stallTimer = void 0;
|
|
617
|
+
if (!activeTurn || stalled || !job.lastActivityAt) return;
|
|
618
|
+
const stalledForMs = Math.max(0, Date.now() - Date.parse(job.lastActivityAt));
|
|
619
|
+
if (stalledForMs < cfg.stallWarnMs) {
|
|
620
|
+
armStallTimer();
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
stalled = true;
|
|
624
|
+
log(job, "stalled", `no Codex activity for ${stalledForMs}ms`);
|
|
625
|
+
store.save(job);
|
|
626
|
+
writeLog("info", "job_stalled", { job_id: job.id, stalled_for_ms: stalledForMs });
|
|
627
|
+
progress("stalled", `job stalled for ${stalledForMs}ms`);
|
|
628
|
+
}
|
|
629
|
+
function recordActivity() {
|
|
630
|
+
const now = /* @__PURE__ */ new Date();
|
|
631
|
+
if (stalled && job.lastActivityAt) {
|
|
632
|
+
const stalledForMs = Math.max(0, now.getTime() - Date.parse(job.lastActivityAt));
|
|
633
|
+
stalled = false;
|
|
634
|
+
log(job, "resumed", `Codex activity resumed after ${stalledForMs}ms`);
|
|
635
|
+
writeLog("info", "job_resumed", { job_id: job.id, stalled_for_ms: stalledForMs });
|
|
636
|
+
progress("resumed", `job resumed after ${stalledForMs}ms`);
|
|
637
|
+
}
|
|
638
|
+
job.lastActivityAt = now.toISOString();
|
|
639
|
+
store.save(job);
|
|
640
|
+
if (activeTurn) armStallTimer();
|
|
641
|
+
}
|
|
642
|
+
function finalize(state, error) {
|
|
643
|
+
clearStallTimer();
|
|
644
|
+
finish(job, state, client, error);
|
|
645
|
+
}
|
|
302
646
|
function handleNotification(method, params) {
|
|
647
|
+
writeLog("debug", "codex_notification", {
|
|
648
|
+
job_id: job.id,
|
|
649
|
+
attempt: job.attempts ?? 1,
|
|
650
|
+
method,
|
|
651
|
+
params: params ?? null
|
|
652
|
+
});
|
|
653
|
+
recordActivity();
|
|
303
654
|
const p = params ?? {};
|
|
304
655
|
switch (method) {
|
|
305
656
|
case "turn/started": {
|
|
306
657
|
activeTurn = true;
|
|
658
|
+
armStallTimer();
|
|
307
659
|
job.turns += 1;
|
|
308
660
|
clearTimeout(quietTimer);
|
|
661
|
+
if (job.state === "starting") {
|
|
662
|
+
job.state = "running";
|
|
663
|
+
store.save(job);
|
|
664
|
+
writeLog("info", "job_state_changed", { job_id: job.id, from: "starting", to: "running" });
|
|
665
|
+
}
|
|
309
666
|
log(job, "turn", `turn ${job.turns} started`);
|
|
667
|
+
writeLog("info", "turn_started", { job_id: job.id, turn: job.turns });
|
|
310
668
|
progress("turn_started", `turn ${job.turns} started`);
|
|
311
669
|
break;
|
|
312
670
|
}
|
|
@@ -328,11 +686,16 @@ async function runJob(job, opts) {
|
|
|
328
686
|
break;
|
|
329
687
|
}
|
|
330
688
|
case "thread/goal/updated": {
|
|
331
|
-
job
|
|
332
|
-
log(job, "goal", `status=${job.goal
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
689
|
+
updateGoal(job, p);
|
|
690
|
+
log(job, "goal", `status=${job.goal?.status} tokensUsed=${job.goal?.tokensUsed ?? "?"}`);
|
|
691
|
+
writeLog("info", "goal_updated", {
|
|
692
|
+
job_id: job.id,
|
|
693
|
+
status: job.goal?.status ?? null,
|
|
694
|
+
tokens_used: job.goal?.tokensUsed ?? null
|
|
695
|
+
});
|
|
696
|
+
progress("goal_updated", `goal status=${job.goal?.status ?? "unknown"}`);
|
|
697
|
+
if (job.goal?.status && TERMINAL_GOAL_STATUSES.has(job.goal.status) && !activeTurn) {
|
|
698
|
+
finalize("done");
|
|
336
699
|
settle();
|
|
337
700
|
}
|
|
338
701
|
break;
|
|
@@ -340,8 +703,12 @@ async function runJob(job, opts) {
|
|
|
340
703
|
case "turn/completed":
|
|
341
704
|
case "turn/failed": {
|
|
342
705
|
activeTurn = false;
|
|
706
|
+
clearStallTimer();
|
|
707
|
+
mergeUsage(job, extractUsage(p));
|
|
708
|
+
store.save(job);
|
|
343
709
|
const status = p.turn?.status ?? (method === "turn/failed" ? "failed" : "completed");
|
|
344
710
|
log(job, "turn", `turn ended (${status})`);
|
|
711
|
+
writeLog("info", "turn_ended", { job_id: job.id, turn: job.turns, status });
|
|
345
712
|
progress("turn_ended", `turn ended (${status})`);
|
|
346
713
|
void onTurnEnded();
|
|
347
714
|
break;
|
|
@@ -352,21 +719,21 @@ async function runJob(job, opts) {
|
|
|
352
719
|
}
|
|
353
720
|
async function onTurnEnded() {
|
|
354
721
|
if (!job.goalSet) {
|
|
355
|
-
|
|
722
|
+
finalize("done");
|
|
356
723
|
settle();
|
|
357
724
|
return;
|
|
358
725
|
}
|
|
359
726
|
if (job.goal?.status && TERMINAL_GOAL_STATUSES.has(job.goal.status)) {
|
|
360
|
-
|
|
727
|
+
finalize("done");
|
|
361
728
|
settle();
|
|
362
729
|
return;
|
|
363
730
|
}
|
|
364
731
|
try {
|
|
365
732
|
const res = await client.request("thread/goal/get", { threadId: job.threadId }, 15e3);
|
|
366
733
|
if (res?.goal) {
|
|
367
|
-
job
|
|
368
|
-
if (job.goal
|
|
369
|
-
|
|
734
|
+
updateGoal(job, res);
|
|
735
|
+
if (job.goal?.status && TERMINAL_GOAL_STATUSES.has(job.goal.status)) {
|
|
736
|
+
finalize("done");
|
|
370
737
|
settle();
|
|
371
738
|
return;
|
|
372
739
|
}
|
|
@@ -377,9 +744,9 @@ async function runJob(job, opts) {
|
|
|
377
744
|
if (!activeTurn) armQuietTimer();
|
|
378
745
|
}
|
|
379
746
|
const overallTimer = setTimeout(() => {
|
|
380
|
-
|
|
747
|
+
finalize("timeout", `job exceeded ${cfg.jobTimeoutMs}ms`);
|
|
381
748
|
settle();
|
|
382
|
-
},
|
|
749
|
+
}, timeoutMs);
|
|
383
750
|
try {
|
|
384
751
|
await client.initialize();
|
|
385
752
|
const threadParams = {
|
|
@@ -399,6 +766,7 @@ async function runJob(job, opts) {
|
|
|
399
766
|
job.threadId = threadRes?.thread?.id ?? threadRes?.id;
|
|
400
767
|
}
|
|
401
768
|
if (!job.threadId) throw new Error(`could not determine thread id from ${JSON.stringify(threadRes).slice(0, 300)}`);
|
|
769
|
+
store.save(job);
|
|
402
770
|
log(job, "thread", `thread ${job.threadId} ${opts.resumeThreadId ? "resumed" : "started"}`);
|
|
403
771
|
if (opts.objective) {
|
|
404
772
|
try {
|
|
@@ -408,24 +776,29 @@ async function runJob(job, opts) {
|
|
|
408
776
|
...opts.tokenBudget ? { tokenBudget: opts.tokenBudget } : {}
|
|
409
777
|
});
|
|
410
778
|
job.goalSet = true;
|
|
411
|
-
job
|
|
412
|
-
log(job, "goal", `goal set (status=${job.goal
|
|
413
|
-
progress("goal_updated", `goal status=${job.goal
|
|
779
|
+
updateGoal(job, goalRes);
|
|
780
|
+
log(job, "goal", `goal set (status=${job.goal?.status ?? "active"})`);
|
|
781
|
+
progress("goal_updated", `goal status=${job.goal?.status ?? "active"}`);
|
|
414
782
|
} catch (err) {
|
|
783
|
+
if (client.exited) throw err;
|
|
415
784
|
log(job, "goal", `thread/goal/set failed (${err?.message ?? err}); continuing without goal loop`);
|
|
416
785
|
job.goalSet = false;
|
|
417
786
|
}
|
|
418
787
|
}
|
|
419
|
-
job.state = "running";
|
|
420
788
|
await client.request("turn/start", {
|
|
421
789
|
threadId: job.threadId,
|
|
422
790
|
input: [{ type: "text", text: opts.prompt }],
|
|
423
791
|
...opts.reasoningEffort ? { effort: opts.reasoningEffort } : {}
|
|
424
792
|
});
|
|
425
793
|
await doneSignal;
|
|
794
|
+
return processError ? { error: processError, processFailure: true } : { processFailure: false };
|
|
795
|
+
} catch (error) {
|
|
796
|
+
const failure = processError ?? (error instanceof Error ? error : new Error(String(error)));
|
|
797
|
+
return { error: failure, processFailure: client.exited };
|
|
426
798
|
} finally {
|
|
427
799
|
clearTimeout(overallTimer);
|
|
428
800
|
clearTimeout(quietTimer);
|
|
801
|
+
clearStallTimer();
|
|
429
802
|
client.kill();
|
|
430
803
|
}
|
|
431
804
|
}
|
|
@@ -463,10 +836,14 @@ function config(cwd) {
|
|
|
463
836
|
approvalPolicy: process.env.CODEX_APPROVAL_POLICY ?? "never",
|
|
464
837
|
permissions: process.env.CODEX_PERMISSIONS || void 0,
|
|
465
838
|
jobTimeoutMs: envInt("CODEX_JOB_TIMEOUT_MS", 18e5),
|
|
466
|
-
quietMs: envInt("CODEX_QUIET_MS", 3e4)
|
|
839
|
+
quietMs: envInt("CODEX_QUIET_MS", 3e4),
|
|
840
|
+
stallWarnMs: Math.max(0, envInt("C2C_STALL_WARN_MS", 12e4)),
|
|
841
|
+
retries: Math.max(0, envInt("C2C_RETRIES", 1)),
|
|
842
|
+
maxConcurrent: Math.max(1, envInt("C2C_MAX_CONCURRENT", 2))
|
|
467
843
|
};
|
|
468
844
|
}
|
|
469
845
|
var OBJECTIVE_MAX = envInt("GOAL_OBJECTIVE_MAX", 2e3);
|
|
846
|
+
var objectiveBudgetDescription = `The rendered thread-goal objective has a GOAL_OBJECTIVE_MAX budget of ${OBJECTIVE_MAX} characters; over-limit calls are rejected.`;
|
|
470
847
|
function jobSummary(job, transcriptTail = 15) {
|
|
471
848
|
return {
|
|
472
849
|
job_id: job.id,
|
|
@@ -474,10 +851,17 @@ function jobSummary(job, transcriptTail = 15) {
|
|
|
474
851
|
state: job.state,
|
|
475
852
|
thread_id: job.threadId ?? null,
|
|
476
853
|
goal: job.goal ?? null,
|
|
854
|
+
usage: job.usage ?? {},
|
|
855
|
+
attempts: job.attempts ?? 1,
|
|
477
856
|
turns: job.turns,
|
|
478
857
|
started_at: job.startedAt,
|
|
479
858
|
ended_at: job.endedAt ?? null,
|
|
480
859
|
error: job.error ?? null,
|
|
860
|
+
...!["done", "error", "timeout"].includes(job.state) ? { last_activity_at: job.lastActivityAt ?? null } : {},
|
|
861
|
+
...job.state === "running" && job.lastActivityAt ? { seconds_since_activity: Math.max(0, (Date.now() - Date.parse(job.lastActivityAt)) / 1e3) } : {},
|
|
862
|
+
...job.contextFiles ? { context_files: job.contextFiles } : {},
|
|
863
|
+
...job.dependsOn ? { depends_on: job.dependsOn } : {},
|
|
864
|
+
...job.state === "queued" ? { queue_position: getQueuePosition(job.id) ?? null } : {},
|
|
481
865
|
transcript_tail: job.transcript.slice(-transcriptTail)
|
|
482
866
|
};
|
|
483
867
|
}
|
|
@@ -487,8 +871,53 @@ function textResult(obj, isError = false) {
|
|
|
487
871
|
isError
|
|
488
872
|
};
|
|
489
873
|
}
|
|
490
|
-
var server = new McpServer({ name: "c2c-codex", version: "0.1
|
|
874
|
+
var server = new McpServer({ name: "c2c-codex", version: "0.3.1" });
|
|
491
875
|
var reasoningEffort = z.enum(["low", "medium", "high", "xhigh", "max", "ultra"]);
|
|
876
|
+
var contextFiles = z.array(z.object({
|
|
877
|
+
path: z.string().min(1),
|
|
878
|
+
note: z.string().optional()
|
|
879
|
+
}));
|
|
880
|
+
async function resolveContextFiles(files, cwd) {
|
|
881
|
+
if (!files?.length) return { files: void 0, missing: [] };
|
|
882
|
+
const resolved = files.map((file) => ({ ...file, path: resolve(cwd, file.path) }));
|
|
883
|
+
const exists = await Promise.all(resolved.map(async (file) => {
|
|
884
|
+
try {
|
|
885
|
+
await stat(file.path);
|
|
886
|
+
return true;
|
|
887
|
+
} catch {
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
}));
|
|
891
|
+
return {
|
|
892
|
+
files: resolved,
|
|
893
|
+
missing: resolved.filter((_, index) => !exists[index]).map((file) => file.path)
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
function missingContextFiles(paths) {
|
|
897
|
+
return textResult({ error: `context_files not found:
|
|
898
|
+
${paths.map((path) => `- ${path}`).join("\n")}` }, true);
|
|
899
|
+
}
|
|
900
|
+
function implementHistory() {
|
|
901
|
+
const totals = listJobs().filter((job) => job.kind === "implement" && job.state === "done").map((job) => job.usage?.totalTokens).filter((total) => typeof total === "number" && Number.isFinite(total)).sort((a, b) => a - b);
|
|
902
|
+
if (!totals.length) return null;
|
|
903
|
+
const middle = Math.floor(totals.length / 2);
|
|
904
|
+
const median = totals.length % 2 ? totals[middle] : (totals[middle - 1] + totals[middle]) / 2;
|
|
905
|
+
return {
|
|
906
|
+
samples: totals.length,
|
|
907
|
+
median_total_tokens: Math.round(median),
|
|
908
|
+
p90_total_tokens: Math.round(totals[Math.ceil(totals.length * 0.9) - 1]),
|
|
909
|
+
mean_total_tokens: Math.round(totals.reduce((sum, total) => sum + total, 0) / totals.length)
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
function dependencyValidationError(dependsOn) {
|
|
913
|
+
if (!dependsOn) return void 0;
|
|
914
|
+
const dependency = getJob(dependsOn);
|
|
915
|
+
if (!dependency) return `unknown depends_on job_id ${dependsOn}`;
|
|
916
|
+
if (dependency.state === "error" || dependency.state === "timeout") {
|
|
917
|
+
return `dependency ${dependency.id} failed: ${dependency.error ?? `state ${dependency.state}`}`;
|
|
918
|
+
}
|
|
919
|
+
return void 0;
|
|
920
|
+
}
|
|
492
921
|
function progressReporter() {
|
|
493
922
|
let sequence = 0;
|
|
494
923
|
return (event) => {
|
|
@@ -509,26 +938,42 @@ server.registerTool(
|
|
|
509
938
|
title: "Delegate a Goal Contract to Codex",
|
|
510
939
|
description: "Start a Codex implementation job for a Goal Contract. Sets the contract as a Codex thread goal (the /goal loop keeps Codex iterating until the objective is achieved or the token budget is hit) and sends the full contract as the first turn. Returns a job_id immediately; poll codex_status, then fetch codex_result when done. Language-agnostic: verification commands come from the contract's Success Conditions, not from this server.",
|
|
511
940
|
inputSchema: {
|
|
512
|
-
goal: z.string().min(1).describe(
|
|
941
|
+
goal: z.string().min(1).describe(`What the code must do when this task is done. ${objectiveBudgetDescription}`),
|
|
513
942
|
constraints: z.array(z.string()).describe("Technical boundaries: files to touch, patterns to follow, 'Do not modify' lists."),
|
|
514
|
-
success_conditions: z.array(z.string()).min(1).describe(
|
|
943
|
+
success_conditions: z.array(z.string()).min(1).describe(`Checkable criteria proving the goal is met. At least one must be a runnable command/test. ${objectiveBudgetDescription}`),
|
|
515
944
|
token_budget: z.number().int().positive().optional().describe("Optional token budget for the Codex goal loop (thread/goal/set tokenBudget)."),
|
|
516
945
|
cwd: z.string().optional().describe("Working directory for Codex. Defaults to the project directory."),
|
|
946
|
+
context_files: contextFiles.optional().describe("Files or directories Codex should read before implementing."),
|
|
947
|
+
depends_on: z.string().min(1).optional().describe("Job id of a prior job that must succeed before this job starts."),
|
|
517
948
|
reasoning_effort: reasoningEffort.optional().describe("Optional Codex reasoning effort; omitted uses the Codex default.")
|
|
518
949
|
}
|
|
519
950
|
},
|
|
520
951
|
async (input) => {
|
|
952
|
+
const jobConfig = config(input.cwd);
|
|
953
|
+
const resolvedContext = await resolveContextFiles(input.context_files, jobConfig.cwd);
|
|
954
|
+
if (resolvedContext.missing.length) return missingContextFiles(resolvedContext.missing);
|
|
521
955
|
const contract = {
|
|
522
956
|
goal: input.goal,
|
|
523
957
|
constraints: input.constraints,
|
|
524
|
-
success_conditions: input.success_conditions
|
|
958
|
+
success_conditions: input.success_conditions,
|
|
959
|
+
context_files: resolvedContext.files
|
|
525
960
|
};
|
|
526
|
-
const
|
|
961
|
+
const prompt = renderGoalContract(contract);
|
|
962
|
+
const dependencyError = dependencyValidationError(input.depends_on);
|
|
963
|
+
if (dependencyError) return textResult({ error: dependencyError }, true);
|
|
964
|
+
const renderedObjective = renderObjectiveDetails(contract, OBJECTIVE_MAX);
|
|
965
|
+
if (renderedObjective.objectiveChars > OBJECTIVE_MAX) {
|
|
966
|
+
return textResult({
|
|
967
|
+
error: `Rendered objective is ${renderedObjective.objectiveChars} characters, exceeding the GOAL_OBJECTIVE_MAX limit of ${OBJECTIVE_MAX}. Shorten goal or success_conditions, or raise GOAL_OBJECTIVE_MAX.`
|
|
968
|
+
}, true);
|
|
969
|
+
}
|
|
527
970
|
const job = startJob({
|
|
528
971
|
kind: "implement",
|
|
529
|
-
prompt
|
|
530
|
-
objective:
|
|
972
|
+
prompt,
|
|
973
|
+
objective: renderedObjective.objective,
|
|
531
974
|
tokenBudget: input.token_budget,
|
|
975
|
+
contextFiles: resolvedContext.files?.map((file) => file.path),
|
|
976
|
+
dependsOn: input.depends_on,
|
|
532
977
|
config: jobConfig,
|
|
533
978
|
reasoningEffort: input.reasoning_effort,
|
|
534
979
|
onProgress: progressReporter()
|
|
@@ -536,7 +981,46 @@ server.registerTool(
|
|
|
536
981
|
return textResult({
|
|
537
982
|
job_id: job.id,
|
|
538
983
|
state: job.state,
|
|
539
|
-
note: "Job started. Poll codex_status until state is 'done', then call codex_result."
|
|
984
|
+
note: job.state === "blocked" ? `Job blocked on dependency ${job.dependsOn}. Poll codex_status for its state.` : job.state === "queued" ? "Job queued. Poll codex_status for its queue position and state." : "Job started. Poll codex_status until state is 'done', then call codex_result."
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
);
|
|
988
|
+
server.registerTool(
|
|
989
|
+
"codex_estimate",
|
|
990
|
+
{
|
|
991
|
+
title: "Estimate a Goal Contract's token cost",
|
|
992
|
+
description: "Read-only estimate made before starting a job. Measures the fully rendered Goal Contract prompt and returns approx_prompt_tokens using an approximate characters/4 heuristic, plus local completed-job history.",
|
|
993
|
+
inputSchema: {
|
|
994
|
+
goal: z.string().min(1).describe(`What the code must do when this task is done. ${objectiveBudgetDescription}`),
|
|
995
|
+
constraints: z.array(z.string()).describe("Technical boundaries: files to touch, patterns to follow, 'Do not modify' lists."),
|
|
996
|
+
success_conditions: z.array(z.string()).min(1).describe(`Checkable criteria proving the goal is met. At least one must be a runnable command/test. ${objectiveBudgetDescription}`),
|
|
997
|
+
cwd: z.string().optional().describe("Working directory used to resolve relative context_files paths. Defaults to the project directory."),
|
|
998
|
+
context_files: contextFiles.optional().describe("Files or directories included in the rendered prompt.")
|
|
999
|
+
},
|
|
1000
|
+
annotations: { readOnlyHint: true }
|
|
1001
|
+
},
|
|
1002
|
+
async (input) => {
|
|
1003
|
+
const jobConfig = config(input.cwd);
|
|
1004
|
+
const resolvedContext = await resolveContextFiles(input.context_files, jobConfig.cwd);
|
|
1005
|
+
if (resolvedContext.missing.length) return missingContextFiles(resolvedContext.missing);
|
|
1006
|
+
const contract = {
|
|
1007
|
+
goal: input.goal,
|
|
1008
|
+
constraints: input.constraints,
|
|
1009
|
+
success_conditions: input.success_conditions,
|
|
1010
|
+
context_files: resolvedContext.files
|
|
1011
|
+
};
|
|
1012
|
+
const prompt = renderGoalContract(contract);
|
|
1013
|
+
const renderedObjective = renderObjectiveDetails(contract, OBJECTIVE_MAX);
|
|
1014
|
+
const history = implementHistory();
|
|
1015
|
+
return textResult({
|
|
1016
|
+
prompt_chars: prompt.length,
|
|
1017
|
+
approx_prompt_tokens: Math.ceil(prompt.length / 4),
|
|
1018
|
+
objective_chars: renderedObjective.objectiveChars,
|
|
1019
|
+
objective_max_chars: OBJECTIVE_MAX,
|
|
1020
|
+
objective_over_limit: renderedObjective.objectiveChars > OBJECTIVE_MAX,
|
|
1021
|
+
history,
|
|
1022
|
+
estimated_total_tokens: history?.median_total_tokens ?? null,
|
|
1023
|
+
note: history ? "Estimated total tokens are based on local history and vary with task complexity." : "No completed implement job history is available; estimates based on local history vary with task complexity."
|
|
540
1024
|
});
|
|
541
1025
|
}
|
|
542
1026
|
);
|
|
@@ -552,10 +1036,14 @@ server.registerTool(
|
|
|
552
1036
|
failed_conditions: z.array(z.string()).describe("The specific Success Conditions from the original contract that did not pass."),
|
|
553
1037
|
constraints: z.array(z.string()).optional().describe("Additional constraints beyond the originals."),
|
|
554
1038
|
cwd: z.string().optional().describe("Working directory for Codex. Defaults to the project directory."),
|
|
1039
|
+
context_files: contextFiles.optional().describe("Files or directories Codex should read before implementing."),
|
|
555
1040
|
reasoning_effort: reasoningEffort.optional().describe("Optional Codex reasoning effort; omitted uses the Codex default.")
|
|
556
1041
|
}
|
|
557
1042
|
},
|
|
558
1043
|
async (input) => {
|
|
1044
|
+
const jobConfig = config(input.cwd);
|
|
1045
|
+
const resolvedContext = await resolveContextFiles(input.context_files, jobConfig.cwd);
|
|
1046
|
+
if (resolvedContext.missing.length) return missingContextFiles(resolvedContext.missing);
|
|
559
1047
|
let threadId = input.thread_id;
|
|
560
1048
|
if (!threadId && input.job_id) {
|
|
561
1049
|
const prev = getJob(input.job_id);
|
|
@@ -570,10 +1058,12 @@ server.registerTool(
|
|
|
570
1058
|
prompt: renderDeltaContract({
|
|
571
1059
|
findings: input.findings,
|
|
572
1060
|
failed_conditions: input.failed_conditions,
|
|
573
|
-
constraints: input.constraints
|
|
1061
|
+
constraints: input.constraints,
|
|
1062
|
+
context_files: resolvedContext.files
|
|
574
1063
|
}),
|
|
575
1064
|
resumeThreadId: threadId,
|
|
576
|
-
|
|
1065
|
+
contextFiles: resolvedContext.files?.map((file) => file.path),
|
|
1066
|
+
config: jobConfig,
|
|
577
1067
|
reasoningEffort: input.reasoning_effort,
|
|
578
1068
|
onProgress: progressReporter()
|
|
579
1069
|
});
|
|
@@ -581,7 +1071,7 @@ server.registerTool(
|
|
|
581
1071
|
job_id: job.id,
|
|
582
1072
|
resumed_thread_id: threadId,
|
|
583
1073
|
state: job.state,
|
|
584
|
-
note: "Rework job started. Poll codex_status until state is 'done', then call codex_result."
|
|
1074
|
+
note: job.state === "queued" ? "Rework job queued. Poll codex_status for its queue position and state." : "Rework job started. Poll codex_status until state is 'done', then call codex_result."
|
|
585
1075
|
});
|
|
586
1076
|
}
|
|
587
1077
|
);
|
|
@@ -601,7 +1091,7 @@ server.registerTool(
|
|
|
601
1091
|
"codex_status",
|
|
602
1092
|
{
|
|
603
1093
|
title: "Check a Codex job",
|
|
604
|
-
description: "Status of a running or finished Codex job: state, thread id, goal state (status/tokens), turn count, and a tail of the activity transcript.",
|
|
1094
|
+
description: "Status of a blocked, queued, running, or finished Codex job: dependency, state, queue position, thread id, goal state (status/tokens), turn count, and a tail of the activity transcript.",
|
|
605
1095
|
inputSchema: {
|
|
606
1096
|
job_id: z.string().describe("Job id returned by codex_implement or codex_rework.")
|
|
607
1097
|
}
|
|
@@ -616,7 +1106,7 @@ server.registerTool(
|
|
|
616
1106
|
"codex_result",
|
|
617
1107
|
{
|
|
618
1108
|
title: "Fetch a finished Codex job's handoff",
|
|
619
|
-
description: "Final result of a finished job: Codex's final message, the parsed structured handoff (Changed Files / Validation / Success Conditions / Risks & Deviations \u2014 a missing section is itself a review failure), and final goal state. Errors if the job is
|
|
1109
|
+
description: "Final result of a finished job: Codex's final message, the parsed structured handoff (Changed Files / Validation / Success Conditions / Risks & Deviations \u2014 a missing section is itself a review failure), and final goal state. Errors if the job is not finished.",
|
|
620
1110
|
inputSchema: {
|
|
621
1111
|
job_id: z.string().describe("Job id returned by codex_implement or codex_rework.")
|
|
622
1112
|
}
|
|
@@ -624,7 +1114,7 @@ server.registerTool(
|
|
|
624
1114
|
async (input) => {
|
|
625
1115
|
const job = getJob(input.job_id);
|
|
626
1116
|
if (!job) return textResult({ error: `unknown job_id ${input.job_id}` }, true);
|
|
627
|
-
if (job.state === "starting" || job.state === "running") {
|
|
1117
|
+
if (job.state === "blocked" || job.state === "queued" || job.state === "starting" || job.state === "running") {
|
|
628
1118
|
return textResult({ error: `job ${job.id} is still ${job.state}; poll codex_status` }, true);
|
|
629
1119
|
}
|
|
630
1120
|
return textResult({
|