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 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.0
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
- const o = A.parse(process.argv.slice(2));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bridgie",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "A file-based operating system for running a company's work through AI agents: decide, build, verify, accept, learn.",
5
5
  "type": "module",
6
6
  "bin": { "bridgie": "bin/bridgie.mjs" },
@@ -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
- const { adapter, model = 'default', effort } = route;
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 { raw, caps } = adapterCapabilities(adapter, seatDir, model);
93
- writeFileSync(join(runDir, 'adapter.json'), raw + '\n');
94
- if ((manifest.tools || []).includes('web') && caps.reads_web !== true) die(`seat ${name} needs web access; adapter '${adapter}' has none (route it to one that does: routing.seats.${name})`);
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 startedAt = new Date().toISOString().replace(/\.\d+Z$/, 'Z'), start = Date.now();
103
- const args = [...pre, '--seat', seatDir, '--prompt', join(runDir, 'prompt.md'), '--out', join(runDir, output), '--cwd', cwd, '--mode', mode, '--model', model, ...(effort ? ['--effort', effort] : [])];
104
- const rc = await launch(cmd, args, join(runDir, 'adapter.log'));
105
- 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 });
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
- // 4. single-writer guard (P1): any added, removed or changed state file fails the run
108
- const d = check(root, join(runDir, '.snapshot'), excludes);
109
- if (d.added.length || d.removed.length || d.changed.length) {
110
- const restored = restore(root, join(runDir, '.snapshot'), excludes);
111
- writeFileSync(join(runDir, 'VIOLATION'), `seat ${name} modified Bridgie state directly; state restored from snapshot\n${JSON.stringify(restored)}\n`);
112
- die(`seat ${name} wrote to Bridgie state files; run failed and state restored (see ${join(runDir, 'VIOLATION')})`);
113
- }
114
- rmSync(join(runDir, '.snapshot'), { recursive: true, force: true }); // kept only beside a VIOLATION
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
- die(`adapter ${adapter} exited ${rc} for seat ${name}${why ? `: ${why}` : ''}; see ${join(runDir, 'adapter.log')}`);
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
  }
@@ -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
- const run = (cmd, args) => spawnSync(cmd, args, { stdio: 'inherit', shell: IS_WIN }); // npm/npx are .cmd files on Windows
52
- const q = s => (IS_WIN && /\s/.test(s) ? `"${s}"` : s); // shell: true on Windows needs quoting
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 (!a['no-link']) {
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 ? (spawnSync(npm, ['root', '-g'], { encoding: 'utf8', shell: IS_WIN }).stdout || '').trim() : '';
66
- if (!npm) say(`npm not found; put ${join(HOME, 'bin')} on your PATH to use bridgie`);
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', q(target)]);
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; put ${join(HOME, 'bin')} on your PATH instead, or fix npm's global prefix (npm config get prefix)`);
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.map(q)) : { status: 1 };
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.join(' ')}`);
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
 
@@ -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: [] };
@@ -99,6 +99,12 @@
99
99
  "routing": {
100
100
  "type": "object",
101
101
  "properties": {
102
+ "fallback": {
103
+ "type": "array",
104
+ "items": {
105
+ "type": "string"
106
+ }
107
+ },
102
108
  "tiers": {
103
109
  "type": "object",
104
110
  "additionalProperties": {