claude2codex 0.3.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 CHANGED
@@ -73,6 +73,27 @@ And the meta-rule for the handoff: **require it in the contract.** Every contrac
73
73
 
74
74
  A missing or incomplete handoff is itself a review failure — you never have to argue about it, because it was a success condition.
75
75
 
76
+ ## The objective budget: your success conditions have a character limit
77
+
78
+ The contract you send and the objective the goal loop audits are **not the same string**. `codex_implement` renders a compact objective from your `goal` plus every `success_condition` and sets it via `thread/goal/set`. That objective — not the full contract prompt — is what Codex's `/goal` loop checks before declaring the goal achieved. Constraints and context files never reach it; they live only in the turn prompt.
79
+
80
+ It has a hard budget: `GOAL_OBJECTIVE_MAX`, default **2000 characters**, of which ~213 are fixed rendering boilerplate. The remaining ~1787 are your goal text plus the concatenation of every success condition.
81
+
82
+ Two consequences:
83
+
84
+ - **Prose burns the budget fast.** A ten-condition contract of ordinary English sentences lands around 1700–2000 characters. The contract that added this very check measured 1987/2000 on its first draft — 13 characters of margin, entirely by accident.
85
+ - **Measure before you send.** `codex_estimate` reports `objective_chars`, `objective_max_chars`, and `objective_over_limit` without creating a job. Over-limit calls to `codex_implement` are rejected at submit time — no job, no process, no tokens spent — with the actual count in the error.
86
+
87
+ Write success conditions as assertions, not paragraphs:
88
+
89
+ ```markdown
90
+ Bad (124 chars): The implementation should make sure that whenever a contract
91
+ exceeds the configured limit, the tool refuses to create a job.
92
+ Good (76 chars): Over-limit contracts return isError and create no job (state dir unchanged).
93
+ ```
94
+
95
+ If a task genuinely needs a longer objective, raise `GOAL_OBJECTIVE_MAX` deliberately. The usual right answer is that ten crisp conditions beat six verbose ones — and the crisp ones review better anyway.
96
+
76
97
  ## Review: never trust, always re-run
77
98
 
78
99
  The handoff says `bun test` passed. Run `bun test` anyway. In this repo's run every handoff happened to be truthful — and that is exactly what you can't assume, because the day one isn't, the re-run is your only defense. The review loop that worked:
package/PATTERNS.zh-CN.md CHANGED
@@ -72,6 +72,27 @@ codex_implement 必须支持声明对先前 job 的依赖:被依赖的 job 成
72
72
 
73
73
  缺失或不完整的 handoff 本身就是审查失败——你永远不需要为此争论,因为它就是一条成功条件。
74
74
 
75
+ ## objective 预算:你的 Success Conditions 有字符上限
76
+
77
+ 你发出去的合同,和 goal loop 实际审的 objective,**不是同一个字符串**。`codex_implement` 会从 `goal` 加上每一条 `success_condition` 渲染出一个精简的 objective,通过 `thread/goal/set` 设进去。Codex 的 `/goal` 循环在宣布目标达成前,审的是这个 objective,而不是完整的合同 prompt。Constraints 和 context files 根本进不去 —— 它们只存在于 turn prompt 里。
78
+
79
+ 它有一条硬预算:`GOAL_OBJECTIVE_MAX`,默认 **2000 字符**,其中约 213 是固定的渲染样板。剩下约 1787 字符由你的 goal 正文和所有 success conditions 拼接而成。
80
+
81
+ 两个后果:
82
+
83
+ - **散文最烧预算。** 十条普通英文句子写成的 success conditions,落点大约在 1700–2000 字符。加入这条检查的那份合同,初稿实测 1987/2000 —— 只剩 13 字符余量,纯属侥幸。
84
+ - **发之前先量。** `codex_estimate` 会返回 `objective_chars`、`objective_max_chars`、`objective_over_limit`,且不创建 job。超限的 `codex_implement` 调用会在提交时被拒 —— 不建 job、不起进程、不烧 token —— 错误里直接给出实际字符数。
85
+
86
+ Success conditions 要写成断言,不要写成段落:
87
+
88
+ ```markdown
89
+ 差(124 字符):The implementation should make sure that whenever a contract
90
+ exceeds the configured limit, the tool refuses to create a job.
91
+ 好( 76 字符):Over-limit contracts return isError and create no job (state dir unchanged).
92
+ ```
93
+
94
+ 如果某个任务确实需要更长的 objective,那就有意识地调高 `GOAL_OBJECTIVE_MAX`。但通常正确的答案是:十条精悍的条件胜过六条啰嗦的 —— 而且精悍的那种审查起来也更省事。
95
+
75
96
  ## 审查:永不轻信,永远重跑
