@lyupro/codex-bridge 0.5.1 → 0.5.2

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.
Files changed (83) hide show
  1. package/CHANGELOG.md +85 -1
  2. package/README.md +19 -2
  3. package/bin/codex-bridge.mjs +4 -0
  4. package/cli/doctor.mjs +118 -19
  5. package/cli/frontmatter.mjs +137 -0
  6. package/cli/hook.mjs +2 -2
  7. package/cli/install-record.mjs +2 -2
  8. package/cli/install.mjs +14 -2
  9. package/cli/manifest.mjs +23 -22
  10. package/cli/permissions.mjs +2 -2
  11. package/cli/projects.mjs +3 -3
  12. package/cli/prune-plan.mjs +2 -2
  13. package/cli/prune.mjs +1 -1
  14. package/cli/read.mjs +2 -2
  15. package/cli/rules-owners.mjs +2 -2
  16. package/cli/run-lookup.mjs +3 -3
  17. package/cli/runs-inventory.mjs +3 -3
  18. package/cli/settings-merge.mjs +1 -1
  19. package/cli/stop.mjs +5 -5
  20. package/cli/unlock.mjs +7 -7
  21. package/package.json +1 -1
  22. package/scripts/check-file-size.mjs +1 -1
  23. package/src/agents/codex-build.md +265 -231
  24. package/src/agents/codex-review.md +219 -184
  25. package/src/agents/codex-scout.md +232 -200
  26. package/src/{hooks → home/hooks}/guard-tries.mjs +1 -1
  27. package/src/{hooks → home/hooks}/live-runs.mjs +43 -3
  28. package/src/home/hooks/order-gate.mjs +135 -0
  29. package/src/{hooks → home/hooks}/prune-guard.mjs +2 -2
  30. package/src/{hooks → home/hooks}/reply-guard.mjs +117 -97
  31. package/src/home/hooks/reply-parser.mjs +27 -0
  32. package/src/home/hooks/reply-verdicts.mjs +174 -0
  33. package/src/{hooks → home/hooks}/stop-guard.mjs +3 -3
  34. package/src/{hooks → home/hooks}/worktree-lock.mjs +3 -3
  35. package/src/home/lib/dispatcher-order.mjs +36 -0
  36. package/src/{hook-definitions.mjs → home/lib/hook-definitions.mjs} +1 -1
  37. package/src/home/lib/host-refusal.mjs +20 -0
  38. package/src/{json-file.mjs → home/lib/json-file.mjs} +4 -4
  39. package/src/{meta → home/lib/meta}/paths.mjs +20 -1
  40. package/src/{meta → home/lib/meta}/reply.mjs +1 -1
  41. package/src/{meta → home/lib/meta}/verdict.mjs +1 -1
  42. package/src/{no-self-execution.mjs → home/lib/no-self-execution.mjs} +7 -2
  43. package/src/{required-inputs.mjs → home/lib/required-inputs.mjs} +56 -5
  44. package/src/{retention.mjs → home/lib/retention.mjs} +1 -1
  45. package/src/{run-codex.mjs → home/lib/run-codex.mjs} +39 -11
  46. package/src/{run-config.mjs → home/lib/run-config.mjs} +2 -2
  47. package/src/{runner → home/lib/runner}/args.mjs +86 -10
  48. package/src/{runner → home/lib/runner}/attach.mjs +58 -9
  49. package/src/{runner → home/lib/runner}/launcher.mjs +33 -21
  50. package/src/home/lib/runner/order-owner.mjs +28 -0
  51. package/src/{runner → home/lib/runner}/run-env.mjs +1 -1
  52. package/src/home/lib/runner/task-file.mjs +92 -0
  53. package/src/home/lib/runner/task-input.mjs +26 -0
  54. package/src/{runner → home/lib/runner}/worker.mjs +1 -1
  55. package/src/home/lib/shell-unsafe.mjs +26 -0
  56. package/src/{write-meta.mjs → home/lib/write-meta.mjs} +12 -1
  57. package/src/hooks/order-gate.mjs +0 -73
  58. /package/src/{run-config.json → home/config.json} +0 -0
  59. /package/src/{conventions.md → home/conventions.md} +0 -0
  60. /package/src/{cli-names.mjs → home/lib/cli-names.mjs} +0 -0
  61. /package/src/{heartbeat.mjs → home/lib/heartbeat.mjs} +0 -0
  62. /package/src/{meta → home/lib/meta}/chain.mjs +0 -0
  63. /package/src/{meta → home/lib/meta}/deadline.mjs +0 -0
  64. /package/src/{meta → home/lib/meta}/environment.mjs +0 -0
  65. /package/src/{meta → home/lib/meta}/events.mjs +0 -0
  66. /package/src/{meta → home/lib/meta}/outcome.mjs +0 -0
  67. /package/src/{meta → home/lib/meta}/pre-start.mjs +0 -0
  68. /package/src/{meta → home/lib/meta}/reason.mjs +0 -0
  69. /package/src/{meta → home/lib/meta}/run-state.mjs +0 -0
  70. /package/src/{meta → home/lib/meta}/startup.mjs +0 -0
  71. /package/src/{meta → home/lib/meta}/transport.mjs +0 -0
  72. /package/src/{process-identity.mjs → home/lib/process-identity.mjs} +0 -0
  73. /package/src/{runner → home/lib/runner}/codex-cmd.mjs +0 -0
  74. /package/src/{runner → home/lib/runner}/continuation.mjs +0 -0
  75. /package/src/{runner → home/lib/runner}/conventions.mjs +0 -0
  76. /package/src/{runner → home/lib/runner}/git-state.mjs +0 -0
  77. /package/src/{runner → home/lib/runner}/project-dir.mjs +0 -0
  78. /package/src/{runner → home/lib/runner}/prompts.mjs +0 -0
  79. /package/src/{runner → home/lib/runner}/run-context.mjs +0 -0
  80. /package/src/{runner → home/lib/runner}/runs-root.mjs +0 -0
  81. /package/src/{runner → home/lib/runner}/schemas.mjs +0 -0
  82. /package/src/{runner → home/lib/runner}/scope-check.mjs +0 -0
  83. /package/src/{stop-contract.mjs → home/lib/stop-contract.mjs} +0 -0
