@lyupro/codex-bridge 0.3.0 → 0.4.0

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 (45) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +27 -7
  3. package/bin/codex-bridge.mjs +28 -4
  4. package/cli/doctor.mjs +53 -3
  5. package/cli/install.mjs +8 -2
  6. package/cli/manifest.mjs +24 -12
  7. package/cli/permissions.mjs +179 -0
  8. package/cli/projects.mjs +17 -4
  9. package/cli/prune-args.mjs +1 -1
  10. package/cli/rules-owners.mjs +6 -8
  11. package/cli/runs-inventory.mjs +2 -1
  12. package/cli/settings-merge.mjs +93 -45
  13. package/cli/stop.mjs +33 -20
  14. package/cli/uninstall.mjs +23 -10
  15. package/cli/unlock.mjs +239 -0
  16. package/package.json +3 -2
  17. package/scripts/check-file-size.mjs +8 -10
  18. package/src/agents/codex-build.md +2 -0
  19. package/src/agents/codex-review.md +2 -0
  20. package/src/agents/codex-scout.md +2 -0
  21. package/src/cli-names.mjs +6 -0
  22. package/src/conventions.md +44 -0
  23. package/src/heartbeat.mjs +1 -1
  24. package/src/hooks/live-runs.mjs +15 -14
  25. package/src/hooks/order-gate.mjs +7 -2
  26. package/src/hooks/prune-guard.mjs +12 -4
  27. package/src/hooks/reply-guard.mjs +4 -3
  28. package/src/json-file.mjs +30 -0
  29. package/src/meta/chain.mjs +8 -6
  30. package/src/meta/paths.mjs +5 -9
  31. package/src/meta/pre-start.mjs +30 -0
  32. package/src/meta/run-state.mjs +31 -16
  33. package/src/process-identity.mjs +140 -0
  34. package/src/required-inputs.mjs +42 -0
  35. package/src/run-config.mjs +4 -8
  36. package/src/runner/args.mjs +16 -2
  37. package/src/runner/attach.mjs +23 -17
  38. package/src/runner/codex-cmd.mjs +1 -1
  39. package/src/runner/conventions.mjs +39 -0
  40. package/src/runner/launcher.mjs +43 -16
  41. package/src/runner/project-dir.mjs +4 -3
  42. package/src/runner/scope-check.mjs +159 -0
  43. package/src/runner/worker.mjs +13 -5
  44. package/src/write-meta.mjs +8 -1
  45. package/cli/sweep.mjs +0 -144
