@lyupro/codex-bridge 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/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.3.1] - 2026-08-07
8
+
9
+ ### Added
10
+
11
+ - The CLI answers to `codexb` as well as `codex-bridge`, and README now documents the global install that puts both names on `PATH`. Every spelling that can reach deletion comes from one frozen list, and the prune guard builds its matcher from that list rather than repeating the names — two independent lists drift silently, which is exactly how the installer and test hook lists had already drifted. The full name stays primary: the docs, checklists and three agent prompts are written with it.
12
+ - Every run carries the rules it is judged by. A seeded `conventions.md` beside `run-config.json` and an optional `.codex-conventions.md` in the repository being worked on are pasted into `task.md` verbatim, under one `## Conventions` section. Either file missing is not an error, and a blank one produces no section at all — rules handed over as an empty heading read as rules ignored. Until now project rules reached a run only when the orchestrator remembered to write "read CLAUDE.md" into the task, and six acceptance passes in a row caught the same class of violation: the rule existed, applied, and had not travelled. Pasting rather than referencing is the point — "read file X" is hope, and a run is free not to.
13
+ - `doctor` opens with the path of the package copy answering, marked `clone` or `installed package`, and reports whether the host conventions file was found, warning when it exists but is empty. A global install puts a second copy of the package beside any clone, and `update` copies host files from whichever copy launched it — so two diagnoses of the same host were previously indistinguishable.
14
+
15
+ ### Fixed
16
+
17
+ - `codex-bridge projects` printed the `last run` column on two clocks. Folder names are written in local time and `meta.finished_at` is an ISO stamp in UTC, and each reached the column through its own branch, so a run whose folder says `144243` was listed as `12:46` — the operator's own zone offset, indistinguishable from a different run. Which run is newest was never affected, only how it printed.
18
+
7
19
  ## [0.3.0] - 2026-08-07
8
20
 
9
21
  ### Added
package/README.md CHANGED
@@ -7,7 +7,10 @@ subscription instead of the Claude one.
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- npx @lyupro/codex-bridge install # into ~/.claude
10
+ npx @lyupro/codex-bridge install # one-off, into ~/.claude
11
+ npm i -g @lyupro/codex-bridge # install both command names globally
12
+ codex-bridge install # full name, from any directory
13
+ codexb install # short name, same command
11
14
  node bin/codex-bridge.mjs install # from a clone, same thing
12
15
  node bin/codex-bridge.mjs install --scope project # into <repo>/.claude
13
16
  node bin/codex-bridge.mjs update # bring an installation up to the current version
@@ -20,6 +23,10 @@ node bin/codex-bridge.mjs prune <project> # reclaim disk space, transport only
20
23
  node bin/codex-bridge.mjs sweep [<project>] # close running records whose runner is gone