package/cli/manifest.mjs CHANGED
@@ -3,14 +3,15 @@ import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { createHash } from 'node:crypto';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { HOOK_DEFINITIONS } from '../src/hook-definitions.mjs';
6
+ import { HOOK_DEFINITIONS } from '../src/home/lib/hook-definitions.mjs';
7
7
  import {
8
8
  renderRequiredInputSummary,
9
9
  renderRequiredInputs,
10
- } from '../src/required-inputs.mjs';
11
- import { renderNoSelfExecution } from '../src/no-self-execution.mjs';
12
- import { renderStopSummary } from '../src/stop-contract.mjs';
13
- import { readJsonFile } from '../src/json-file.mjs';
10
+ } from '../src/home/lib/required-inputs.mjs';
11
+ import { renderNoSelfExecution } from '../src/home/lib/no-self-execution.mjs';
12
+ import { renderStopSummary } from '../src/home/lib/stop-contract.mjs';
13
+ import { readJsonFile } from '../src/home/lib/json-file.mjs';
14
+ import { normalizeFrontmatter } from './frontmatter.mjs';
14
15
  import {
15
16
  INSTALL_RECORD_NAME,
16
17
  RULES_NAME,
@@ -49,18 +50,19 @@ const RULES_SOURCE = `src/rules/${RULES_NAME}`;
49
50
 
50
51
  /**
51
52
  * Files the operator owns once they exist. Seeded on first install so the defaults are
52
- * visible and editable, then never written, never compared, never removed: run-config.json
53
+ * visible and editable, then never written, never compared, never removed: config.json
53
54
  * is where a host states which model each mode runs on, conventions.md is the rule set it hands
54
55
  * every run, and an installer that overwrites either would erase host decisions on the next
55
56
  * update — the same way overwriting a .env would.
56
57
  */
57
- export const SEEDED_SOURCES = Object.freeze(['src/run-config.json', 'src/conventions.md']);
58
+ export const SEEDED_SOURCES = Object.freeze(['src/home/config.json', 'src/home/conventions.md']);
58
59
 
59
60
  export const INSTALL_TABLE = Object.freeze([
60
61
  { source: 'src/agents/*.md', root: 'claude', target: 'agentsDir', processing: 'placeholders' },
61
62
  { source: 'src/commands/*.md', root: 'claude', target: 'commandsDir', processing: 'placeholders' },
62
- { source: 'src/hooks/**', root: 'brand', target: 'brandHooksDir', processing: 'copy' },
63
- { source: 'src/**', root: 'brand', target: 'brandRunnerDir', processing: 'copy' },
63
+ // The source is the host image. Keeping one source root and one target root makes any future
64
+ // remapping visible as a test failure instead of another clone-only import success.
65
+ { source: 'src/home/**', root: 'brand', target: 'brandRoot', processing: 'copy' },
64
66
  // The runner reads its own version out of the package.json beside it, so meta.json can say
65
67
  // which code wrote a run (Plan_34). In the clone that file sits one level above src/; without
66
68
  // this line the installed layout has nothing one level above lib/, and every run on a real host
@@ -72,12 +74,13 @@ const posix = (value) => value.split(path.sep).join('/');
72
74
 
73
75
  export function replacePlaceholders(content, installationRoot) {
74
76
  const agentType = content.match(/^name:\s*([^\r\n]+)$/m)?.[1].trim();
75
- return content
77
+ const replaced = content
76
78
  .replaceAll('{{CODEX_BRIDGE_DIR}}', posix(path.resolve(installationRoot)))
77
79
  .replaceAll('{{CODEX_REQUIRED_INPUTS_SUMMARY}}', renderRequiredInputSummary(agentType))
78
80
  .replaceAll('{{CODEX_REQUIRED_INPUTS}}', renderRequiredInputs(agentType))
79
81
  .replaceAll('{{CODEX_NO_SELF_EXECUTION}}', renderNoSelfExecution())
80
82
  .replaceAll('{{CODEX_STOP_SUMMARY}}', renderStopSummary());
83
+ return normalizeFrontmatter(replaced);
81
84
  }
82
85
 
83
86
  export async function packageInfo(packageRoot = PACKAGE_ROOT) {
@@ -92,14 +95,13 @@ export function installedHookPath(host, definition) {
92
95
  /** Where each seeded file comes from and where it goes; contents are copied verbatim. */
93
96
  export function seedPlan(host, packageRoot = PACKAGE_ROOT) {
94
97
  return SEEDED_SOURCES.map((source) => {
95
- const target = source.endsWith('run-config.json')
96
- ? host.brandConfigPath
97
- : host.brandConventionsPath;
98
+ const relativeToRoot = posix(path.relative('src/home', source));
99
+ const target = path.join(host.brandRoot, relativeToRoot);
98
100
  return {
99
101
  source: path.join(packageRoot, source),
100
102
  target,
101
103
  root: 'brand',
102
- relativeToRoot: posix(path.relative(host.brandRoot, target)),
104
+ relativeToRoot,
103
105
  processing: 'copy',
104
106
  };
105
107
  });
@@ -115,13 +117,17 @@ export function rulesPlan(host, packageRoot = PACKAGE_ROOT) {
115
117
 
116
118
  export async function fileFingerprint(absolutePath) {
117
119
  try {
118
- return createHash('sha256').update(await fs.readFile(absolutePath)).digest('hex');
120
+ return contentFingerprint(await fs.readFile(absolutePath));
119
121
  } catch (err) {
120
122
  if (err.code === 'ENOENT') return null;
121
123
  throw err;
122
124
  }
123
125
  }
124
126
 
127
+ export function contentFingerprint(content) {
128
+ return createHash('sha256').update(content).digest('hex');
129
+ }
130
+
125
131
  function targetBase(host, mapping) {
126
132
  const base = host[mapping.target];
127
133
  if (!base) throw new Error(`host has no ${mapping.target} install target`);
@@ -136,17 +142,13 @@ function targetRelative(packageRoot, source, mapping) {
136
142
  if (mapping.processing === 'placeholders' || mapping.source === 'package.json') {
137
143
  return path.basename(source);
138
144
  }
139
- const sourceRoot = mapping.source === 'src/hooks/**'
140
- ? path.join(packageRoot, 'src', 'hooks')
141
- : path.join(packageRoot, 'src');
142
- return path.relative(sourceRoot, source);
145
+ return path.relative(path.join(packageRoot, 'src', 'home'), source);
143
146
  }
144
147
 
145
148
  export async function buildInstallPlan(host, packageRoot = PACKAGE_ROOT) {
146
149
  const files = [];
147
- const claimedSources = [];
148
150
  for (const mapping of INSTALL_TABLE) {
149
- for await (const relative of fs.glob(mapping.source, { cwd: packageRoot, exclude: claimedSources })) {
151
+ for await (const relative of fs.glob(mapping.source, { cwd: packageRoot })) {
150
152
  if (posix(relative) === RULES_SOURCE || SEEDED_SOURCES.includes(posix(relative))) continue;
151
153
  const source = path.join(packageRoot, relative);
152
154
  if (!(await fs.stat(source)).isFile()) continue;
@@ -165,7 +167,6 @@ export async function buildInstallPlan(host, packageRoot = PACKAGE_ROOT) {
165
167
  : rootFor(host, mapping),
166
168
  });
167
169
  }
168
- claimedSources.push(mapping.source);
169
170
  }
170
171
  return files.sort((a, b) => recordFileKey(a).localeCompare(recordFileKey(b)));
171
172
  }
@@ -1,8 +1,8 @@
1
1
  /** Manages the optional shell permission rules for every codex-bridge command spelling. */
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
- import { CLI_NAMES } from '../src/cli-names.mjs';
5
- import { SHELL_TOOLS } from '../src/hook-definitions.mjs';
4
+ import { CLI_NAMES } from '../src/home/lib/cli-names.mjs';
5
+ import { SHELL_TOOLS } from '../src/home/lib/hook-definitions.mjs';
6
6
  import { readSettings, updateSettings, withSettingsRun } from './settings-merge.mjs';
7
7
 
8
8
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
package/cli/projects.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  /** Implements the projects command. */
2
2
  import { listProjectRuns, listProjects } from './runs-inventory.mjs';
3
3
  import { renderTable } from './table.mjs';
4
- import { allLiveRuns } from '../src/hooks/live-runs.mjs';
5
- import { runsRoot } from '../src/runner/runs-root.mjs';
6
- import { STOP_COMMAND_TEMPLATE } from '../src/stop-contract.mjs';
4
+ import { allLiveRuns } from '../src/home/hooks/live-runs.mjs';
5
+ import { runsRoot } from '../src/home/lib/runner/runs-root.mjs';
6
+ import { STOP_COMMAND_TEMPLATE } from '../src/home/lib/stop-contract.mjs';
7
7
 
8
8
  // The table renders whatever it is given; how a byte count or a timestamp reads is this command's
9
9
  // business. Raw bytes and ISO milliseconds are technically exact and practically unreadable — an
@@ -3,8 +3,8 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { listProjectRuns, listProjects, recursiveSize } from './runs-inventory.mjs';
5
5
  import { parseOlderThan } from './prune-args.mjs';
6
- import { runsRoot } from '../src/runner/runs-root.mjs';
7
- import { runIsOlderThan, TRANSPORT_FILES } from '../src/retention.mjs';
6
+ import { runsRoot } from '../src/home/lib/runner/runs-root.mjs';
7
+ import { runIsOlderThan, TRANSPORT_FILES } from '../src/home/lib/retention.mjs';
8
8
 
9
9
  export { runIsOlderThan, TRANSPORT_FILES };
10
10
 
package/cli/prune.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import readline from 'node:readline/promises';
5
- import { normalizePath } from '../src/hooks/live-runs.mjs';
5
+ import { normalizePath } from '../src/home/hooks/live-runs.mjs';
6
6
  import { parsePruneArgs } from './prune-args.mjs';
7
7
  import { prunePlan } from './prune-plan.mjs';
8
8
  import { recursiveSize } from './runs-inventory.mjs';
package/cli/read.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  /** Renders a run's structured Codex transport as a readable event-by-event report. */
2
- import { readEvents } from '../src/meta/events.mjs';
3
- import { runsRoot } from '../src/runner/runs-root.mjs';
2
+ import { readEvents } from '../src/home/lib/meta/events.mjs';
3
+ import { runsRoot } from '../src/home/lib/runner/runs-root.mjs';
4
4
  import { resolveRunFolder } from './run-lookup.mjs';
5
5
 
6
6
  const result = (exitCode, output) => ({ exitCode, output });
@@ -2,8 +2,8 @@
2
2
  import fs from 'node:fs/promises';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import path from 'node:path';
5
- import { normalizeRepoPath } from '../src/runner/project-dir.mjs';
6
- import { readJsonFile } from '../src/json-file.mjs';
5
+ import { normalizeRepoPath } from '../src/home/lib/runner/project-dir.mjs';
6
+ import { readJsonFile } from '../src/home/lib/json-file.mjs';
7
7
 
8
8
  export const RULES_REGISTRY_NAME = '.codex-bridge-rules.json';
9
9
  export const RULES_REGISTRY_VERSION = 1;
@@ -1,9 +1,9 @@
1
1
  /** Resolves a run argument and confirms that it names an existing run directory. */
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { git } from '../src/runner/git-state.mjs';
5
- import { resolveProjectRunsDir } from '../src/runner/project-dir.mjs';
6
- import { runsRoot } from '../src/runner/runs-root.mjs';
4
+ import { git } from '../src/home/lib/runner/git-state.mjs';
5
+ import { resolveProjectRunsDir } from '../src/home/lib/runner/project-dir.mjs';
6
+ import { runsRoot } from '../src/home/lib/runner/runs-root.mjs';
7
7
 
8
8
  function projectRoot(cwd) {
9
9
  const result = git(cwd, ['rev-parse', '--show-toplevel']);
@@ -1,9 +1,9 @@
1
1
  /** Reads run-store directories into inventory data. */
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { liveRuns } from '../src/hooks/live-runs.mjs';
5
- import { readJsonFileSync } from '../src/json-file.mjs';
6
- import { runsRoot } from '../src/runner/runs-root.mjs';
4
+ import { liveRuns } from '../src/home/hooks/live-runs.mjs';
5
+ import { readJsonFileSync } from '../src/home/lib/json-file.mjs';
6
+ import { runsRoot } from '../src/home/lib/runner/runs-root.mjs';
7
7
 
8
8
  const record = (value) => value && typeof value === 'object' && !Array.isArray(value);
9
9
 
@@ -5,7 +5,7 @@ import { existsSync, statSync } from 'node:fs';
5
5
  import fs from 'node:fs/promises';
6
6
  import path from 'node:path';
7
7
  import { randomUUID } from 'node:crypto';
8
- import { readJsonFileWithRaw } from '../src/json-file.mjs';
8
+ import { readJsonFileWithRaw } from '../src/home/lib/json-file.mjs';
9
9
 
10
10
  const LEGACY_SPEC = { event: 'SubagentStop', matcher: '*' };
11
11
  const settingsRuns = new AsyncLocalStorage();
package/cli/stop.mjs CHANGED
@@ -1,16 +1,16 @@
1
1
  /** Stops a live run and records the same abandoned FAIL artifacts as the normal unlock. */
2
2
  import path from 'node:path';
3
- import { stopCodex } from '../src/runner/codex-cmd.mjs';
4
- import { git, worktreeSnapshot } from '../src/runner/git-state.mjs';
3
+ import { stopCodex } from '../src/home/lib/runner/codex-cmd.mjs';
4
+ import { git, worktreeSnapshot } from '../src/home/lib/runner/git-state.mjs';
5
5
  import {
6
6
  IDENTITY_ALIVE,
7
7
  IDENTITY_FOREIGN,
8
8
  IDENTITY_UNVERIFIED,
9
9
  processAlive,
10
10
  processIdentity,
11
- } from '../src/process-identity.mjs';
12
- import { markAbandoned, readJson } from '../src/write-meta.mjs';
13
- import { runsRoot } from '../src/runner/runs-root.mjs';
11
+ } from '../src/home/lib/process-identity.mjs';
12
+ import { markAbandoned, readJson } from '../src/home/lib/write-meta.mjs';
13
+ import { runsRoot } from '../src/home/lib/runner/runs-root.mjs';
14
14
  import { resolveRunFolder } from './run-lookup.mjs';
15
15
 
16
16
  const EXIT_POLL_MS = 25;
package/cli/unlock.mjs CHANGED
@@ -2,17 +2,17 @@
2
2
  /** Closes dead-pid running records without deleting any run artifacts. */
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
- import { formatSilence, heartbeatAge, HEARTBEAT_STALE_MS } from '../src/heartbeat.mjs';
6
- import { markAbandoned } from '../src/meta/run-state.mjs';
5
+ import { formatSilence, heartbeatAge, HEARTBEAT_STALE_MS } from '../src/home/lib/heartbeat.mjs';
6
+ import { markAbandoned } from '../src/home/lib/meta/run-state.mjs';
7
7
  import {
8
8
  IDENTITY_ALIVE,
9
9
  IDENTITY_UNVERIFIED,
10
10
  processIdentity,
11
- } from '../src/process-identity.mjs';
12
- import { git } from '../src/runner/git-state.mjs';
13
- import { resolveProjectRunsDir } from '../src/runner/project-dir.mjs';
14
- import { runsRoot } from '../src/runner/runs-root.mjs';
15
- import { readJson } from '../src/write-meta.mjs';
11
+ } from '../src/home/lib/process-identity.mjs';
12
+ import { git } from '../src/home/lib/runner/git-state.mjs';
13
+ import { resolveProjectRunsDir } from '../src/home/lib/runner/project-dir.mjs';
14
+ import { runsRoot } from '../src/home/lib/runner/runs-root.mjs';
15
+ import { readJson } from '../src/home/lib/write-meta.mjs';
16
16
  import { renderTable } from './table.mjs';
17
17
 
18
18
  const CLOSED_COLUMNS = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyupro/codex-bridge",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Claude Code-to-Codex delegation with 5 guards, 604 tests, zero runtime dependencies, and zero build steps.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -21,7 +21,7 @@
21
21
  import fs from 'node:fs/promises';
22
22
  import path from 'node:path';
23
23
  import { fileURLToPath } from 'node:url';
24
- import { readJsonFile } from '../src/json-file.mjs';
24
+ import { readJsonFile } from '../src/home/lib/json-file.mjs';
25
25
 
26
26
  const HERE = path.dirname(fileURLToPath(import.meta.url));
27
27
  export const REPO_ROOT = path.resolve(HERE, '..');