package/cli/sweep.mjs DELETED
@@ -1,144 +0,0 @@
1
- #!/usr/bin/env node
2
- /** Closes dead-pid running records without deleting any run artifacts. */
3
- import fs from 'node:fs';
4
- import path from 'node:path';
5
- import { formatSilence, heartbeatAge, isHeartbeatFresh } from '../src/heartbeat.mjs';
6
- import { markAbandoned, pidAlive } from '../src/meta/run-state.mjs';
7
- import { runsRoot } from '../src/runner/runs-root.mjs';
8
- import { readJson } from '../src/write-meta.mjs';
9
- import { renderTable } from './table.mjs';
10
-
11
- const CLOSED_COLUMNS = [
12
- { key: 'project', label: 'project' },
13
- { key: 'run', label: 'run' },
14
- { key: 'state', label: 'state' },
15
- ];
16
- const STALE_COLUMNS = [
17
- { key: 'project', label: 'project' },
18
- { key: 'run', label: 'run' },
19
- { key: 'silence', label: 'silent for' },
20
- ];
21
-
22
- function result(exitCode, output) {
23
- return { exitCode, output };
24
- }
25
-
26
- function parseArgs(argv) {
27
- const positional = [];
28
- for (const arg of argv) {
29
- if (typeof arg !== 'string' || arg.startsWith('-')) {
30
- return { error: `codex-bridge sweep: unknown option "${arg}".` };
31
- }
32
- positional.push(arg);
33
- }
34
- if (positional.length > 1) {
35
- return { error: 'codex-bridge sweep accepts at most one project name.' };
36
- }
37
- const project = positional[0] || null;
38
- if (project && (path.isAbsolute(project) || path.dirname(project) !== '.')) {
39
- return { error: 'codex-bridge sweep requires a bare project name.' };
40
- }
41
- return { project };
42
- }
43
-
44
- function directories(root) {
45
- try {
46
- return fs.readdirSync(root, { withFileTypes: true })
47
- .filter((entry) => entry.isDirectory())
48
- .sort((left, right) => left.name.localeCompare(right.name));
49
- } catch {
50
- return [];
51
- }
52
- }
53
-
54
- function selectedProjects(root, projectName) {
55
- const entries = directories(root);
56
- if (!projectName) {
57
- return entries.map((entry) => ({ name: entry.name, dir: path.join(root, entry.name) }));
58
- }
59
- const entry = entries.find((candidate) => candidate.name === projectName);
60
- if (!entry) return { error: `codex-bridge sweep: unknown project "${projectName}".` };
61
- return [{ name: entry.name, dir: path.join(root, entry.name) }];
62
- }
63
-
64
- function staleLiveRuns(project) {
65
- // The 2026-08-06 incident showed why stale heartbeat is report-only: a live worker remains
66
- // the sole writer until stop kills it and records the outcome.
67
- const stale = [];
68
- for (const entry of directories(project.dir)) {
69
- const dir = path.join(project.dir, entry.name);
70
- const status = readJson(path.join(dir, 'status.json'));
71
- if (!status || status.state !== 'running' || !pidAlive(status.pid) || isHeartbeatFresh(dir)) continue;
72
- stale.push({
73
- project: project.name,
74
- run: entry.name,
75
- silence: formatSilence(heartbeatAge(dir)),
76
- });
77
- }
78
- return stale;
79
- }
80
-
81
- export function sweepPlan(argv = [], options = {}) {
82
- const parsed = parseArgs(argv);
83
- if (parsed.error) return parsed;
84
- const root = path.resolve(options.runsRootPath || runsRoot());
85
- const projects = selectedProjects(root, parsed.project);
86
- if (projects.error) return projects;
87
- return { root, project: parsed.project, projects };
88
- }
89
-
90
- function closeDeadRuns(plan) {
91
- const closed = [];
92
- const failures = [];
93
- for (const project of plan.projects) {
94
- try {
95
- // No worktree snapshot is passed, and that is the choice rather than an omission: `stop`
96
- // closes one named run and knows which repository to look at, while a sweep of the whole
97
- // store would have to run git in every repository every project ever pointed at. The runs
98
- // it closes died long ago, so that list would describe today's tree, not their work — the
99
- // reason text already says a file list is unavailable, which is the honest answer here.
100
- for (const change of markAbandoned(project.dir)) {
101
- closed.push({ project: project.name, run: change.run, state: change.state });
102
- }
103
- } catch (error) {
104
- failures.push({ project: project.name, message: error.message });
105
- }
106
- }
107
- return { closed, failures };
108
- }
109
-
110
- function renderReport(plan, closed, stale, failures, options) {
111
- const scope = plan.project ? `project "${plan.project}"` : 'all projects';
112
- const lines = [`Sweep of ${scope} completed.`];
113
- if (closed.length) {
114
- lines.push(`Closed ${closed.length} running record${closed.length === 1 ? '' : 's'}:`);
115
- lines.push(renderTable(CLOSED_COLUMNS, closed, options));
116
- } else {
117
- lines.push('Nothing to close; the store is unchanged.');
118
- }
119
- if (stale.length) {
120
- lines.push(`Left ${stale.length} live-pid run${stale.length === 1 ? '' : 's'} with stale heartbeats untouched:`);
121
- lines.push(renderTable(STALE_COLUMNS, stale, options));
122
- lines.push('End each one explicitly:');
123
- for (const run of stale) lines.push(` codex-bridge stop ${run.run}`);
124
- } else {
125
- lines.push('Live-pid runs with stale heartbeats left untouched: none.');
126
- }
127
- if (failures.length) {
128
- lines.push('Some project records could not be swept:');
129
- for (const failure of failures) lines.push(` ${failure.project}: ${failure.message}`);
130
- }
131
- return lines.join('\n');
132
- }
133
-
134
- export function sweep(argv = [], options = {}) {
135
- const plan = sweepPlan(argv, options);
136
- if (plan.error) return result(2, plan.error);
137
- const { closed, failures } = closeDeadRuns(plan);
138
- // This is a report-only probe. markAbandoned owns the closure decision and is the only writer.
139
- const stale = plan.projects.flatMap((project) => staleLiveRuns(project));
140
- return result(
141
- failures.length ? 1 : 0,
142
- renderReport(plan, closed, stale, failures, options),
143
- );
144
- }