76
97
 
77
98
  handoff 说 `bun test` 过了。你还是要自己跑一遍 `bun test`。在本仓库的实战中,每一份 handoff 碰巧都是诚实的——而这恰恰是你不能假设的事,因为哪天有一份不诚实,你的重跑就是唯一的防线。行之有效的审查循环:
package/README.md CHANGED
@@ -4,6 +4,14 @@ An MCP server that lets Claude Code delegate implementation work to OpenAI Codex
4
4
 
5
5
  *[中文版](./README.zh-CN.md)*
6
6
 
7
+ ## Demo
8
+
9
+ One real contract round-trip, recorded live: Claude drafts the Goal Contract, `codex_estimate` prices it, Codex implements under its goal loop while `codex_status` streams progress, and Claude independently re-verifies the handoff.
10
+
11
+ ![c2c demo — a full Claude→Codex contract round-trip](./demo/demo.gif)
12
+
13
+ *Real Codex run (13k tokens, 50s), waits compressed. Replay in your terminal: `asciinema play demo/demo.cast`*
14
+
7
15
  ## The Idea
8
16
 
9
17
  Software development has distinct roles: **planning** what to build, **building** it, and **reviewing** the result. This project splits those roles between two AI agents:
@@ -82,15 +90,17 @@ flowchart TD
82
90
 
83
91
  ## Three-Layer Prompt Architecture
84
92
 
85
- When claude2codex sends work to Codex, the prompt is assembled from three layers:
93
+ The prompt Codex works from is assembled from three layers, but only two of them come from claude2codex:
86
94
 
87
95
  1. **Protocol layer** (embedded in the MCP server) — universal rules: how to self-verify work, how to format the handoff, how to handle rework. These never change regardless of project.
88
96
 
89
- 2. **Project layer** (optional `AGENTS.md` in the working directory) — project-specific coding conventions, toolchain instructions, architectural constraints. Injected automatically when present.
97
+ 2. **Project layer** (`AGENTS.md`) — project-specific coding conventions, toolchain instructions, architectural constraints. **Loaded by Codex itself, not by this server.** Codex discovers `AGENTS.md` files across the working directory's repository tree (plus `AGENTS.override.md`) and places them in its instructions layer, subject to its own `project_doc_max_bytes` budget. claude2codex used to inject the file a second time and no longer does.
90
98
 
91
99
  3. **Task layer** (the Goal/Delta Contract itself) — the specific work to be done this time.
92
100
 
93
- This separation means the MCP server works out of the box (layer 1 is always there), benefits from project context when available (layer 2), and carries the unique task specification (layer 3).
101
+ This separation means the MCP server works out of the box (layer 1 is always there), inherits whatever project context Codex already loads (layer 2), and carries the unique task specification (layer 3).
102
+
103
+ Note that the contract prompt and the **thread-goal objective** are different strings. The objective — rendered from `goal` plus every `success_condition` — is what Codex's `/goal` loop audits before declaring the goal achieved, and it has its own `GOAL_OBJECTIVE_MAX` budget. See [PATTERNS.md](PATTERNS.md#the-objective-budget-your-success-conditions-have-a-character-limit).
94
104
 
95
105
  ## Installation
96
106
 
@@ -123,7 +133,7 @@ Add to your Claude Code MCP settings:
123
133
  | `codex_status` | Check job progress: state, queue position, dependency, turns, goal status, token usage, time since last activity, transcript. |
124
134
  | `codex_result` | Fetch the finished job's structured handoff. |
125
135
  | `codex_rework` | Resume the Codex thread with a Delta Contract (also accepts `context_files`). |
126
- | `codex_estimate` | Read-only pre-job cost estimate: rendered-prompt size plus token statistics from your local completed jobs. |
136
+ | `codex_estimate` | Read-only pre-job cost estimate: rendered-prompt size, thread-goal objective size against `GOAL_OBJECTIVE_MAX`, plus token statistics from your local completed jobs. |
127
137
  | `codex_config` | Read-only view of Codex's current model, version, and settings. |
128
138
 
