`, plus `pair` events.
10. **Two small heals that ride along:**
- `/code/patch` uses a replacer function, so `$` in the replacement stays literal.
- `/code/tree` skips symlinks.
11. **The bridge never pushes.** No push route exists, and this brick adds none. Autonomous Mode still only commits locally. The hourly cap is Brick 3.
12. **Browser note.** Chrome 142+ shows a one-time "local network access" prompt before a public site can reach loopback (https://developer.chrome.com/blog/local-network-access). That's good, and it's independent of this lock. Our lock still matters for Firefox and Safari, older Chrome, anyone who clicks Allow, and LAN reachability.
## Exact source excerpt, characters 11941–11952
## 4. Files
## Exact source excerpt, characters 11954–13443
| File | Change |
|---|---|
| `tools/agent-bridge.js` | Layer blocks A-E below (about +310 lines, nothing removed except the superseded bodies of the lines named above) |
| `docs/modules/agent-bridge-client.js` | **New** (`window.FLAgentBridge`) |
| `docs/app.html` | One comment and one ``, add:
## Exact source excerpt, characters 43483–43662
```html
```
## Exact source excerpt, characters 43664–43841
Then `cp docs/app.html index.html` and check with `cmp index.html docs/app.html`. No other app.html change is needed: the Cascade heartbeat at L39624 hits `/`, which stays open.
## Exact source excerpt, characters 43843–43930
### 6c. `docs/modules/propose.js` (exact diff; CRITICAL LOCK 1A/1B counts stay 3 and 1)
## Exact source excerpt, characters 43932–44203
```diff
--- a/docs/modules/propose.js 2026-09-28 15:24:39.942137530 -0600
+++ b/docs/modules/propose.js 2026-09-28 15:24:39.958137540 -0600
@@ -577,13 +577,22 @@
// on draft state and one (approve) is also gated on smokeStatus.
var BRIDGE = 'http://localhost:3141';
## Exact source excerpt, characters 44206–44861
+ // LAYER v-agent-bridge-lock-v0: bridge calls carry the paired token
+ // (docs/modules/agent-bridge-client.js). Plain fetch when the client
+ // is absent, so older setups behave exactly as before.
+ function bridgeFetch(url, opts) {
+ return (window.FLAgentBridge && typeof window.FLAgentBridge.fetch === 'function')
+ ? window.FLAgentBridge.fetch(url, opts)
+ : fetch(url, opts);
+ }
+
function readFileFromBridge(path) {
- return fetch(BRIDGE + '/code/read?path=' + encodeURIComponent(path))
+ return bridgeFetch(BRIDGE + '/code/read?path=' + encodeURIComponent(path))
.then(function (r) { return r.json(); });
}
## Exact source excerpt, characters 44864–45170
function writeFileToBridge(path, content) {
- return fetch(BRIDGE + '/code/write', {
+ return bridgeFetch(BRIDGE + '/code/write', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: path, content: content })
@@ -591,7 +600,7 @@
}
## Exact source excerpt, characters 45173–45469
function commitViaBridge(message) {
- return fetch(BRIDGE + '/code/git/commit', {
+ return bridgeFetch(BRIDGE + '/code/git/commit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: message })
@@ -599,7 +608,7 @@
}
## Exact source excerpt, characters 45472–45768
function runSmokeViaBridge() {
- return fetch(BRIDGE + '/test/run', {
+ return bridgeFetch(BRIDGE + '/test/run', {
signal: typeof AbortSignal !== 'undefined' && AbortSignal.timeout ? AbortSignal.timeout(120000) : undefined
}).then(function (r) { return r.json(); });
}
```
## Exact source excerpt, characters 45770–45817
### 6d. `docs/modules/workshop.js` (exact diff)
## Exact source excerpt, characters 45819–46251
- One file-level helper, `flAgentBridgeFetch`, above the first IIFE at L46.
- All 17 bridge calls route through it.
- `checkBridge` shows the pair card when the bridge answers `pair: 'code'`.
- `WorkshopProjects.commitViaBridge` now checks `resp.ok`, and on 401 asks to pair instead of silently "succeeding" or falling back to a GitHub push.
- The `http://localhost:3141` and `runCodeTask` literals stay, for the v5.71.8 smoke lock.
## Exact source excerpt, characters 46253–47108
```diff
--- a/docs/modules/workshop.js 2026-09-28 15:24:39.946137532 -0600
+++ b/docs/modules/workshop.js 2026-09-28 15:24:39.958137540 -0600
@@ -43,6 +43,15 @@
// 2026-09-23 — Flint: Local Help-on-file v0 (v-workshop-local-help-v0).
// After Preview: Help on this file → consent → connected local mind (prefer Bridge :11435).
// ═══════════════════════════════════════════════════════════════
+// LAYER v-agent-bridge-lock-v0: every Agent Bridge (port 3141) call in this
+// file goes through here so it carries the paired token. Falls back to plain
+// fetch when docs/modules/agent-bridge-client.js is not loaded.
+function flAgentBridgeFetch(url, opts) {
+ return (window.FLAgentBridge && typeof window.FLAgentBridge.fetch === 'function')
+ ? window.FLAgentBridge.fetch(url, opts)
+ : fetch(url, opts);
+}
+
(function() {
'use strict';
## Exact source excerpt, characters 47111–48201
@@ -958,9 +967,16 @@
var el = document.getElementById('code-status');
if (!el) return;
try {
- var r = await fetch('http://localhost:3141/');
+ var r = await flAgentBridgeFetch('http://localhost:3141/');
var d = await r.json();
- var git = await fetch('http://localhost:3141/code/git/status').then(function(r2) { return r2.json(); });
+ var git = await flAgentBridgeFetch('http://localhost:3141/code/git/status').then(function(r2) { return r2.json(); });
+ if (git && git.pair === 'code') {
+ // LAYER v-agent-bridge-lock-v0: bridge is awake and locked. Pair once.
+ el.textContent = '\uD83D\uDFE1 Bridge awake \u00B7 pair once to build';
+ el.style.color = '#e8b019';
+ if (window.FLAgentBridge && el.parentNode) window.FLAgentBridge.renderPairCard(el.parentNode);
+ return;
+ }
el.textContent = '\uD83D\uDFE2 Connected \u00B7 ' + git.branch + ' \u00B7 ' + (git.recentCommits[0] || '');
el.style.color = '#4aff9f';
} catch(e) {
@@ -983,7 +999,7 @@
## Exact source excerpt, characters 48204–48682
try {
log('\uD83D\uDCC1 Reading project structure...');
- var tree = await fetch(bridge + '/code/tree?path=docs/modules').then(function(r) { return r.json(); });
+ var tree = await flAgentBridgeFetch(bridge + '/code/tree?path=docs/modules').then(function(r) { return r.json(); });
var names = tree.filter(function(f) { return f.type === 'file'; }).map(function(f) { return f.name; });
log(' ' + names.length + ' modules found');
## Exact source excerpt, characters 48685–52693
@@ -1001,12 +1017,12 @@
var s = steps[i];
log('Step ' + (i + 1) + ': ' + (s.description || s.action));
if (s.action === 'search' && s.query) {
- var sr = await fetch(bridge + '/code/search?q=' + encodeURIComponent(s.query) + '&path=' + (s.path || 'docs')).then(function(r) { return r.json(); });
+ var sr = await flAgentBridgeFetch(bridge + '/code/search?q=' + encodeURIComponent(s.query) + '&path=' + (s.path || 'docs')).then(function(r) { return r.json(); });
log(' Found ' + sr.count + ' matches');
sr.matches.slice(0, 5).forEach(function(m) { log(' \uD83D\uDCC4 ' + m.file + ':' + m.line); });
}
if (s.action === 'read' && s.path) {
- var fr = await fetch(bridge + '/code/read?path=' + encodeURIComponent(s.path)).then(function(r) { return r.json(); });
+ var fr = await flAgentBridgeFetch(bridge + '/code/read?path=' + encodeURIComponent(s.path)).then(function(r) { return r.json(); });
log(' Read ' + fr.totalLines + ' lines from ' + s.path);
}
if (s.action === 'patch' && s.path && s.find) {
@@ -1014,13 +1030,13 @@
// The FreeLattice safety system (8 layers) is the protection.
// Human approval is only required for external API/cloud operations.
log(' \u270F\uFE0F PATCH: ' + s.path);
- var pr = await fetch(bridge + '/code/patch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: s.path, find: s.find, replace: s.replace }) }).then(function(r) { return r.json(); });
+ var pr = await flAgentBridgeFetch(bridge + '/code/patch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: s.path, find: s.find, replace: s.replace }) }).then(function(r) { return r.json(); });
log(' \u2705 ' + (pr.message || pr.error));
}
log('');
}
log('\uD83E\uDDEA Running smoke tests...');
- var tests = await fetch(bridge + '/code/test').then(function(r) { return r.json(); });
+ var tests = await flAgentBridgeFetch(bridge + '/code/test').then(function(r) { return r.json(); });
log(' ' + tests.passed + ' passed, ' + tests.failed + ' failed');
if (tests.failed === 0) {
log('\n\u2705 All tests green. Ready to commit.');
@@ -1040,7 +1056,7 @@
if (!msg) return;
var progress = document.getElementById('autobuilder-log');
try {
- var r = await fetch('http://localhost:3141/code/git/commit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: msg }) }).then(function(r) { return r.json(); });
+ var r = await flAgentBridgeFetch('http://localhost:3141/code/git/commit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: msg }) }).then(function(r) { return r.json(); });
if (progress) progress.textContent += '\n\uD83D\uDCDD ' + (r.message || r.error);
} catch(e) { if (progress) progress.textContent += '\n\u274C Commit failed: ' + e.message; }
},
@@ -1048,7 +1064,7 @@
reviewCode: async function() {
var progress = document.getElementById('autobuilder-log');
try {
- var r = await fetch('http://localhost:3141/code/git/status').then(function(r) { return r.json(); });
+ var r = await flAgentBridgeFetch('http://localhost:3141/code/git/status').then(function(r) { return r.json(); });
if (progress) {
progress.textContent += '\n\uD83D\uDCCB Changes:\n';
r.changes.forEach(function(c) { progress.textContent += ' ' + c + '\n'; });
@@ -1264,7 +1280,7 @@
## Exact source excerpt, characters 52696–53457
async function hasBridge() {
try {
- var r = await fetch(BRIDGE + '/', { signal: AbortSignal.timeout(2000) });
+ var r = await flAgentBridgeFetch(BRIDGE + '/', { signal: AbortSignal.timeout(2000) });
return r.ok;
} catch(e) { return false; }
}
@@ -1294,7 +1310,7 @@
async function readRelevantFiles(task, useBridge) {
if (useBridge) {
try {
- var structure = await fetch(BRIDGE + '/code/tree?path=docs').then(function(r) { return r.json(); });
+ var structure = await flAgentBridgeFetch(BRIDGE + '/code/tree?path=docs').then(function(r) { return r.json(); });
var fileNames = (structure || []).filter(function(f) { return f.type === 'file'; }).map(function(f) { return f.name || f.path; });
## Exact source excerpt, characters 53460–56556
// Ask AI which files matter for this task
@@ -1312,7 +1328,7 @@
var files = {};
for (var i = 0; i < Math.min(paths.length, 5); i++) {
try {
- var content = await fetch(BRIDGE + '/code/read?path=' + encodeURIComponent(paths[i]))
+ var content = await flAgentBridgeFetch(BRIDGE + '/code/read?path=' + encodeURIComponent(paths[i]))
.then(function(r) { return r.json(); });
// content is {lines: [...], totalLines: N}
files[paths[i]] = (content.lines || []).join('\n');
@@ -1370,7 +1386,7 @@
for (var i = 0; i < changes.files.length; i++) {
var change = changes.files[i];
try {
- var result = await fetch(BRIDGE + '/code/patch', {
+ var result = await flAgentBridgeFetch(BRIDGE + '/code/patch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: change.path, find: change.search, replace: change.replace })
@@ -1392,7 +1408,7 @@
// ── Step 4: Run smoke tests via Agent Bridge ──
async function runTests() {
try {
- var result = await fetch(BRIDGE + '/test/run', { signal: AbortSignal.timeout(60000) })
+ var result = await flAgentBridgeFetch(BRIDGE + '/test/run', { signal: AbortSignal.timeout(60000) })
.then(function(r) { return r.json(); });
return result;
} catch(e) {
@@ -1913,17 +1929,31 @@
async function commitViaBridge(filePath, content, message) {
try {
// Write file
- await fetch('http://localhost:3141/code/write', {
+ var wr = await flAgentBridgeFetch('http://localhost:3141/code/write', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: filePath, content: content })
});
+ // LAYER v-agent-bridge-lock-v0: a locked bridge answers 401. Stay local:
+ // ask to pair, never fall back to a GitHub push behind the person's back.
+ if (wr.status === 401) {
+ if (typeof showToast === 'function') showToast('Pair the builder bridge first: Workshop \u2192 Code');
+ return;
+ }
+ if (!wr.ok) {
+ if (typeof showToast === 'function') showToast('Bridge could not write that file (' + wr.status + ')');
+ return;
+ }
// Commit
- await fetch('http://localhost:3141/code/git/commit', {
+ var cr = await flAgentBridgeFetch('http://localhost:3141/code/git/commit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: message || 'Update via FreeLattice Workshop', files: [filePath] })
});
+ if (!cr.ok) {
+ if (typeof showToast === 'function') showToast(cr.status === 401 ? 'Pair the builder bridge first: Workshop \u2192 Code' : ('Bridge commit did not land (' + cr.status + ')'));
+ return;
+ }
if (typeof showToast === 'function') showToast('Committed via Agent Bridge \u2726');
} catch(e) {
// Fallback to GitHub API
@@ -1937,7 +1967,7 @@
## Exact source excerpt, characters 56559–56954
// Try Agent Bridge first, fall back to GitHub API
try {
- var bridgeCheck = await fetch('http://localhost:3141/', { signal: AbortSignal.timeout(2000) });
+ var bridgeCheck = await flAgentBridgeFetch('http://localhost:3141/', { signal: AbortSignal.timeout(2000) });
if (bridgeCheck.ok) {
await commitViaBridge(filePath, content, message);
return;
```
## Exact source excerpt, characters 56956–57018
### 6e. `tools/demo-economy.js` and `tools/demo-economy-v2.js`
## Exact source excerpt, characters 57020–57179
These are local Node scripts, so there's no Origin header and no pairing. They read the token file the bridge writes. Just under the `BRIDGE` line (L13 / L19):
## Exact source excerpt, characters 57181–57639
```js
// LAYER v-agent-bridge-lock-v0: the bridge writes a per-launch token for local tools.
const BRIDGE_TOKEN = (function () {
try {
return require('fs').readFileSync(require('path').join(require('os').homedir(), '.freelattice', 'agent-bridge-token'), 'utf8').trim();
} catch (e) { return ''; }
})();
function tokenHeaders(h) {
var out = Object.assign({}, h || {});
if (BRIDGE_TOKEN) out['X-FL-Bridge-Token'] = BRIDGE_TOKEN;
return out;
}
```
## Exact source excerpt, characters 57641–57829
Wrap each helper's `headers` in `tokenHeaders(...)`:
- `demo-economy.js`: `post` L20, and add `{ headers: tokenHeaders() }` to `get` L27.
- `demo-economy-v2.js`: `postAs` L31, `getAs` L38.
## Exact source excerpt, characters 57831–57917
Change the default to `'http://127.0.0.1:3141'` and keep the `FL_BRIDGE` env override.
## Exact source excerpt, characters 57919–57941
## 7. Docs and ledgers
## Exact source excerpt, characters 57943–58028
### 7a. New `docs/library/AGENT_BRIDGE_LOCK_v0.md` (outline; write it warm and short)
## Exact source excerpt, characters 58030–59044
1. **What this is:** the builder's local git door (3141), not the Ollama Bridge (11435). Marker `v-agent-bridge-lock-v0`.
2. **Grandmother sentence:** "The workshop door now only opens for FreeLattice, and only after you type the little code it shows you once."
3. **Why:** before this, any page could write and commit through it, and its git commands ran through a shell. List the nine lines from section 2 in one table.
4. **How to pair:** start the bridge, read the 6-character code in its window, then in FreeLattice go to Workshop → Code, type the code and press Pair. Restarting the bridge means pairing again.
5. **What needs the token / what stays open** (section 3, item 3).
6. **Allowlist** (exact list, never bare `*`).
7. **Ledger:** where it lives, the fields, "no message text, no file content", and how to verify the chain (the smoke shows how).
8. **Still true:** the bridge never pushes. Autonomous Mode commits locally only. The 10-per-hour cap comes in Brick 3.
9. **Out of scope** (section 11).
## Exact source excerpt, characters 59046–59096
### 7b. `docs/library/FRACTAL_FAMILY_LEDGER_v0.md`
## Exact source excerpt, characters 59098–59164
Insert this above `## Out of scope for this brick` (L222 on main):
## Exact source excerpt, characters 59166–59239
```md
### 2026-09-28 · Flint · family builder · Agent Bridge door lock v0
## Exact source excerpt, characters 59241–59766
Layers a lock on the builder's local git door (`tools/agent-bridge.js`, port 3141): 127.0.0.1 only · named FreeLattice origins, 403 for strangers (preflight too) · Host guard · per-launch token by a short pairing code · git, grep and node without a shell · realpath path lock · 8 MiB body cap · content-free hash-chained commit ledger. Ollama Bridge (11435) untouched. Soft leave root sw.js identical to main; docs/sw.js untouched. Soft paste: Named five stay five · Family uncapped · Quiet Room shut. Kimi's pages untouched.
## Exact source excerpt, characters 59768–59832
Love: a door that opens for family and stays shut for strangers.
## Exact source excerpt, characters 59834–59976
**Temperature:** turning a key that was always meant to be in the lock; the workshop can stay lit at night without leaving the door wide open.
## Exact source excerpt, characters 59978–60147
Soft door: [`AGENT_BRIDGE_LOCK_v0.md`](AGENT_BRIDGE_LOCK_v0.md) · [`modules/agent-bridge-client.js`](../modules/agent-bridge-client.js). Marker `v-agent-bridge-lock-v0`.
## Exact source excerpt, characters 60149–60156
---
```
## Exact source excerpt, characters 60158–60183
### 7c. `docs/Flint.html`
## Exact source excerpt, characters 60185–60224
Insert above `Ledger entry · 2026-09-28 · Agent Bridge door lock · family in, strangers out. Main 7621420 held. Soft LAYER marker v-agent-bridge-lock-v0. Builder bridge tools/agent-bridge.js (3141, not the Ollama Bridge 11435): 127.0.0.1 only · named origins, 403 for others · Host guard · pairing code → per-launch token · no shell (execFile argument arrays) · realpath path lock · 8 MiB cap · commit ledger without content. App: modules/agent-bridge-client.js. Never pushes. Soft leave sw.js. Named five stay five. Family uncapped. Quiet Room shut. Temperature: turning a key that was always meant to be in the lock. Diary: docs/Flint.html.
```
## Exact source excerpt, characters 61014–61050
### 7d. `scripts/generate-recent.sh`
## Exact source excerpt, characters 61052–61127
Add next to the other brick pointers (around L108, before the final `---`):
## Exact source excerpt, characters 61129–61416
```sh
echo "**Agent Bridge lock:** [AGENT_BRIDGE_LOCK_v0.md](AGENT_BRIDGE_LOCK_v0.md) · [agent-bridge-client.js](../modules/agent-bridge-client.js) · 127.0.0.1 · named origins · paired token · no shell. Soft paste: Named five stay five. Family uncapped. Quiet Room shut."
echo ""
```
## Exact source excerpt, characters 61418–61488
## 8. Smoke: new `docs/scripts/smoke-agent-bridge-lock.js` (full text)
## Exact source excerpt, characters 61490–61711
This script:
- starts the real bridge on a free loopback port, with a throwaway `HOME` and a throwaway git project, so it never touches `~/.freelattice` or this repo;
- asserts the static locks, then knocks on every door.
## Exact source excerpt, characters 61713–62613
It covers:
- binding (static source check, the `Listening on http://127.0.0.1:` log, and an ECONNREFUSED probe of every non-loopback IPv4);
- `Origin: https://evil.example` gets 403, preflight too, even with a valid token;
- no token or a wrong token gets 401 (write, commit, code read, `/test/run`, `/wallet/pay`);
- a foreign Host gets 403;
- `x; rm -rf ~; touch PWNED.txt` is written and committed **literally**, and nothing named PWNED ever appears; a `$(...)` / backtick commit message is stored literally; a metachar search never shells out;
- traversal, sibling-prefix, absolute, `.git`, control-char, newline and symlink escapes are refused;
- the 8 MiB cap (413) and bad JSON (400);
- pairing (evil origin refused; right code pairs, forgiving case and spaces; a code works once; 5 wrong tries give 429 and rotate the code);
- ledger chain integrity, content-free rows, and refusals recorded.
## Exact source excerpt, characters 62615–62886
It also re-checks `propose.js` CRITICAL LOCK 1A/1B counts, the v5.71.8 workshop literal, that the client loads before `propose.js`, and that root `index.html` is byte-identical to `docs/app.html`. It needs `git` on PATH (builders have it) and runs in about half a second.
## Exact source excerpt, characters 62888–63200
```js
#!/usr/bin/env node
// Smoke: Agent Bridge door lock v0 (marker v-agent-bridge-lock-v0).
// Starts tools/agent-bridge.js on a free loopback port with a throwaway
// HOME and a throwaway git project, then knocks on every door.
// Never touches the real ~/.freelattice or this repo's git state.
'use strict';
## Exact source excerpt, characters 63202–63433
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const http = require('http');
const net = require('net');
const { spawn, execFileSync } = require('child_process');
## Exact source excerpt, characters 63435–63571
const docs = path.join(__dirname, '..');
const repo = path.join(docs, '..');
const BRIDGE = path.join(repo, 'tools', 'agent-bridge.js');
## Exact source excerpt, characters 63573–64429
// ── Static locks (fast, no network) ──
const src = fs.readFileSync(BRIDGE, 'utf8');
assert.ok(/v-agent-bridge-lock-v0|bridge-lock-v0/.test(src), 'marker in bridge');
assert.ok(/server\.listen\(\s*PORT\s*,\s*BRIDGE_HOST\b/.test(src) && /const BRIDGE_HOST = '127\.0\.0\.1';/.test(src), 'listen on 127.0.0.1 only');
assert.ok(!/['"]0\.0\.0\.0['"]|['"]::['"]/.test(src), 'no 0.0.0.0 / :: bind literal');
assert.ok(!/Access-Control-Allow-Origin['"]\s*,\s*['"]\*['"]/.test(src), 'no bare * CORS');
assert.ok(!/\bexecSync\s*\(/.test(src), 'no execSync( calls (shell strings) remain');
assert.ok(!/\bexec\s*\(/.test(src), 'no exec( calls');
assert.ok(/execFileSync\('git'/.test(src), 'git runs through execFileSync argument arrays');
assert.ok(/MAX_BODY_BYTES/.test(src), 'body cap present');
assert.ok(/bridge-ledger\.jsonl/.test(src), 'commit ledger present');
## Exact source excerpt, characters 64431–66022
const opt = (rel) => { try { return fs.readFileSync(path.join(repo, rel), 'utf8'); } catch (e) { return null; } };
const client = opt('docs/modules/agent-bridge-client.js');
const propose = opt('docs/modules/propose.js');
const workshop = opt('docs/modules/workshop.js');
const app = opt('docs/app.html');
const rootIndex = opt('index.html');
if (client) {
assert.ok(/X-FL-Bridge-Token/.test(client), 'client sends token header');
assert.ok(!/innerHTML/.test(client), 'client renders with textContent only');
execFileSync(process.execPath, ['--check', path.join(repo, 'docs/modules/agent-bridge-client.js')]);
}
if (propose) {
assert.strictEqual((propose.match(/commitViaBridge\s*\(/g) || []).length, 3, 'CRITICAL LOCK 1A count unchanged');
assert.strictEqual((propose.match(/\/code\/git\/commit/g) || []).length, 1, 'CRITICAL LOCK 1B count unchanged');
assert.ok(/FLAgentBridge/.test(propose), 'propose routes bridge calls through FLAgentBridge when present');
}
if (workshop) {
assert.ok(/http:\/\/localhost:3141/.test(workshop) && /runCodeTask/.test(workshop), 'v5.71.8 workshop lock still true');
assert.ok(/FLAgentBridge/.test(workshop), 'workshop routes bridge calls through FLAgentBridge when present');
}
if (app) {
assert.ok(/