bridgie 0.6.0 → 0.6.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/README.md +8 -0
- package/VERSION +1 -1
- package/adapters/antigravity/run.mjs +9 -1
- package/package.json +1 -1
- package/runner/cli/seat.mjs +53 -22
- package/runner/cmd/setup.mjs +20 -13
- package/runner/cmd/verify.mjs +1 -1
- package/runner/doctor.mjs +11 -0
- package/runner/state.mjs +17 -0
- package/schemas/policy.json +6 -0
package/README.md
CHANGED
|
@@ -93,6 +93,14 @@ Verification commands live in `.bridgie/policy.yaml` (`verify.commands.<tier>`;
|
|
|
93
93
|
writes). Without them Bridgie uses the lane's commands for the stacks it detects; a tier the risk requires
|
|
94
94
|
but nobody configured is never skipped silently.
|
|
95
95
|
|
|
96
|
+
Which model CLI runs each seat is set in `~/.bridgie/policy.yaml` (`routing.tiers`, `routing.seats`). When the
|
|
97
|
+
routed CLI cannot run a seat at all (not installed, not set up for the folder, signed out, over its limit,
|
|
98
|
+
crashed), Bridgie warns and runs the seat on the next CLI that is installed and fits it: the agent you are
|
|
99
|
+
running Bridgie from, then Claude Code, Codex, Antigravity, Copilot. A model that ran but answered badly is not
|
|
100
|
+
retried elsewhere, and the reviewer still needs a different model family from the builder. Set your own order
|
|
101
|
+
with `routing.fallback: [codex, claude-code]`, turn it off with `routing.fallback: []`, or pin one CLI for a
|
|
102
|
+
command with `BRIDGIE_ADAPTER=<name>`.
|
|
103
|
+
|
|
96
104
|
While an agent runs, Bridgie state is read-only (a project-wide seat lock): your decisions are refused with a
|
|
97
105
|
message rather than rolled back, and anything the agent adds, changes or deletes under `.bridgie/` is restored.
|
|
98
106
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.6.
|
|
1
|
+
0.6.1
|
|
@@ -4,7 +4,9 @@ import { dirname, join, basename } from 'node:path';
|
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import * as A from '../lib/common.mjs';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// --check-cwd DIR (used by bridgie doctor): OK when an Antigravity project covers DIR, else the reason.
|
|
8
|
+
const cc = process.argv.indexOf('--check-cwd');
|
|
9
|
+
const o = cc >= 0 ? {} : A.parse(process.argv.slice(2));
|
|
8
10
|
function findAgy() {
|
|
9
11
|
if (process.env.BRIDGIE_AGY_BIN) return process.env.BRIDGIE_AGY_BIN;
|
|
10
12
|
const onPath = A.which('agy');
|
|
@@ -33,6 +35,12 @@ function findProject(dir) {
|
|
|
33
35
|
return best?.id || null;
|
|
34
36
|
}
|
|
35
37
|
const bin = findAgy();
|
|
38
|
+
if (cc >= 0) {
|
|
39
|
+
const dir = process.argv[cc + 1];
|
|
40
|
+
const id = findProject(dir);
|
|
41
|
+
if (id) { process.stdout.write(`OK ${id}\n`); process.exit(0); }
|
|
42
|
+
process.stdout.write(`no Antigravity project covers ${dir}\n`); process.exit(1);
|
|
43
|
+
}
|
|
36
44
|
if (o.probe) A.probe(bin, 'agy (PATH or %LOCALAPPDATA%/agy/bin)');
|
|
37
45
|
if (o.caps) A.caps({ vendor: 'google', reads_repo: true, reads_web: false });
|
|
38
46
|
A.requireRunArgs(o);
|
package/package.json
CHANGED
package/runner/cli/seat.mjs
CHANGED
|
@@ -84,39 +84,70 @@ export async function runSeat(slug, seat, opts = {}) {
|
|
|
84
84
|
const { manifest } = loadSeat(seat);
|
|
85
85
|
let route;
|
|
86
86
|
try { route = st.route(root, manifest); } catch (e) { die(`routing failed: ${e.message}`); }
|
|
87
|
-
|
|
88
|
-
if (adapter === 'none') die(`seat ${seat} has no model adapter (mechanical seats run in the verify command)`);
|
|
89
|
-
const [cmd, pre] = adapterCommand(adapter);
|
|
87
|
+
if (route.adapter === 'none') die(`seat ${seat} has no model adapter (mechanical seats run in the verify command)`);
|
|
90
88
|
const mode = manifest.writes === 'worktree' ? 'write' : 'readonly';
|
|
91
89
|
const output = manifest.output;
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
const needsWeb = (manifest.tools || []).includes('web');
|
|
91
|
+
// fits: can this adapter take the seat? (web for research; repo access and another model family
|
|
92
|
+
// for the reviewer, when the caller says so)
|
|
93
|
+
const fits = caps => (!needsWeb || caps.reads_web === true) && (!opts.needsRepo || caps.reads_repo === true) && (!opts.avoidVendor || caps.vendor !== opts.avoidVendor);
|
|
94
|
+
let { adapter, model = 'default', effort } = route;
|
|
95
|
+
let { raw, caps } = adapterCapabilities(adapter, seatDir, model);
|
|
96
|
+
if (needsWeb && caps.reads_web !== true) die(`seat ${name} needs web access; adapter '${adapter}' has none (route it to one that does: routing.seats.${name})`);
|
|
97
|
+
const spare = st.fallbacks(root, adapter);
|
|
98
|
+
// nextFallback: the next spare adapter whose CLI is installed and that fits the seat.
|
|
99
|
+
const nextFallback = () => {
|
|
100
|
+
while (spare.length) {
|
|
101
|
+
const a = spare.shift();
|
|
102
|
+
if (!existsSync(join(HOME, 'adapters', a))) continue;
|
|
103
|
+
const [c, p] = adapterCommand(a);
|
|
104
|
+
const probe = spawnSync(c, [...p, '--probe'], { encoding: 'utf8', timeout: 30000 });
|
|
105
|
+
if (probe.status !== 0 || !/OK/.test(probe.stdout || '')) continue;
|
|
106
|
+
const got = adapterCapabilities(a, seatDir, 'default');
|
|
107
|
+
if (fits(got.caps)) return { adapter: a, ...got };
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
};
|
|
95
111
|
|
|
96
|
-
// 3. lock, snapshot, launch, measure
|
|
112
|
+
// 3. lock, snapshot, launch, measure; on an adapter that cannot run at all, warn and fall back
|
|
97
113
|
const locked = opts.lock !== false;
|
|
98
114
|
const excludes = [relTo(root, runDir), ...(opts.exclude ? [relTo(root, opts.exclude)] : [])];
|
|
99
115
|
if (locked) seatLock(root, name, runDir);
|
|
100
116
|
try {
|
|
101
117
|
try { snapshot(root, join(runDir, '.snapshot'), excludes); } catch (e) { die(e.message); }
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
118
|
+
const fellBack = [];
|
|
119
|
+
for (;;) {
|
|
120
|
+
writeFileSync(join(runDir, 'adapter.json'), raw + '\n');
|
|
121
|
+
const [cmd, pre] = adapterCommand(adapter);
|
|
122
|
+
const startedAt = new Date().toISOString().replace(/\.\d+Z$/, 'Z'), start = Date.now();
|
|
123
|
+
const args = [...pre, '--seat', seatDir, '--prompt', join(runDir, 'prompt.md'), '--out', join(runDir, output), '--cwd', cwd, '--mode', mode, '--model', model, ...(effort ? ['--effort', effort] : [])];
|
|
124
|
+
const rc = await launch(cmd, args, join(runDir, 'adapter.log'));
|
|
125
|
+
st.costAdd(runDir, { seat: name, adapter, model, started_at: startedAt, duration_s: Math.round((Date.now() - start) / 1000), attempt: n, exit_code: rc, tokens_in: null, tokens_out: null, usd: null });
|
|
106
126
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (rc !== 0) {
|
|
127
|
+
// 4. single-writer guard (P1): any added, removed or changed state file fails the run
|
|
128
|
+
const d = check(root, join(runDir, '.snapshot'), excludes);
|
|
129
|
+
if (d.added.length || d.removed.length || d.changed.length) {
|
|
130
|
+
const restored = restore(root, join(runDir, '.snapshot'), excludes);
|
|
131
|
+
writeFileSync(join(runDir, 'VIOLATION'), `seat ${name} modified Bridgie state directly; state restored from snapshot\n${JSON.stringify(restored)}\n`);
|
|
132
|
+
die(`seat ${name} wrote to Bridgie state files; run failed and state restored (see ${join(runDir, 'VIOLATION')})`);
|
|
133
|
+
}
|
|
134
|
+
if (rc === 0) break;
|
|
116
135
|
let why = '';
|
|
117
|
-
try { why = readFileSync(join(runDir, 'adapter.log'), 'utf8').trim().split(/\r?\n/).filter(l => /^(adapter|extract-json):|not found/.test(l)).pop() || ''; } catch {}
|
|
118
|
-
|
|
136
|
+
try { why = readFileSync(join(runDir, 'adapter.log'), 'utf8').trim().split(/\r?\n/).filter(l => /^(adapter|extract-json):|not found|no Antigravity project/.test(l)).pop() || ''; } catch {}
|
|
137
|
+
const failed = `adapter ${adapter} exited ${rc} for seat ${name}${why ? `: ${why}` : ''}`;
|
|
138
|
+
// 1: the adapter could not start (CLI missing, not set up); 6: the CLI failed (signed out, over
|
|
139
|
+
// its limit, crashed); 127: could not launch. A model that ran but answered badly (4, 5) is not
|
|
140
|
+
// a reason to switch models.
|
|
141
|
+
const next = [1, 6, 127].includes(rc) ? nextFallback() : null;
|
|
142
|
+
if (!next) die(`${failed}; see ${join(runDir, 'adapter.log')}${fellBack.length ? ` (after falling back from ${fellBack.join(', ')})` : ''}`);
|
|
143
|
+
process.stderr.write(`bridgie: warning: ${failed}; falling back to ${next.adapter}\n`);
|
|
144
|
+
renameSync(join(runDir, 'adapter.log'), join(runDir, `adapter-${adapter}.log`));
|
|
145
|
+
for (const f of [output, 'reply.txt']) rmSync(join(runDir, f), { force: true });
|
|
146
|
+
fellBack.push(adapter);
|
|
147
|
+
writeFileSync(join(runDir, 'fallback.json'), JSON.stringify({ routed: route.adapter, failed: fellBack, used: next.adapter }, null, 2) + '\n');
|
|
148
|
+
({ adapter, raw, caps } = next); model = 'default'; effort = null;
|
|
119
149
|
}
|
|
150
|
+
rmSync(join(runDir, '.snapshot'), { recursive: true, force: true }); // kept only beside a VIOLATION
|
|
120
151
|
} finally {
|
|
121
152
|
if (locked) seatUnlock(root);
|
|
122
153
|
}
|
package/runner/cmd/setup.mjs
CHANGED
|
@@ -48,8 +48,14 @@ function routing() {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
const inside = (dir, parent) => { const r = relative(parent, dir); return r && !r.startsWith('..') && !isAbsolute(r); };
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
// Run npm or npx. On Windows they are .cmd files, which only run through cmd.exe, so build one command
|
|
52
|
+
// line with every part quoted (npm usually lives in C:\Program Files\nodejs) and let cmd find the
|
|
53
|
+
// command by name; elsewhere run the resolved path directly with no shell.
|
|
54
|
+
const winQuote = s => (/[\s"&|<>^()%!]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s);
|
|
55
|
+
export const commandLine = (name, args) => [name, ...args].map(winQuote).join(' ');
|
|
56
|
+
const run = (name, path, args, opts = {}) => IS_WIN
|
|
57
|
+
? spawnSync(commandLine(name, args), { stdio: 'inherit', ...opts, shell: true })
|
|
58
|
+
: spawnSync(path, args, { stdio: 'inherit', ...opts });
|
|
53
59
|
|
|
54
60
|
export default async function setup(argv) {
|
|
55
61
|
const a = parseArgs(argv, { 'no-link': 'bool', 'no-skill': 'bool', 'no-policy': 'bool', agent: 'list', path: 'bool', policy: 'bool' }, 'setup');
|
|
@@ -57,28 +63,29 @@ export default async function setup(argv) {
|
|
|
57
63
|
const say = s => process.stdout.write(`${s}\n`);
|
|
58
64
|
const npm = which('npm'), npx = which('npx');
|
|
59
65
|
|
|
60
|
-
// 1. command on PATH
|
|
66
|
+
// 1. command on PATH. The old bash-era Windows shim goes only once the new command is in place.
|
|
61
67
|
const oldShim = join(process.env.LOCALAPPDATA || join(home, 'AppData', 'Local'), 'Microsoft', 'WindowsApps', 'bridgie.cmd');
|
|
62
|
-
if (existsSync(oldShim) && /bin[\\/]bridgie"/.test(readFileSync(oldShim, 'utf8'))) { rmSync(oldShim); say(`removed the old bash shim ${oldShim}`); }
|
|
63
|
-
if (
|
|
68
|
+
const dropShim = () => { if (existsSync(oldShim) && /bin[\\/]bridgie"/.test(readFileSync(oldShim, 'utf8'))) { rmSync(oldShim); say(`removed the old bash shim ${oldShim}`); } };
|
|
69
|
+
if (a['no-link']) dropShim();
|
|
70
|
+
else {
|
|
64
71
|
const fromClone = existsSync(join(HOME, '.git'));
|
|
65
|
-
const globalRoot = npm ? (
|
|
66
|
-
if (!npm) say(`npm not found;
|
|
67
|
-
else if (!fromClone && globalRoot && inside(resolve(HOME), resolve(globalRoot))) say(`bridgie ${version()} is already installed with npm`);
|
|
72
|
+
const globalRoot = npm ? (run('npm', npm, ['root', '-g'], { stdio: 'pipe', encoding: 'utf8' }).stdout || '').trim() : '';
|
|
73
|
+
if (!npm) say(`npm not found; install Node.js (it includes npm), then run: npx bridgie@latest setup`);
|
|
74
|
+
else if (!fromClone && globalRoot && inside(resolve(HOME), resolve(globalRoot))) { say(`bridgie ${version()} is already installed with npm`); dropShim(); }
|
|
68
75
|
else {
|
|
69
76
|
const target = fromClone ? HOME : `bridgie@${version()}`;
|
|
70
|
-
const r = run(npm, ['install', '-g',
|
|
71
|
-
if (r.status === 0) say(`bridgie ${fromClone ? 'linked from this clone' : `${version()} installed`} with npm: works in PowerShell, cmd, Git Bash, macOS and Linux terminals`);
|
|
72
|
-
else say(`npm install -g failed;
|
|
77
|
+
const r = run('npm', npm, ['install', '-g', target]);
|
|
78
|
+
if (r.status === 0) { say(`bridgie ${fromClone ? 'linked from this clone' : `${version()} installed`} with npm: works in PowerShell, cmd, Git Bash, macOS and Linux terminals`); dropShim(); }
|
|
79
|
+
else say(`npm install -g failed; run it yourself to see why: npm install -g ${fromClone ? `"${HOME}"` : target} (or check npm's global prefix: npm config get prefix)`);
|
|
73
80
|
}
|
|
74
81
|
}
|
|
75
82
|
|
|
76
83
|
// 2. agent skill, for every agent on this machine, through the skills installer
|
|
77
84
|
if (!a['no-skill']) {
|
|
78
85
|
const args = addArgs(HOME, a.agent);
|
|
79
|
-
const r = npx ? run(npx, args
|
|
86
|
+
const r = npx ? run('npx', npx, args) : { status: 1 };
|
|
80
87
|
if (r.status === 0) say('bridgie skill installed for the agents found on this machine (list them: npx skills ls -g)');
|
|
81
|
-
else say(`could not run the skills installer${npx ? '' : ' (npx not found)'}; install the skill by hand: npx ${args
|
|
88
|
+
else say(`could not run the skills installer${npx ? '' : ' (npx not found)'}; install the skill by hand: npx -y ${args[1]} add anoopvijayan-dev/bridgie -g -y --copy`);
|
|
82
89
|
say('Claude chat and Claude cloud sessions read skills from claude.ai: upload skills/bridgie/SKILL.md there once (and again after it changes)');
|
|
83
90
|
}
|
|
84
91
|
|
package/runner/cmd/verify.mjs
CHANGED
|
@@ -92,7 +92,7 @@ export default async function verify(argv) {
|
|
|
92
92
|
// 4. reviewer in a detached, read-only worktree (R3)
|
|
93
93
|
const rv = g.reviewWorktree(root, slug, task);
|
|
94
94
|
let rvDir, failure = null;
|
|
95
|
-
try { rvDir = await runSeat(slug, 'verify/reviewer', { root, task, cwd: rv, packet: vdir }); } catch (e) { failure = e; }
|
|
95
|
+
try { rvDir = await runSeat(slug, 'verify/reviewer', { root, task, cwd: rv, packet: vdir, needsRepo: true, avoidVendor: fl.crossFamily(root, slug) ? fl.producerVendor(root, slug, task) : '' }); } catch (e) { failure = e; }
|
|
96
96
|
const dirty = git(['-C', rv, 'status', '--porcelain']).out.trim();
|
|
97
97
|
g.removeWorktree(root, rv);
|
|
98
98
|
if (failure) die(`${failure.message}\nverify: reviewer run failed for ${task}; task stays in verify`);
|
package/runner/doctor.mjs
CHANGED
|
@@ -63,6 +63,17 @@ export function doctor(root) {
|
|
|
63
63
|
|
|
64
64
|
if (!root) { warn('not inside a project: tool checks only'); return r; }
|
|
65
65
|
try { st.loadPolicy(root); ok('project policy merges and validates'); } catch (e) { fail(`project policy: ${e.message}`); }
|
|
66
|
+
try { // Antigravity runs only in a folder one of its projects covers; say so before a seat fails
|
|
67
|
+
const pol = st.loadPolicy(root), routes = [...Object.values(pol.routing?.tiers || {}), ...Object.values(pol.routing?.seats || {})];
|
|
68
|
+
if (!process.env.BRIDGIE_ADAPTER && routes.some(x => x.adapter === 'antigravity')) {
|
|
69
|
+
const p = spawnSync(process.execPath, [join(st.HOME, 'adapters', 'antigravity', 'run.mjs'), '--check-cwd', root], { encoding: 'utf8', timeout: 30000 });
|
|
70
|
+
if (p.status === 0) ok(`Antigravity project for this folder: ${(p.stdout || '').trim().replace(/^OK /, '')}`);
|
|
71
|
+
else {
|
|
72
|
+
const spare = st.fallbacks(root, 'antigravity')[0];
|
|
73
|
+
warn(`no Antigravity project covers ${root}; seats routed to Antigravity will ${spare ? `fall back to ${spare}` : 'fail'}. Fix: open this folder once in Antigravity, or set BRIDGIE_AGY_PROJECT to a project id`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch { /* policy errors are reported above */ }
|
|
66
77
|
const betSchema = st.loadSchema('bet'), decSchema = st.loadSchema('decisions'); let betFail = 0;
|
|
67
78
|
for (const s of st.listBets(root)) {
|
|
68
79
|
const e1 = validate(betSchema, st.readFm(st.betPath(root, s)).data); if (e1.length) { betFail++; fail(`bet ${s}: ${e1.join('; ')}`); }
|
package/runner/state.mjs
CHANGED
|
@@ -306,6 +306,23 @@ export function route(root, manifest) {
|
|
|
306
306
|
effort: r.effort || null,
|
|
307
307
|
};
|
|
308
308
|
}
|
|
309
|
+
// parentAdapter: the agent harness running Bridgie, when it can be told from the environment.
|
|
310
|
+
export function parentAdapter() {
|
|
311
|
+
if (process.env.BRIDGIE_PARENT_ADAPTER) return process.env.BRIDGIE_PARENT_ADAPTER;
|
|
312
|
+
if (process.env.CLAUDECODE) return 'claude-code';
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// fallbacks: adapters to try, in order, when the routed one cannot run a seat at all (not installed,
|
|
317
|
+
// not set up, signed out, crashed). BRIDGIE_ADAPTER pins one adapter: no fallback. routing.fallback
|
|
318
|
+
// sets the list ([] turns fallback off); without it: the parent harness, then the known model CLIs.
|
|
319
|
+
export function fallbacks(root, primary) {
|
|
320
|
+
if (process.env.BRIDGIE_ADAPTER) return [];
|
|
321
|
+
const set = loadPolicy(root).routing?.fallback;
|
|
322
|
+
const list = Array.isArray(set) ? set : [parentAdapter(), 'claude-code', 'codex', 'antigravity', 'copilot'].filter(a => a !== 'fake');
|
|
323
|
+
return [...new Set(list.filter(a => a && a !== primary && a !== 'none'))];
|
|
324
|
+
}
|
|
325
|
+
|
|
309
326
|
export function costAdd(runDir, entry) {
|
|
310
327
|
const f = join(runDir, 'cost.json');
|
|
311
328
|
const doc = existsSync(f) ? readJson(f) : { entries: [] };
|
package/schemas/policy.json
CHANGED