129
139
  Jobs are persisted (`C2C_STATE_DIR`), so status, results, and rework survive server restarts. Jobs beyond the concurrency cap queue FIFO; a stall watchdog reports when a running job stops making progress. See [PATTERNS.md](./PATTERNS.md) for field-tested guidance on writing contracts that pass review on the first try.
@@ -151,7 +161,7 @@ Codex then enters its goal-continuation loop: after each turn, it checks "did I
151
161
  | `CODEX_PERMISSIONS` | Unset | Permissions profile passed to Codex |
152
162
  | `CODEX_JOB_TIMEOUT_MS` | `1800000` | Maximum job duration (30 min) |
153
163
  | `CODEX_QUIET_MS` | `30000` | Quiet period before treating an active goal as finished |
154
- | `GOAL_OBJECTIVE_MAX` | `2000` | Maximum goal-objective string length |
164
+ | `GOAL_OBJECTIVE_MAX` | `2000` | Maximum rendered goal-objective length; `codex_implement` rejects over-limit contracts at submit time |
155
165
  | `C2C_STATE_DIR` | `~/.claude2codex/jobs` | Persistent job registry (atomic per-job JSON files) |
156
166
  | `C2C_LOG_LEVEL` | `info` | Structured JSON logs on stderr: `silent`, `error`, `info`, `debug` |
157
167
  | `C2C_RETRIES` | `1` | Automatic retries for process failures before the first turn starts |
package/README.zh-CN.md CHANGED
@@ -4,6 +4,14 @@
4
4
 
5
5
  *[English](./README.md)*
6
6
 
7
+ ## 演示
8
+
9
+ 一次真实的契约往返实录:Claude 起草 Goal Contract,`codex_estimate` 预估成本,Codex 在 goal loop 下自主实现(`codex_status` 实时流式进度),最后 Claude 独立复核 handoff。
10
+
11
+ ![c2c 演示 — 完整的 Claude→Codex 契约往返](./demo/demo.gif)
12
+
13
+ *真实 Codex 运行(13k tokens,50 秒),等待时间已压缩。终端回放:`asciinema play demo/demo.cast`*
14
+
7
15
  ## 核心理念
8
16
 
9
17
  软件开发有不同的角色:**规划**做什么、**动手**写代码、**审查**结果。本项目把这些角色分配给两个 AI 智能体:
@@ -82,15 +90,17 @@ flowchart TD
82
90
 
83
91
  ## 三层 Prompt 架构
84
92
 
85
- claude2codex 向 Codex 发送任务时,prompt 由三层组装而成:
93
+ Codex 最终工作时的 prompt 由三层组成,但只有两层来自 claude2codex:
86
94
 
87
95
  1. **协议层**(内嵌在 MCP 服务器中)— 通用规则:如何自我验证、如何格式化交接报告、如何处理返工。无论什么项目都一样。
88
96
 
89
- 2. **项目层**(可选,工作目录中的 `AGENTS.md`)— 项目特定的编码规范、工具链说明、架构约束。存在时自动注入。
97
+ 2. **项目层**(`AGENTS.md`)— 项目特定的编码规范、工具链说明、架构约束。**由 Codex 自己加载,不是本服务器注入的。** Codex 会在工作目录所属的仓库树中查找 `AGENTS.md`(以及 `AGENTS.override.md`),放进它自己的 instructions 层,并受其 `project_doc_max_bytes` 预算约束。claude2codex 过去会重复注入一遍,现已移除。
90
98
 
91
99
  3. **任务层**(Goal/Delta Contract 本身)— 这次要做的具体工作。
92
100
 