21
24
  ```
22
25
 
26
+ When a global install and a clone coexist, there are two copies of the package. `codex-bridge update`
27
+ and `codexb update` copy host files from whichever copy launched the command, so `codexb update`
28
+ from `PATH` can silently revert a host that `npm run dev:install` from the clone just updated.
29
+
23
30
  `install` copies the package into the host's `agents/codex/`, its slash commands into
24
31
  `commands/codex/`, and registers its four hooks in `settings.json` — **merged**, so hooks that
25
32
  are already there survive, and the file is backed up before every write. Run it twice and the
@@ -27,6 +27,7 @@ Usage:
27
27
  codex-bridge sweep [<project>]
28
28
  codex-bridge read <run>
29
29
  codex-bridge stop <run>
30
+ codexb <same command forms as codex-bridge>
30
31
  codex-bridge --help
31
32
  codex-bridge --version
32
33
 
package/cli/doctor.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  /** Diagnoses a Claude Code host without modifying it. */
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
4
5
  import { spawnSync } from 'node:child_process';
5
6
  import {
6
7
  fileFingerprint,
@@ -47,6 +48,34 @@ function retentionCheck(host) {
47
48
  }
48
49
  }
49
50
 
51
+ async function conventionsCheck(host) {
52
+ const file = path.join(host.agentsDir, 'conventions.md');
53
+ let content;
54
+ try {
55
+ content = await fs.readFile(file, 'utf8');
56
+ } catch (err) {
57
+ if (err.code === 'ENOENT') return check('conventions', 'ok', `${file} (not found; optional)`);
58
+ return check('conventions', 'fail', `cannot read ${file}: ${err.message}`);
59
+ }
60
+ return content.trim()
61
+ ? check('conventions', 'ok', `${file} (found)`)
62
+ : check('conventions', 'warn', `${file} (found but empty)`);
63
+ }
64
+
65
+ /**
66
+ * Which copy of the package is answering. Plan_19 gives the CLI a second name, and a global install
67
+ * puts a second copy of the package on the machine beside any clone. `update` copies host files
68
+ * from whichever copy launched it, so `codexb update` from PATH silently reverts a host that
69
+ * `npm run dev:install` from the clone had just refreshed — and every line below this one describes
70
+ * the host as seen by THIS copy. An operator comparing two diagnoses has no other way to tell them
71
+ * apart.
72
+ */
73
+ function sourceCheck() {
74
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
75
+ const installed = root.split(/[\\/]/).includes('node_modules');
76
+ return check('source', 'ok', `${root} (${installed ? 'installed package' : 'clone'})`);
77
+ }
78
+
50
79
  /**
51
80
  * The runner asks git for the repository root before it picks a runs folder, so doctor has to
52
81
  * ask the same question: run from `src/runner`, a plain cwd would name the folder `runner` and
@@ -134,7 +163,7 @@ async function rulesCheck(host, record) {
134
163
  }
135
164
 
136
165
  export async function diagnose({ host, codexProbe = probeCodex, currentPackage } = {}) {
137
- const checks = [];
166
+ const checks = [sourceCheck()];
138
167
  const hostExists = await exists(host.root);
139
168
  checks.push(check('host', hostExists ? 'ok' : 'warn', `${host.root} (${host.scope}, ${hostExists ? 'exists' : 'absent'})`));
140
169
 
@@ -173,6 +202,8 @@ export async function diagnose({ host, codexProbe = probeCodex, currentPackage }
173
202
  checks.push(...await hookChecks(host, record));
174
203
  const retention = retentionCheck(host);
175
204
  checks.push(retention);
205
+ const conventions = await conventionsCheck(host);
206
+ checks.push(conventions);
176
207
 
177
208
  const codex = codexProbe();
178
209
  checks.push(check('codex', codex.available ? 'ok' : 'warn', codex.value || 'available'));
@@ -184,7 +215,7 @@ export async function diagnose({ host, codexProbe = probeCodex, currentPackage }
184
215
 
185
216
  return {
186
217
  exitCode: !record || recordBroken || missingFiles.length || rules.status === 'fail'
187
- || retention.status === 'fail' || projectRuns.status === 'fail' ? 1 : 0,
218
+ || retention.status === 'fail' || conventions.status === 'fail' || projectRuns.status === 'fail' ? 1 : 0,
188
219
  checks,
189
220
  record,
190
221
  missingFiles,
@@ -194,6 +225,8 @@ export async function diagnose({ host, codexProbe = probeCodex, currentPackage }
194
225
  export function renderDoctor(result) {
195
226
  return result.checks.map(({ key, status, value }) => {
196
227
  const rendered = `[${status}] ${key}: ${value}`;
197
- return key === 'retention' && status === 'warn' ? `${WARNING}${rendered}${RESET}` : rendered;
228
+ return ['retention', 'conventions'].includes(key) && status === 'warn'
229
+ ? `${WARNING}${rendered}${RESET}`
230
+ : rendered;
198
231
  }).join('\n');
199
232
  }
package/cli/manifest.mjs CHANGED
@@ -22,10 +22,11 @@ const RULES_SOURCE = `src/rules/${RULES_NAME}`;
22
22
  /**
23
23
  * Files the operator owns once they exist. Seeded on first install so the defaults are
24
24
  * visible and editable, then never written, never compared, never removed: run-config.json
25
- * is where a host states which model each mode runs on, and an installer that overwrites it
26
- * would erase that on the next update — the same way overwriting a .env would.
25
+ * is where a host states which model each mode runs on, conventions.md is the rule set it hands
26
+ * every run, and an installer that overwrites either would erase host decisions on the next
27
+ * update — the same way overwriting a .env would.
27
28
  */
28
- export const SEEDED_SOURCES = Object.freeze(['src/run-config.json']);
29
+ export const SEEDED_SOURCES = Object.freeze(['src/run-config.json', 'src/conventions.md']);
29
30
  const seededTargets = (host) =>
30
31
  new Set(SEEDED_SOURCES.map((source) => posix(
31
32
  path.relative(host.root, path.join(host.agentsDir, path.relative('src', source))),
package/cli/projects.mjs CHANGED
@@ -15,13 +15,26 @@ function formatSize(bytes) {
15
15
  return `${bytes} B`;
16
16
  }
17
17
 
18
- /** Both timestamp shapes in the store: an ISO stamp from artifacts, a folder name otherwise. */
18
+ const pad = (value) => String(value).padStart(2, '0');
19
+
20
+ /**
21
+ * Both timestamp shapes in the store, printed on one clock. A run folder is named in local time
22
+ * (`2026-08-07_144243`); `meta.finished_at` is an ISO stamp in UTC. Reading the ISO one with a
23
+ * regular expression printed its UTC hours in the same column as local ones, and the 2026-08-07
24
+ * live check (Plan_17-1 §1) saw the run whose folder says `144243` listed as `12:46` — the
25
+ * operator's own zone offset, looking exactly like a different run. Slicing a timestamp is cheaper
26
+ * than parsing it only until the value carries a zone. The folder name stays text because it is
27
+ * already local; parsing it would invent a zone it never had.
28
+ */
19
29
  function formatStamp(value) {
20
30
  if (typeof value !== 'string' || !value.trim()) return null;
21
- const iso = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/.exec(value);
22
- if (iso) return `${iso[1]} ${iso[2]}`;
23
31
  const folder = /^(\d{4}-\d{2}-\d{2})_(\d{2})(\d{2})/.exec(value);
24
- return folder ? `${folder[1]} ${folder[2]}:${folder[3]}` : value;
32
+ if (folder) return `${folder[1]} ${folder[2]}:${folder[3]}`;
33
+ const moment = Date.parse(value);
34
+ if (Number.isNaN(moment)) return value;
35
+ const at = new Date(moment);
36
+ return `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}`
37
+ + ` ${pad(at.getHours())}:${pad(at.getMinutes())}`;
25
38
  }
26
39
 
27
40
  const PROJECT_COLUMNS = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyupro/codex-bridge",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Delegate implementation, reconnaissance and code review from Claude Code to the Codex CLI: dispatcher agents, a runner that survives a dropped call, and a verdict that checks the report against the worktree.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -23,7 +23,8 @@
23
23
  "homepage": "https://github.com/lyupro/codex-bridge#readme",
24
24
  "type": "module",
25
25
  "bin": {
26
- "codex-bridge": "./bin/codex-bridge.mjs"
26
+ "codex-bridge": "./bin/codex-bridge.mjs",
27
+ "codexb": "./bin/codex-bridge.mjs"
27
28
  },
28
29
  "engines": {
29
30
  "node": ">=24.0.0"
@@ -0,0 +1,6 @@
1
+ /** Names this CLI answers to. */
2
+
3
+ // Plan_17 §5 found installer and test hook lists drifting; keep executable spellings here so a
4
+ // new alias cannot silently bypass the prune guard. Under src/ because the installed guard reads
5
+ // it too, and cli/ is not copied into the host.
6
+ export const CLI_NAMES = Object.freeze(['codex-bridge', 'codexb']);
@@ -0,0 +1,44 @@
1
+ # Delegated-run conventions
2
+
3
+ These are the host conventions for every Codex run. Each block records a real acceptance
4
+ violation caught in this repository and turns it into a guard for the next run.
5
+
6
+ ## Task boundaries - Plan_16 acceptance incident
7
+
8
+ - Do not refactor code the task did not ask to change.
9
+ - Do not delete code on your own initiative.
10
+ - Do not change behavior branches whose behavior the task does not mention, even inside an
11
+ allowed file.
12
+
13
+ The 2026-08-06 Plan_16 acceptance finding applied a new reason order to a transport branch that
14
+ already named its reason. Preserve existing branches unless the task explicitly changes them.
15
+
16
+ ## Contracts without silent defaults - Plan_16 reasonFrom incident
17
+
18
+ - Require arguments that the contract needs instead of supplying a default.
19
+ - Fail loud when a required value is absent; do not silently fall back.
20
+ - Keep volatile identifiers such as model IDs and API versions in configuration, not runtime code.
21
+
22
+ The 2026-08-06 `reasonFrom(runDir, eventData = {})` finding replaced a required contract with a
23
+ silent default. Missing inputs must remain visible at the boundary that can explain them.
24
+
25
+ ## Structure and size - 2026-08-06 projects reload incident
26
+
27
+ - Keep every source file at or below 400 lines.
28
+ - Give each module one responsibility.
29
+ - Do not create `utils.mjs` dumping grounds.
30
+ - Never split coherent pieces solely to reach the line limit.
31
+
32
+ The 2026-08-06 `projects` reload finding exposed pseudo-duplicate exports and just-in-case code.
33
+ Keep related code together, but remove a real responsibility or create a focused module when a
34
+ boundary is needed.
35
+
36
+ ## Tests and comments - 2026-08-05 step 2b incident
37
+
38
+ - Add a guard test for every invariant accepted by the task.
39
+ - Splitting a file must delete no test case.
40
+ - Comments explain WHY and name the incident or decision that makes the rule necessary.
41
+
42
+ The 2026-08-05 step 2b acceptance found the abandoned-run check below the commit check. Test the
43
+ ordering invariant itself, and leave the incident in the comment so a later cleanup does not
44
+ silently undo it.
@@ -11,6 +11,7 @@
11
11
  * Anything unrecognised passes. A guard that misreads its input must not break unrelated host work.
12
12
  */
13
13
  import fs from 'node:fs';
14
+ import { CLI_NAMES } from '../cli-names.mjs';
14
15
  import { SHELL_TOOLS } from '../hook-definitions.mjs';
15
16
 
16
17
  const SHELL_TOOL_NAMES = new Set(SHELL_TOOLS);
@@ -18,10 +19,17 @@ const pass = () => process.exit(0);
18
19
 
19
20
  // The binary, then the verb, with any path in front of it: an agent reaches the CLI as
20
21
  // `codex-bridge`, as `node bin/codex-bridge.mjs`, or through an absolute path, and a guard that
21
- // only knew the bare name would be bypassed by the spelling everyone uses inside a clone. Matching
22
- // a lone `prune` is not an option either — `git prune` and `npm prune` are not this package's
23
- // business, and a guard that breaks unrelated work is a guard that gets uninstalled.
24
- const PRUNE_CALL = /(^|[\s;&|(`])(\S*[\\/])?codex-bridge(\.mjs)?\s+prune(\s|$)/;
22
+ // only knew the bare name would be bypassed by the spelling everyone uses inside a clone. Plan_19
23
+ // adds a second executable spelling, so build this matcher from the shared list instead of
24
+ // repeating names here. Matching a lone `prune` is not an option either — `git prune` and
25
+ // `npm prune` are not this package's business, and a guard that breaks unrelated work is a guard
26
+ // that gets uninstalled.
27
+ const escapeRegex = (name) => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
28
+ const CLI_NAME_PATTERN = CLI_NAMES.map(escapeRegex).join('|');
29
+ const PRUNE_CALL = new RegExp(
30
+ '(^|[\\s;&|(`])(\\S*[\\\\/])?(?:' + CLI_NAME_PATTERN
31
+ + ')(\\.mjs)?\\s+prune(\\s|$)',
32
+ );
25
33
 
26
34
  let input;
27
35
  try {
@@ -0,0 +1,39 @@
1
+ /** Renders the host and repository conventions that a run must carry in its task artifact. */
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { CONFIG_PATH } from '../run-config.mjs';
5
+
6
+ const HOST_FILE = path.join(path.dirname(CONFIG_PATH), 'conventions.md');
7
+ const REPOSITORY_FILE = '.codex-conventions.md';
8
+
9
+ function readOptional(file) {
10
+ try {
11
+ return fs.readFileSync(file, 'utf8');
12
+ } catch (err) {
13
+ if (err.code === 'ENOENT') return null;
14
+ throw err;
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Reads both convention layers at the point of use. Plan_13 showed that a reference to a required
20
+ * file is not enforcement: the task must contain the bytes the run was given.
21
+ *
22
+ * A layer that is present but blank is dropped rather than rendered as an empty heading, for the
23
+ * same reason doctor warns about it — an empty rules file looks like a working mechanism and is
24
+ * not one, and in the artifact it would look like rules that were given and ignored.
25
+ *
26
+ * Duplication is impossible by construction and deliberately not guarded against: `sections`
27
+ * starts empty on every launch and task.md is written into a folder that is never reused, so
28
+ * there is nothing to append to. A guard reading the operator's task text for this heading would
29
+ * only find the word in a task that talks ABOUT conventions, and silently drop the section from
30
+ * the one run that most obviously needed it.
31
+ */
32
+ export function renderConventions(repoRoot, hostFile = HOST_FILE) {
33
+ const layers = [];
34
+ const host = readOptional(hostFile);
35
+ if (host?.trim()) layers.push(`### Host conventions\n\n${host}`);
36
+ const repository = readOptional(path.join(repoRoot, REPOSITORY_FILE));
37
+ if (repository?.trim()) layers.push(`### Repository conventions\n\n${repository}`);
38
+ return layers.length ? `## Conventions\n\n${layers.join('\n\n')}` : '';
39
+ }
@@ -37,6 +37,7 @@ import { codexArgs, requireCodex, runMode, unsafeForCmd } from './codex-cmd.mjs'
37
37
  import { runsRoot } from './runs-root.mjs';
38
38
  import { resolveProjectRunsDir } from './project-dir.mjs';
39
39
  import { cleanupRetention } from '../retention.mjs';
40
+ import { renderConventions } from './conventions.mjs';
40
41
 
41
42
  /**
42
43
  * The worker is this same program re-invoked as `--worker <runDir>`, so the path spawned
@@ -279,6 +280,8 @@ export async function launcher() {
279
280
  ].join('\n'),
280
281
  );
281
282
  }
283
+ const conventions = renderConventions(repoRoot);
284
+ if (conventions) sections.push(conventions);
282
285
  sections.push(`## Instructions for Codex\n\n${INSTRUCTIONS[opts.agent](opts, scope, questions)}`);
283
286
  fs.writeFileSync(path.join(runDir, 'task.md'), `${sections.join('\n\n')}\n`);
284
287
  fs.writeFileSync(