93
- 这种分离意味着:MCP 服务器开箱即用(第 1 层始终存在),有项目上下文时更好(第 2 层),每次任务都有独特的规格说明(第 3 层)。
101
+ 这种分离意味着:MCP 服务器开箱即用(第 1 层始终存在),直接继承 Codex 已经加载的项目上下文(第 2 层),每次任务都有独特的规格说明(第 3 层)。
102
+
103
+ 注意:合同 prompt 和 **thread-goal objective** 是两个不同的字符串。objective 由 `goal` 加上每一条 `success_condition` 渲染而成,是 Codex `/goal` 循环在宣布目标达成前实际审查的对象,并有自己的 `GOAL_OBJECTIVE_MAX` 预算。详见 [PATTERNS.zh-CN.md](PATTERNS.zh-CN.md#objective-预算你的-success-conditions-有字符上限)。
94
104
 
95
105
  ## 安装
96
106
 
@@ -123,7 +133,7 @@ npx claude2codex
123
133
  | `codex_status` | 查看任务进度:状态、队列位置、依赖、轮次、目标状态、token 用量、距上次活动的时间、活动记录。 |
124
134
  | `codex_result` | 获取已完成任务的结构化交接报告。 |
125
135
  | `codex_rework` | 用 Delta Contract 恢复 Codex 线程进行返工(同样支持 `context_files`)。 |
126
- | `codex_estimate` | 只读的任务前成本预估:渲染后的 prompt 规模 + 本机已完成任务的 token 统计。 |
136
+ | `codex_estimate` | 只读的任务前成本预估:渲染后的 prompt 规模、thread-goal objective 相对 `GOAL_OBJECTIVE_MAX` 的规模,以及本机已完成任务的 token 统计。 |
127
137
  | `codex_config` | 只读查看 Codex 当前模型、版本和配置。 |
128
138
 
129
139
  任务会持久化(`C2C_STATE_DIR`),因此状态查询、结果获取和返工在 server 重启后依然可用。超出并发上限的任务按 FIFO 排队;停滞看门狗会在运行中的任务失去进展时主动上报。关于如何写出"一次过审"的合同,参见实战指南 [PATTERNS.zh-CN.md](./PATTERNS.zh-CN.md)。
@@ -151,7 +161,7 @@ npx claude2codex
151
161
  | `CODEX_PERMISSIONS` | 未设置 | 权限配置透传 |
152
162
  | `CODEX_JOB_TIMEOUT_MS` | `1800000` | 单任务最大时长(30 分钟) |
153
163
  | `CODEX_QUIET_MS` | `30000` | 判定活跃目标已完成前的静默等待时间 |
154
- | `GOAL_OBJECTIVE_MAX` | `2000` | 目标字符串最大长度 |
164
+ | `GOAL_OBJECTIVE_MAX` | `2000` | 渲染后 goal-objective 的最大长度;超限的合同会被 `codex_implement` 在提交时拒绝 |
155
165
  | `C2C_STATE_DIR` | `~/.claude2codex/jobs` | 持久化任务注册表(每任务一个原子写入的 JSON 文件) |
156
166
  | `C2C_LOG_LEVEL` | `info` | stderr 上的结构化 JSON 日志:`silent`、`error`、`info`、`debug` |
157
167
  | `C2C_RETRIES` | `1` | 首个 turn 开始前的进程故障自动重试次数 |
package/dist/server.js CHANGED
@@ -9,8 +9,6 @@ import { z } from "zod";
9
9
 
10
10
  // src/contracts.ts
11
11
  import { statSync } from "fs";
12
- import { readFile } from "fs/promises";
13
- import { join } from "path";
14
12
  var PROTOCOL_INSTRUCTIONS = `## Protocol Instructions
15
13
 
16
14
  ### Execution Rules
@@ -42,14 +40,6 @@ End every task with exactly these sections:
42
40
 
43
41
  ### Rework
44
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.`;
45
- async function optionalAgentsInstructions(cwd) {
46
- try {
47
- return await readFile(join(cwd, "AGENTS.md"), "utf8");
48
- } catch (error) {
49
- if (error?.code === "ENOENT") return void 0;
50
- throw error;
51
- }
52
- }
53
43
  function renderContextFiles(contextFiles2) {
54
44
  if (!contextFiles2?.length) return [];
55
45
  return [
@@ -63,11 +53,9 @@ function renderContextFiles(contextFiles2) {
63
53
  ""
64
54
  ];
65
55
  }
66
- async function renderGoalContract(c, cwd = process.cwd()) {
67
- const agents = await optionalAgentsInstructions(cwd);
56
+ function renderGoalContract(c) {
68
57
  const lines = [
69
58
  PROTOCOL_INSTRUCTIONS,
70
- ...agents ? ["", "## Project Instructions", "", agents.trim()] : [],
71
59
  "",
72
60
  "## Goal Contract",
73
61
  "",
@@ -102,10 +90,13 @@ function renderDeltaContract(d) {
102
90
  ];
103
91
  return lines.join("\n");
104
92
  }
105
- function renderObjective(c, maxLen) {
93
+ function renderObjectiveDetails(c, maxLen) {
106
94
  const conditions = c.success_conditions.map((x) => `(${x})`).join(" ");
107
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.`;
108
- return text.length <= maxLen ? text : text.slice(0, maxLen - 1) + "\u2026";
96
+ return {
97
+ objective: text.length <= maxLen ? text : text.slice(0, maxLen - 1) + "\u2026",
98
+ objectiveChars: text.length
99
+ };
109
100
  }
110
101
  var HANDOFF_SECTIONS = [
111
102
  "Changed Files",
@@ -231,7 +222,7 @@ var CodexAppServer = class {
231
222
  }
232
223
  async initialize() {
233
224
  const result = await this.request("initialize", {
234
- clientInfo: { name: "claude2codex", title: "Claude\u2194Codex Contract Bridge", version: "0.3.0" },
225
+ clientInfo: { name: "claude2codex", title: "Claude\u2194Codex Contract Bridge", version: "0.3.1" },
235
226
  capabilities: { experimentalApi: true }
236
227
  });
237
228
  this.notify("initialized", {});
@@ -258,7 +249,7 @@ function writeLog(level, event, fields = {}) {
258
249
  // src/store.ts
259
250
  import { accessSync, constants, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "fs";
260
251
  import { homedir } from "os";
261
- import { join as join2 } from "path";
252
+ import { join } from "path";
262
253
  var MAX_JOBS = 50;
263
254
  var warned = false;
264
255
  function warnOnce(error) {
@@ -269,7 +260,7 @@ function warnOnce(error) {
269
260
  });
270
261
  }
271
262
  function defaultStateDir() {
272
- return process.env.C2C_STATE_DIR ?? join2(homedir(), ".claude2codex", "jobs");
263
+ return process.env.C2C_STATE_DIR ?? join(homedir(), ".claude2codex", "jobs");
273
264
  }
274
265
  function createJobStore(dir = defaultStateDir()) {
275
266
  const records = /* @__PURE__ */ new Map();
@@ -281,7 +272,7 @@ function createJobStore(dir = defaultStateDir()) {
281
272
  for (const name of readdirSync(dir)) {
282
273
  if (!name.endsWith(".json")) continue;
283
274
  try {
284
- const job = JSON.parse(readFileSync(join2(dir, name), "utf8"));
275
+ const job = JSON.parse(readFileSync(join(dir, name), "utf8"));
285
276
  if (!job?.id || !job?.startedAt || !Array.isArray(job.transcript)) continue;
286
277
  if (job.state === "blocked" || job.state === "queued" || job.state === "starting" || job.state === "running") {
287
278
  job.state = "error";
@@ -317,8 +308,8 @@ function createJobStore(dir = defaultStateDir()) {
317
308
  };
318
309
  }
319
310
  function atomicWrite(dir, job) {
320
- const target = join2(dir, `${job.id}.json`);
321
- const temp = join2(dir, `.${job.id}.${process.pid}.${Date.now()}.tmp`);
311
+ const target = join(dir, `${job.id}.json`);
312
+ const temp = join(dir, `.${job.id}.${process.pid}.${Date.now()}.tmp`);
322
313
  writeFileSync(temp, JSON.stringify(job), { encoding: "utf8", mode: 384 });
323
314
  renameSync(temp, target);
324
315
  }
@@ -329,7 +320,7 @@ function prune(dir, records) {
329
320
  for (const job of entries.slice(MAX_JOBS)) {
330
321
  records.delete(job.id);
331
322
  try {
332
- unlinkSync(join2(dir, `${job.id}.json`));
323
+ unlinkSync(join(dir, `${job.id}.json`));
333
324
  } catch (error) {
334
325
  if (error?.code !== "ENOENT") throw error;
335
326
  }
@@ -852,6 +843,7 @@ function config(cwd) {
852
843
  };
853
844
  }
854
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.`;
855
847
  function jobSummary(job, transcriptTail = 15) {
856
848
  return {
857
849
  job_id: job.id,
@@ -879,7 +871,7 @@ function textResult(obj, isError = false) {
879
871
  isError
880
872
  };
881
873
  }
882
- var server = new McpServer({ name: "c2c-codex", version: "0.3.0" });
874
+ var server = new McpServer({ name: "c2c-codex", version: "0.3.1" });
883
875
  var reasoningEffort = z.enum(["low", "medium", "high", "xhigh", "max", "ultra"]);
884
876
  var contextFiles = z.array(z.object({
885
877
  path: z.string().min(1),
@@ -946,9 +938,9 @@ server.registerTool(
946
938
  title: "Delegate a Goal Contract to Codex",
947
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.",
948
940
  inputSchema: {
949
- goal: z.string().min(1).describe("What the code must do when this task is done."),
941
+ goal: z.string().min(1).describe(`What the code must do when this task is done. ${objectiveBudgetDescription}`),
950
942
  constraints: z.array(z.string()).describe("Technical boundaries: files to touch, patterns to follow, 'Do not modify' lists."),
951
- 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."),
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}`),
952
944
  token_budget: z.number().int().positive().optional().describe("Optional token budget for the Codex goal loop (thread/goal/set tokenBudget)."),
953
945
  cwd: z.string().optional().describe("Working directory for Codex. Defaults to the project directory."),
954
946
  context_files: contextFiles.optional().describe("Files or directories Codex should read before implementing."),
@@ -966,13 +958,19 @@ server.registerTool(
966
958
  success_conditions: input.success_conditions,
967
959
  context_files: resolvedContext.files
968
960
  };
969
- const prompt = await renderGoalContract(contract, jobConfig.cwd);
961
+ const prompt = renderGoalContract(contract);
970
962
  const dependencyError = dependencyValidationError(input.depends_on);
971
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
+ }
972
970
  const job = startJob({
973
971
  kind: "implement",
974
972
  prompt,
975
- objective: renderObjective(contract, OBJECTIVE_MAX),
973
+ objective: renderedObjective.objective,
976
974
  tokenBudget: input.token_budget,
977
975
  contextFiles: resolvedContext.files?.map((file) => file.path),
978
976
  dependsOn: input.depends_on,
@@ -993,10 +991,10 @@ server.registerTool(
993
991
  title: "Estimate a Goal Contract's token cost",
994
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.",
995
993
  inputSchema: {
996
- goal: z.string().min(1).describe("What the code must do when this task is done."),
994
+ goal: z.string().min(1).describe(`What the code must do when this task is done. ${objectiveBudgetDescription}`),
997
995
  constraints: z.array(z.string()).describe("Technical boundaries: files to touch, patterns to follow, 'Do not modify' lists."),
998
- 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."),
999
- cwd: z.string().optional().describe("Working directory used to render the prompt. Defaults to the project directory."),
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."),
1000
998
  context_files: contextFiles.optional().describe("Files or directories included in the rendered prompt.")
1001
999
  },
1002
1000
  annotations: { readOnlyHint: true }
@@ -1005,16 +1003,21 @@ server.registerTool(
1005
1003
  const jobConfig = config(input.cwd);
1006
1004
  const resolvedContext = await resolveContextFiles(input.context_files, jobConfig.cwd);
1007
1005
  if (resolvedContext.missing.length) return missingContextFiles(resolvedContext.missing);
1008
- const prompt = await renderGoalContract({
1006
+ const contract = {
1009
1007
  goal: input.goal,
1010
1008
  constraints: input.constraints,
1011
1009
  success_conditions: input.success_conditions,
1012
1010
  context_files: resolvedContext.files
1013
- }, jobConfig.cwd);
1011
+ };
1012
+ const prompt = renderGoalContract(contract);
1013
+ const renderedObjective = renderObjectiveDetails(contract, OBJECTIVE_MAX);
1014
1014
  const history = implementHistory();
1015
1015
  return textResult({
1016
1016
  prompt_chars: prompt.length,
1017
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,
1018
1021
  history,
1019
1022
  estimated_total_tokens: history?.median_total_tokens ?? null,
1020
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."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude2codex",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "MCP server bridging Claude (architect/reviewer) and Codex (implementer) via Goal/Delta Contracts and Codex's thread-goal loop.",
6
6
  "bin": {
@@ -30,7 +30,8 @@
30
30
  "start": "bun src/server.ts",
31
31
  "build": "tsup",
32
32
  "test": "bun test",
33
- "prepack": "cp ../README.md ../README.zh-CN.md ../PATTERNS.md ../PATTERNS.zh-CN.md ."
33
+ "prepack": "cp ../README.md ../README.zh-CN.md ../PATTERNS.md ../PATTERNS.zh-CN.md .",
34
+ "postpack": "rm -f README.md README.zh-CN.md PATTERNS.md PATTERNS.zh-CN.md"
34
35
  },
35
36
  "dependencies": {
36
37
  "@modelcontextprotocol/sdk": "^1.29.0",