@passioncode-ai/passioncode 0.2.45 → 0.2.47

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 (63) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +1 -1
  3. package/SECURITY.md +2 -1
  4. package/family.json +1 -1
  5. package/lib/doctor.js +25 -3
  6. package/lib/launcher.js +34 -5
  7. package/lib/releases.js +3 -2
  8. package/lib/setup.js +2 -1
  9. package/lib/windows-apps.js +74 -0
  10. package/package.json +1 -1
  11. package/payload/.claude-plugin/marketplace.json +2 -2
  12. package/payload/manifest.json +8 -8
  13. package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  14. package/payload/plugins/fabric-agent-adapter/hooks/hooks.json +1 -1
  15. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
  16. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +5 -1
  17. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +73 -18
  18. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +6 -5
  19. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +70 -20
  20. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +7 -1
  21. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +9 -0
  22. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +14 -5
  23. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +1 -0
  24. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +1 -1
  25. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +4 -1
  26. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +16 -9
  27. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +87 -156
  28. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +180 -0
  29. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -0
  30. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +4 -2
  31. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +2 -0
  32. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +15 -3
  33. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +2 -0
  34. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +12 -1
  35. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +12 -1
  36. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -0
  37. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +32 -13
  38. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +115 -147
  39. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +173 -0
  40. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +9 -42
  41. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +53 -4
  42. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +7 -3
  43. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +4 -2
  44. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +87 -11
  45. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +19 -8
  46. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +30 -6
  47. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +9 -1
  48. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +15 -1
  49. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +73 -18
  50. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +1 -1
  51. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +134 -24
  52. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +155 -21
  53. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
  54. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +10 -10
  55. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +2 -1
  56. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +39 -13
  57. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +6 -2
  58. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +17 -3
  59. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +23 -6
  60. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +10 -39
  61. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +190 -0
  62. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +1 -1
  63. package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.47 - 2026-10-28
4
+
5
+ ### Fixed
6
+
7
+ - **`update` on Windows moves `current` to the new release** (PC-85, the Windows acceptance of 2026-10-28): Windows
8
+ refuses a rename over a directory link (`EPERM ... rename 'current.<pid>.lnk' -> 'current'`), so an update stopped
9
+ halfway, kept the old release and left a `current.<pid>.lnk` per attempt. There the old link is now removed first
10
+ (the link itself, never what it points at) and put back if the rename fails again; the temporary link never outlives
11
+ the call, and the ones a failed update left are removed by the next one.
12
+ - **`doctor` checks the apps on Windows** (PC-86): it reads each app's uninstall entry (`reg query`, the name and
13
+ version its installer wrote) or finds it in `%LOCALAPPDATA%\Programs\<name>`, `%LOCALAPPDATA%\<name>` or Program
14
+ Files, and says which version is installed and whether a newer one is out, instead of "app checks run on macOS".
15
+ Setup still installs apps on macOS only.
16
+
17
+ ## 0.2.46 - 2026-10-28
18
+
19
+ ### Changed
20
+
21
+ - **Fabric Agent Adapter is pinned at `v0.56.0`** (it was `v0.55.0`): stability and Windows. working-with-fabric names
22
+ the repositories a Windows turn touched; the board responder recovers from a refused slot, leaves within 3 s when
23
+ idle and logs its notices at `warning` with a subject; the reference service can take a moved slot back; a spend
24
+ gate fails closed.
25
+
3
26
  ## 0.2.45 - 2026-10-27
4
27
 
5
28
  ### Changed
package/README.md CHANGED
@@ -47,7 +47,7 @@ secret into a product's own hidden prompt, starting Docker for Fabric, and resta
47
47
  one question per app; `--yes` accepts the plan, `--with`/`--without <ids>` choose apps
48
48
  (`switchboard`, `dashboards`, `observatory`, `inbox`, `fabric`), `--no-apps` sets up the skills
49
49
  and MCP only, `--dry-run` shows the plan. Apps are installed on macOS; on other systems setup
50
- names each download page. Skills only: `npx @passioncode-ai/passioncode@latest update`.
50
+ names each download page, and on Windows `doctor` reads which version of each app is installed. Skills only: `npx @passioncode-ai/passioncode@latest update`.
51
51
  2. **Configure:** nothing to set, no account and no key. Background updates are on by default;
52
52
  `npx @passioncode-ai/passioncode@latest config set update.auto off` turns them off. Each app's
53
53
  own onboarding (sign-ins, Fabric Inbox's server, Observatory's projects) is listed by `setup`.
package/SECURITY.md CHANGED
@@ -32,7 +32,8 @@ returns a nonzero exit code. See the [CLI recovery contract](docs/reference/cli.
32
32
  `doctor` reads: `~/.claude.json`, `~/.codex/config.toml`, `~/.gemini/settings.json`,
33
33
  `~/.kimi-code/mcp.json` (entry names, commands and arguments; environment values are passed to
34
34
  the server it starts and never printed), the Fabric service descriptors (never their token
35
- files), the apps' `Info.plist`, and the GitHub releases API. It starts each registered stdio MCP
35
+ files), the apps' `Info.plist` (on Windows, the apps' uninstall entries through `reg query` and their install folders),
36
+ and the GitHub releases API. It starts each registered stdio MCP
36
37
  server for one proving call and stops it (never Fabric's, which enrols a session as it starts), and writes only `~/.passioncode/doctor.json` and a
37
38
  release cache.
38
39
 
package/family.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "name": "fabric-agent-adapter",
9
9
  "displayName": "Fabric Agent Adapter",
10
10
  "repo": "passioncode-ai/fabric-agent-adapter",
11
- "ref": "v0.55.0",
11
+ "ref": "v0.56.0",
12
12
  "kind": "plugin",
13
13
  "path": "plugins/fabric-agent-adapter",
14
14
  "legacyPluginIds": [
package/lib/doctor.js CHANGED
@@ -3,7 +3,8 @@
3
3
  * `passioncode doctor`: reads the whole PassionCode.ai setup of this machine and names, for every
4
4
  * check, its state, the exact fix and what setup may repair (ADR-0004). It changes nothing: the only
5
5
  * processes it starts are the registered stdio MCP servers for one proving call each (never Fabric's,
6
- * which enrols a session when it starts), stopped when the call returns. `doctor --fix` is setup's
6
+ * which enrols a session when it starts), stopped when the call returns, and on Windows three
7
+ * read-only `reg query` calls for the apps' uninstall entries (PC-86). `doctor --fix` is setup's
7
8
  * repair half (lib/setup.js), not this module.
8
9
  */
9
10
  const fs = require('fs');
@@ -12,6 +13,7 @@ const path = require('path');
12
13
  const L = require('./launcher');
13
14
  const catalog = require('./catalog');
14
15
  const releases = require('./releases');
16
+ const W = require('./windows-apps');
15
17
  const { probe } = require('./mcp-probe');
16
18
 
17
19
  const PKG = (() => { try { return require('../package.json').version; } catch (_) { return '0.0.0'; } })();
@@ -123,12 +125,32 @@ async function collect({ home, platform, claude, online }) {
123
125
  const cacheFile = path.join(p.state, 'releases-cache.json');
124
126
  const probes = new Map();
125
127
  let docker = null;
128
+ let windowsEntries = null; // the uninstall entries, read once (three `reg query` calls)
126
129
  for (const pr of catalog.PRODUCTS) {
127
130
  const at = catalog.appPath(pr);
128
131
  const installed = platform === 'darwin' && fs.existsSync(at);
129
132
  const label = `${pr.name}${pr.preview ? ' (early preview)' : ''}`;
130
- if (platform !== 'darwin') {
131
- add({ id: `app.${pr.id}`, area: 'apps', product: pr.id, state: 'info', detail: `${label}: app checks run on macOS; on this system download it from ${pr.page}` });
133
+ if (platform === 'win32') {
134
+ // PC-86: presence and version from the installer's own records; setup installs nothing here.
135
+ if (!windowsEntries) {
136
+ windowsEntries = W.readUninstallEntries(releases.run);
137
+ for (const u of windowsEntries.unread) add({ id: `apps.registry.${u.hive.split('\\')[0].toLowerCase()}${u.hive.includes('WOW6432Node') ? '-wow64' : ''}`, area: 'apps', state: 'warn', detail: `could not read ${u.hive} (${u.why}); an app installed for every user may show as not installed` });
138
+ }
139
+ const found = W.findWindowsApp(pr, { entries: windowsEntries.entries, home });
140
+ if (!found) {
141
+ add({ id: `app.${pr.id}`, area: 'apps', product: pr.id, state: 'info', detail: `${label}: not installed`, fix: `download it from ${pr.page}` });
142
+ } else {
143
+ let latest = null, latestErr = null;
144
+ if (online) { try { latest = await releases.latestRelease(pr, { cacheFile }); } catch (error) { latestErr = error.message; } }
145
+ const outdated = latest && found.version && newer(latest.version, found.version);
146
+ add({
147
+ id: `app.${pr.id}`, area: 'apps', product: pr.id, state: outdated || (!latest && online) ? 'warn' : 'ok',
148
+ detail: `${label} ${found.version || '(version unknown)'}${found.at ? ` in ${found.at}` : ''}${latest ? (outdated ? `; ${latest.version} is out` : found.version ? ' (latest)' : `; the latest is ${latest.version}`) : online ? `; latest unknown (${latestErr})` : '; latest not checked (--offline)'}`,
149
+ fix: outdated ? `install ${latest.version} from ${pr.page} (or accept the update ${pr.name} offers)` : null,
150
+ });
151
+ }
152
+ } else if (platform !== 'darwin') {
153
+ add({ id: `app.${pr.id}`, area: 'apps', product: pr.id, state: 'info', detail: `${label}: app checks run on macOS and Windows; on this system download it from ${pr.page}` });
132
154
  } else if (!installed) {
133
155
  add({ id: `app.${pr.id}`, area: 'apps', product: pr.id, state: 'info', detail: `${label}: not installed`, fix: `${NPX} setup --with ${pr.id}` });
134
156
  } else {
package/lib/launcher.js CHANGED
@@ -65,13 +65,42 @@ function readState(file, { tolerant = false } = {}) {
65
65
  }
66
66
  }
67
67
 
68
- /** Replace a symlink atomically (create beside it, rename over it). */
69
- function symlinkAtomic(target, at) {
68
+ /**
69
+ * Replace a symlink atomically (create beside it, rename over it). Windows refuses any rename over
70
+ * a directory link (EPERM, PC-85), so there the old link is removed first: unlink takes the link
71
+ * itself, never what it points at, and the old link is put back if the rename fails again. The
72
+ * temporary link never outlives the call, and the ones an earlier failed update left are removed.
73
+ * On Windows the swap is therefore not atomic: between the unlink and the rename `at` is absent,
74
+ * and a run killed there leaves no `current` until the next update (status reports it missing).
75
+ */
76
+ function symlinkAtomic(target, at, { platform = process.platform, rename = fs.renameSync, unlink = fs.unlinkSync } = {}) {
70
77
  fs.mkdirSync(path.dirname(at), { recursive: true });
71
78
  const tmp = `${at}.${process.pid}.lnk`;
72
- try { fs.unlinkSync(tmp); } catch (_) { /* none */ }
79
+ const dropTmp = () => { try { fs.unlinkSync(tmp); } catch (_) { /* none */ } };
80
+ dropTmp();
73
81
  fs.symlinkSync(target, tmp);
74
- fs.renameSync(tmp, at);
82
+ try {
83
+ rename(tmp, at);
84
+ } catch (error) {
85
+ const old = lstat(at);
86
+ if (platform !== 'win32' || !['EPERM', 'EACCES', 'EEXIST'].includes(error.code) || !old || !old.isSymbolicLink()) { dropTmp(); throw error; }
87
+ const previous = linkTarget(at);
88
+ try { unlink(at); } catch (unlinkError) { dropTmp(); throw unlinkError; }
89
+ try { rename(tmp, at); } catch (again) {
90
+ dropTmp();
91
+ try { fs.symlinkSync(previous, at); } catch (back) { throw new Error(`${again.message}; and putting back ${at} → ${previous} failed (${back.message})`); }
92
+ throw again;
93
+ }
94
+ }
95
+ if (platform !== 'win32') return; // only a refused rename on Windows ever left one (PC-85)
96
+ const base = path.basename(at);
97
+ let siblings = [];
98
+ try { siblings = fs.readdirSync(path.dirname(at)); } catch (_) { /* nothing to sweep */ }
99
+ for (const name of siblings) {
100
+ if (!name.startsWith(`${base}.`) || !/^\.\d+\.lnk$/.test(name.slice(base.length))) continue;
101
+ const left = path.join(path.dirname(at), name);
102
+ if (lstat(left)?.isSymbolicLink()) { try { fs.unlinkSync(left); } catch (_) { /* the next update sweeps it */ } }
103
+ }
75
104
  }
76
105
 
77
106
  // --- one update at a time (lifecycle contract LC-03) ---------------------------------------
@@ -854,5 +883,5 @@ module.exports = {
854
883
  restore: (opts = {}) => withUpdateLock(opts.home, () => restore(opts)),
855
884
  uninstall: (opts = {}) => withUpdateLock(opts.home, () => uninstall(opts)),
856
885
  status, setConfig, paths, CHANNELS, MARKETPLACE, payloadManifest, marketplaceAddArg,
857
- acquireUpdateLock, UpdateRunning, staleReleases, claudeRunner, bundledMcpServers, mcpFileTargets, readSettings, tomlTableRange, tomlTableCommand, tomlTableArgs, tomlMentions,
886
+ acquireUpdateLock, UpdateRunning, staleReleases, symlinkAtomic, claudeRunner, bundledMcpServers, mcpFileTargets, readSettings, tomlTableRange, tomlTableCommand, tomlTableArgs, tomlMentions,
858
887
  };
package/lib/releases.js CHANGED
@@ -16,9 +16,10 @@ const catalog = require('./catalog');
16
16
  const API = () => (process.env.PASSIONCODE_GITHUB_API || 'https://api.github.com').replace(/\/$/, '');
17
17
  const CACHE_TTL_MS = 30 * 60 * 1000;
18
18
 
19
- function run(cmd, args, { timeout = 60000, input } = {}) {
20
- const r = spawnSync(cmd, args, { encoding: 'utf8', timeout, input });
19
+ function run(cmd, args, { timeout = 60000, input, maxBuffer = 1024 * 1024 } = {}) {
20
+ const r = spawnSync(cmd, args, { encoding: 'utf8', timeout, input, maxBuffer });
21
21
  if (r.error && r.error.code === 'ENOENT') return { code: 127, out: '', err: `${cmd} not found` };
22
+ if (r.error && r.error.code === 'ENOBUFS') return { code: 1, out: '', err: `${cmd} wrote more than ${maxBuffer} bytes` };
22
23
  if (r.error && r.error.code === 'ETIMEDOUT') return { code: 124, out: r.stdout || '', err: `${cmd} timed out after ${Math.round(timeout / 1000)} s` };
23
24
  return { code: r.status === null ? 1 : r.status, out: r.stdout || '', err: r.stderr || '' };
24
25
  }
package/lib/setup.js CHANGED
@@ -76,7 +76,8 @@ async function setup({
76
76
  const claude = L.claudeRunner();
77
77
  const before = D.sortChecks(await D.collect({ home, platform, claude, online }));
78
78
  const fresh = !before.some((c) => c.id === 'skills.installed' && c.state !== 'fail')
79
- && !catalog.PRODUCTS.some((pr) => platform === 'darwin' && fs.existsSync(catalog.appPath(pr)));
79
+ // An app doctor found (ok or warn: on Windows from the installer's records, PC-86) makes it existing.
80
+ && !before.some((c) => /^app\.[^.]+$/.test(c.id) && (c.state === 'ok' || c.state === 'warn'));
80
81
  const items = plan(before, { platform, withIds, withoutIds, noApps, repairOnly });
81
82
  const result = {
82
83
  version: D.PKG, platform, mode: fresh ? 'new' : 'existing', at: new Date().toISOString(), plan: items,
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+ /**
3
+ * Where a PassionCode.ai app is installed on Windows and which version it is, read without
4
+ * starting it (PC-86): the uninstall entry its installer wrote (electron-builder and Tauri both
5
+ * write DisplayName and DisplayVersion; Tauri also InstallLocation), else the folder a per-user
6
+ * installer uses (%LOCALAPPDATA%\Programs\<name> for electron-builder, %LOCALAPPDATA%\<name> for
7
+ * Tauri) or a per-machine one under Program Files, with the program in it.
8
+ */
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const HIVES = [
13
+ 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
14
+ 'HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
15
+ 'HKLM\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
16
+ ];
17
+
18
+ /** `reg query <hive> /s` output → [{ key, DisplayName, DisplayVersion, InstallLocation, UninstallString }]. */
19
+ function parseUninstall(text) {
20
+ const entries = [];
21
+ let entry = null;
22
+ for (const line of String(text || '').split(/\r?\n/)) {
23
+ if (/^HKEY_/.test(line)) { entry = { key: line.trim() }; entries.push(entry); continue; }
24
+ const m = /^\s+(\S+)\s+REG_(?:SZ|EXPAND_SZ)\s+(.*)$/.exec(line);
25
+ if (m && entry) entry[m[1]] = m[2].trim();
26
+ }
27
+ return entries.filter((e) => e.DisplayName);
28
+ }
29
+
30
+ /**
31
+ * Every uninstall entry on this machine, and the hives `reg` could not read (a missing hive is
32
+ * normal: HKLM's WOW6432Node exists only on 64-bit Windows), so a reader can say an app it did not
33
+ * find may still be there.
34
+ */
35
+ function readUninstallEntries(run) {
36
+ const entries = [], unread = [];
37
+ for (const hive of HIVES) {
38
+ const r = run('reg', ['query', hive, '/s'], { timeout: 15000, maxBuffer: 32 * 1024 * 1024 });
39
+ if (r.code === 0) entries.push(...parseUninstall(r.out));
40
+ else if (!/unable to find/i.test(`${r.err || ''}${r.out || ''}`)) unread.push({ hive, why: (r.err || r.out || `exit ${r.code}`).trim().split(/\r?\n/)[0] });
41
+ }
42
+ return { entries, unread };
43
+ }
44
+
45
+ /** electron-builder names its entry "<name> <version>", Tauri "<name>"; "Fabric" never matches "Fabric Inbox". */
46
+ function names(entry, name) {
47
+ return entry.DisplayName === name || (!!entry.DisplayVersion && entry.DisplayName === `${name} ${entry.DisplayVersion}`);
48
+ }
49
+
50
+ /** The install folder an entry names, or null: an MSI entry's `MsiExec.exe /X{...}` names none. */
51
+ function folderOf(entry) {
52
+ if (entry.InstallLocation) return entry.InstallLocation.replace(/^"|"$/g, '').replace(/\\$/, '');
53
+ const m = /^"([^"]+)"|^(\S+\.exe)/i.exec(entry.UninstallString || '');
54
+ const exe = m && (m[1] || m[2]);
55
+ if (!exe || !exe.includes('\\') || /(^|\\)msiexec\.exe$/i.test(exe)) return null;
56
+ return exe.slice(0, exe.lastIndexOf('\\'));
57
+ }
58
+
59
+ function folders(name, { env, home }) {
60
+ const local = env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
61
+ return [path.join(local, 'Programs', name), path.join(local, name), env.ProgramFiles && path.join(env.ProgramFiles, name)].filter(Boolean);
62
+ }
63
+
64
+ function hasProgram(dir) { try { return fs.readdirSync(dir).some((n) => /\.exe$/i.test(n)); } catch (_) { return false; } }
65
+
66
+ /** { at, version, source: 'registry' | 'folder' } or null when the app is not installed. */
67
+ function findWindowsApp(product, { entries, env = process.env, home }) {
68
+ const dir = folders(product.name, { env, home }).find(hasProgram);
69
+ const hit = entries.find((e) => names(e, product.name));
70
+ if (hit) return { at: folderOf(hit) || dir || null, version: hit.DisplayVersion || null, source: 'registry' };
71
+ return dir ? { at: dir, version: null, source: 'folder' } : null;
72
+ }
73
+
74
+ module.exports = { parseUninstall, readUninstallEntries, findWindowsApp, HIVES };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@passioncode-ai/passioncode",
3
- "version": "0.2.45",
3
+ "version": "0.2.47",
4
4
  "description": "Set up PassionCode.ai on a computer with one command — the apps from their verified releases, their MCP servers for every coding agent and the PassionCode.ai skills — and check and repair it with the same command or `doctor`.",
5
5
  "bin": {
6
6
  "passioncode": "bin/passioncode.js"
@@ -11,7 +11,7 @@
11
11
  "name": "fabric-agent-adapter",
12
12
  "displayName": "Fabric Agent Adapter",
13
13
  "source": "./plugins/fabric-agent-adapter",
14
- "version": "0.55.0",
14
+ "version": "0.56.0",
15
15
  "description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
16
16
  "author": {
17
17
  "name": "PassionCode.ai",
@@ -35,7 +35,7 @@
35
35
  "name": "passioncode",
36
36
  "displayName": "PassionCode.ai",
37
37
  "source": "./plugins/passioncode",
38
- "version": "0.2.45",
38
+ "version": "0.2.47",
39
39
  "description": "Set up and check PassionCode.ai on a computer with one command (setting-up-passioncode), the organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
40
40
  "author": {
41
41
  "name": "PassionCode.ai",
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "family": "passioncode",
3
- "version": "0.2.45",
3
+ "version": "0.2.47",
4
4
  "release": true,
5
5
  "members": [
6
6
  {
7
7
  "name": "fabric-agent-adapter",
8
8
  "displayName": "Fabric Agent Adapter",
9
9
  "repo": "passioncode-ai/fabric-agent-adapter",
10
- "ref": "v0.55.0",
11
- "commit": "f3f2d14e381e3526600915a63eff25a912c7875d",
12
- "version": "0.55.0",
10
+ "ref": "v0.56.0",
11
+ "commit": "250d5d7a18980bacdfc7ba3bb449794995d4c07d",
12
+ "version": "0.56.0",
13
13
  "via": "clone of https://***@github.com/passioncode-ai/fabric-agent-adapter.git",
14
14
  "skills": [
15
15
  "adapting-projects-to-fabric",
@@ -23,7 +23,7 @@
23
23
  "legacyMarketplaces": [
24
24
  "fabric-agent-adapter"
25
25
  ],
26
- "contentHash": "sha256:74f7d28f584cd89d1b8d541534b291bdbad058df62c887c9e1b71b8fa25d0cd1",
26
+ "contentHash": "sha256:a27c7b99d9b29039b28aea11680bf1acce43a6c7792b8e80b16310559d424856",
27
27
  "description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
28
28
  "author": {
29
29
  "name": "PassionCode.ai",
@@ -62,9 +62,9 @@
62
62
  "name": "passioncode",
63
63
  "displayName": "PassionCode.ai",
64
64
  "repo": "passioncode-ai/passioncode",
65
- "ref": "v0.2.45",
65
+ "ref": "v0.2.47",
66
66
  "commit": null,
67
- "version": "0.2.45",
67
+ "version": "0.2.47",
68
68
  "via": "this repository",
69
69
  "skills": [
70
70
  "setting-up-passioncode",
@@ -72,7 +72,7 @@
72
72
  ],
73
73
  "legacyPluginIds": [],
74
74
  "legacyMarketplaces": [],
75
- "contentHash": "sha256:71445c5a7cad160da4d789b8a9cef7db8c20c0b8e9b65b91cc6d33022d047f90",
75
+ "contentHash": "sha256:4fbc28ab1c059e892eb3998c42946149e777a76d54eca40842bfe66927342ccb",
76
76
  "description": "Set up and check PassionCode.ai on a computer with one command (setting-up-passioncode), the organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
77
77
  "author": {
78
78
  "name": "PassionCode.ai",
@@ -3,7 +3,7 @@
3
3
  "name": "fabric-agent-adapter",
4
4
  "displayName": "Fabric Agent Adapter",
5
5
  "description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
6
- "version": "0.55.0",
6
+ "version": "0.56.0",
7
7
  "author": {
8
8
  "name": "PassionCode.ai",
9
9
  "url": "https://passioncode.ai/"
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "Two silent-unless-relevant hook sets. The dashboard-kit notice: one line at session start or /clear in a repository that owns a Fabric service whose dashboard has no kit or an older one (fabric-dashboard/0.1, DEC-0036), phrased as an upgrade the agent may make itself. The Fabric session hooks (working-with-fabric, COM-04, FAA-12a): at session start or /clear, the preamble when Claude Code has the MCP server `fabric` and Fabric runs (fabric_whoami, then the project board through com.status and com.list, claim and release); when Fabric is down, one line, only in a project that uses Fabric (a Fabric contract file, a project-bound registration or a service descriptor naming the repository); nothing on compact or resume; each turn's facts (HEAD, files changed, lines added and removed, never content), the board's unread count, and, once the running Fabric's fabric_session_observe takes them (never on Windows), the other repositories the turn touched (the last 512 KB of the session transcript, message text and tool output included (a secret the session displayed can be there; read in memory only), is then read and only the current turn's tool names and path arguments are used; host/owner/name leave the machine, never a local path or text) and the session's end through Fabric, only when the running Fabric advertises session-hooks/1; at each prompt one line only when the unread count rose, then each nudge a turn's answer carried, once (watch-mail prints those of turns it delivered). All silent in a session Fabric launched. Every hook reads no Fabric token (a credential embedded in an origin URL is read by git config and dropped in memory, never spooled or sent), prints at most one block, always exits 0, and hands any Fabric call to a detached worker with a 10 s deadline.",
2
+ "description": "Two silent-unless-relevant hook sets. The dashboard-kit notice: one line at session start or /clear in a repository that owns a Fabric service whose dashboard has no kit or an older one (fabric-dashboard/0.1, DEC-0036), phrased as an upgrade the agent may make itself. The Fabric session hooks (working-with-fabric, COM-04, FAA-12a): at session start or /clear, the preamble when Claude Code has the MCP server `fabric` and Fabric runs (fabric_whoami, then the project board through com.status and com.list, claim and release); when Fabric is down, one line, only in a project that uses Fabric (a Fabric contract file, a project-bound registration or a service descriptor naming the repository); nothing on compact or resume; each turn's facts (HEAD, files changed, lines added and removed, never content), the board's unread count, and, once the running Fabric's fabric_session_observe takes them (unless FABRIC_SESSION_NO_REPOSITORIES=1), the other repositories the turn touched (the last 512 KB of the session transcript, message text and tool output included (a secret the session displayed can be there; read in memory only), is then read and only the current turn's tool names and path arguments are used; host/owner/name leave the machine, never a local path or text) and the session's end through Fabric, only when the running Fabric advertises session-hooks/1; at each prompt one line only when the unread count rose, then each nudge a turn's answer carried, once (watch-mail prints those of turns it delivered). All silent in a session Fabric launched. Every hook reads no Fabric token (a credential embedded in an origin URL is read by git config and dropped in memory, never spooled or sent), prints at most one block, always exits 0 once python3 starts (on Windows it must name Python in Git Bash, FAA-183), and hands any Fabric call to a detached worker with a 10 s deadline.",
3
3
  "hooks": {
4
4
  "SessionStart": [
5
5
  {
@@ -5,7 +5,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
5
5
  compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode.
6
6
  metadata:
7
7
  author: PassionCode.ai
8
- version: "0.55.0"
8
+ version: "0.56.0"
9
9
  contract-version: "0.1.0"
10
10
  contract-commit: "00e050ba7429c0c60261cec871779e0fd6aeeb28"
11
11
  ---
@@ -47,7 +47,11 @@ ENV_MAX = 64
47
47
  # Whole-value patterns end in \Z, never `$` (R1: `$` also matches before a trailing newline).
48
48
  _ID = re.compile(r"^[a-z][a-z0-9-]{1,62}\Z")
49
49
  _ENV_NAME = re.compile(r"^[A-Z_][A-Z0-9_]*\Z")
50
- _URI_PASSWORD = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*://[^/?#@]*:[^/?#@]*@")
50
+ # A special scheme (http, https, ws, wss, ftp) reads a user:password@ after any run of slashes, none included, as
51
+ # `new URL` does: https:bot:pw@host carries the password too (0.56.0 seam r7).
52
+ _URI_PASSWORD = re.compile(r"^(?:(?:[Hh][Tt][Tt][Pp][Ss]?|[Ww][Ss][Ss]?|[Ff][Tt][Pp]):[/\\]*[^/\\?#]*:[^/\\?#]*@"
53
+ # any other scheme: a backslash belongs to the user information (seam r8)
54
+ r"|(?![Hh][Tt][Tt][Pp][Ss]?:|[Ww][Ss][Ss]?:|[Ff][Tt][Pp]:)[A-Za-z][A-Za-z0-9+.-]*://[^/?#]*:[^/?#]*@)")
51
55
  _SECRET_REF = re.compile(r"^secret-ref:([A-Za-z0-9][A-Za-z0-9._/:@-]{0,255})\Z")
52
56
  _LOCAL_PATH = re.compile(LOCAL_PATH_PATTERN)
53
57
  _URL = re.compile(URL_PATTERN)
@@ -2,12 +2,15 @@
2
2
  """The contract's one credential detector (Fabric Agent Contract DEC-0044, `src/secret-shape.ts` at the pin): a name
3
3
  that can only carry a credential, a value shaped like one, a launch argument that carries one, and a URL that does
4
4
  (LC-21). A port held to the contract's own answers by test/fixtures/contract/profile-vectors.json. Standard library
5
- only. The dashboard kit's fabric-button.js carries its own copy of the name and value checks (FL-4)."""
5
+ only. The dashboard kit's fabric-button.js carries its own copy of the name and value checks (FL-4).
6
+
7
+ It reads as the contract's ECMAScript does (FAA-136): ECMAScript's whitespace (Python's `\\s` also takes U+0085 and
8
+ U+001C to U+001F, and not U+FEFF), `.` stopping at every line terminator, ASCII case folding, an ASCII word boundary."""
6
9
 
7
10
  from __future__ import annotations
8
11
 
9
12
  import re
10
- from typing import Dict, List
13
+ from typing import Dict, List, Tuple
11
14
  import urllib.parse
12
15
 
13
16
  CREDENTIAL_WORDS = (
@@ -22,20 +25,28 @@ CREDENTIAL_PAIRS: Dict[str, tuple] = {
22
25
  "session": ("token", "id"),
23
26
  "code": ("login", "auth", "access", "session"),
24
27
  }
28
+ #: ECMAScript's \s, as a character class body.
29
+ JS_SPACE = "\t\n\x0b\x0c\r \xa0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
30
+ _NOT_LINE_END = "[^\n\r\u2028\u2029]" # ECMAScript's `.` without the s flag
25
31
  JWT_SHAPE = re.compile(r"eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*")
26
- SECRET_VALUE = re.compile(r"(^bearer\s.+|sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,})",
27
- re.IGNORECASE | re.DOTALL)
32
+ SECRET_VALUE = re.compile(r"(^bearer[%s]%s+|sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}"
33
+ r"|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,})" % (JS_SPACE, _NOT_LINE_END), re.IGNORECASE | re.ASCII)
28
34
  _LONG_RUN = re.compile(r"[A-Za-z0-9+/=_-]{40,}")
29
- _BEARER_INSIDE = re.compile(r"\bbearer\s+(?=[A-Za-z0-9._~+/-]*\d)[A-Za-z0-9._~+/-]{12,}=*", re.IGNORECASE)
30
- _PATH_LIKE = re.compile(r"^(?:[/~.]|[A-Za-z]:[\\/]|[a-z][a-z0-9+.-]*://)", re.IGNORECASE)
35
+ _BEARER_INSIDE = re.compile(r"(?<![A-Za-z0-9_])bearer[%s]+(?=[A-Za-z0-9._~+/-]*[0-9])[A-Za-z0-9._~+/-]{12,}=*" % JS_SPACE,
36
+ re.IGNORECASE | re.ASCII)
37
+ _PATH_LIKE = re.compile(r"^(?:[/~.]|[A-Za-z]:[\\/]|[a-z][a-z0-9+.-]*://)", re.IGNORECASE | re.ASCII)
38
+ _WORD_SPLIT = re.compile("[%s._-]+" % JS_SPACE)
39
+ _ARGUMENT_SPLIT = re.compile("[%s=]" % JS_SPACE)
31
40
  _SCHEME = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:")
41
+ #: WHATWG's special schemes that carry an authority with user information (file has none).
42
+ _SPECIAL = ("ftp", "http", "https", "ws", "wss")
32
43
 
33
44
 
34
45
  def name_words(name: str) -> List[str]:
35
46
  """A name's words: split at `.`, `_`, `-` and camelCase humps, lowercased (`APIKey` is `api key`)."""
36
47
  spaced = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", name)
37
48
  spaced = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1 \2", spaced)
38
- return [w.lower() for w in re.split(r"[\s._-]+", spaced) if w]
49
+ return [w.lower() for w in _WORD_SPLIT.split(spaced) if w]
39
50
 
40
51
 
41
52
  def _with_singulars(words: List[str]) -> List[str]:
@@ -73,7 +84,7 @@ def secret_argument(value: str) -> bool:
73
84
  so a folder path holding a session id is not taken for a key."""
74
85
  if JWT_SHAPE.search(value) or SECRET_VALUE.search(value) or _BEARER_INSIDE.search(value):
75
86
  return True
76
- for piece in re.split(r"[\s=]", value):
87
+ for piece in _ARGUMENT_SPLIT.split(value):
77
88
  if _path_like(piece):
78
89
  if any(_long_secret(part) for part in re.split(r"[\\/]", piece)):
79
90
  return True
@@ -93,7 +104,8 @@ def _decode_keeping_plus(raw: str) -> str:
93
104
 
94
105
 
95
106
  def _form_name(text: str) -> str:
96
- pairs = urllib.parse.parse_qsl(text, keep_blank_values=True)
107
+ # URLSearchParams drops one leading `?` from the text it is given (0.56.0 unit r7).
108
+ pairs = urllib.parse.parse_qsl(text[1:] if text.startswith("?") else text, keep_blank_values=True)
97
109
  return pairs[0][0] if pairs else ""
98
110
 
99
111
 
@@ -109,23 +121,66 @@ def raw_values(query: str) -> Dict[str, List[str]]:
109
121
  return values
110
122
 
111
123
 
124
+ #: The C0 controls and space, U+0000 to U+0020: what WHATWG URL parsing trims from a URL's ends.
125
+ _C0_OR_SPACE = "".join(map(chr, range(0x21)))
126
+
127
+
128
+ _LONE_SURROGATE = re.compile("[\ud800-\udfff]")
129
+
130
+
131
+ def _percent_encode(text: str, also: str) -> str:
132
+ """WHATWG's percent-encoding of a query or a fragment: every C0 control, space, `"`, `<`, `>`, every code point
133
+ above U+007E and the characters in `also`, as UTF-8 %XX (a lone surrogate as U+FFFD, as a USVString reads it).
134
+ `new URL` hands the contract's secretUrl the text in this form, so a malformed escape left in it is read the same
135
+ way by both ports (0.56.0 unit r8)."""
136
+ out = []
137
+ for char in _LONE_SURROGATE.sub("\ufffd", text):
138
+ if ord(char) <= 0x20 or ord(char) > 0x7E or char in '"<>' or char in also:
139
+ out.append("".join("%%%02X" % byte for byte in char.encode("utf-8")))
140
+ else:
141
+ out.append(char)
142
+ return "".join(out)
143
+
144
+
145
+ def _url_parts(value: str) -> Tuple[str, str, str]:
146
+ """The password, query and fragment of an absolute URL as WHATWG URL parsing splits them, read by hand: urlsplit
147
+ refuses a `[` or `]` outside an IPv6 host (`https://u:p[w@h/`), where `new URL` reads the password (0.56.0 unit
148
+ r7). The fragment starts at the first `#`, the query at the first `?` before it. A special scheme's authority starts
149
+ after every slash or backslash that follows `scheme:` (the special authority ignore slashes state) and ends at the
150
+ next slash, backslash, `?` or `#`, so `https:u:p@h` and `https:\\u:p@h` carry the password p; any other scheme
151
+ has an authority only after `//`, and `file` has no user information. The user information ends at the authority's
152
+ last `@`, and the password follows its first `:`. A host `new URL` would refuse is not checked: such a URL is read
153
+ for its password and parameters all the same, the safe direction for a detector."""
154
+ scheme, _, rest = value.partition(":")
155
+ rest, _, fragment = rest.partition("#")
156
+ rest, _, query = rest.partition("?")
157
+ lowered = scheme.lower()
158
+ if lowered in _SPECIAL:
159
+ authority = rest.replace("\\", "/").lstrip("/").partition("/")[0]
160
+ elif lowered != "file" and rest.startswith("//"):
161
+ authority = rest[2:].partition("/")[0]
162
+ else:
163
+ authority = ""
164
+ userinfo, at, _ = authority.rpartition("@")
165
+ return ((userinfo.partition(":")[2] if at else ""), _percent_encode(query, "'" if lowered in _SPECIAL or lowered == "file" else ""), # file is special too
166
+ _percent_encode(fragment, "`"))
167
+
168
+
112
169
  def secret_url(value: str) -> bool:
113
170
  """Whether a URL an MCP entry or a launch names carries a credential (LC-21): anything secret_argument finds, a
114
171
  password in its user information, or a query or fragment parameter whose name can only carry a credential and has
115
172
  a value, or whose value is shaped like one. A string that is not an absolute URL is checked as an argument only."""
116
173
  if secret_argument(value):
117
174
  return True
118
- if not _SCHEME.match(value.strip()):
175
+ # What WHATWG URL parsing does first: trim C0 controls and space from the ends (never other Unicode spaces, which it
176
+ # refuses), then remove every tab, CR and LF.
177
+ value = re.sub(r"[\t\n\r]", "", value.strip(_C0_OR_SPACE))
178
+ if not _SCHEME.match(value):
119
179
  return False # not an absolute URL (WHATWG URL parsing refuses it)
120
- try:
121
- parts = urllib.parse.urlsplit(value.strip())
122
- except ValueError:
123
- return False
124
- if not parts.scheme:
125
- return False
126
- if parts.password:
180
+ password, query, fragment = _url_parts(value)
181
+ if password:
127
182
  return True
128
- for part in (parts.query, parts.fragment):
183
+ for part in (query, fragment):
129
184
  for name, raws in raw_values(part).items():
130
185
  if any((raw != "" and credential_name(name)) or secret_value(raw) for raw in raws):
131
186
  return True
@@ -14,7 +14,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
14
14
  compatibility: Python 3.9+ or Node.js 20+ for the kits (Node.js 22+ for a Linux service's instance lock); the probe needs Python 3.9+. launchd steps are macOS-only; Linux services install a systemd user unit and Windows ones a per-user Scheduled Task with the kits' writers. Dashboard handoff optionally uses Fabric Dashboards MCP link/host_status/open; without it, report unresolved host capability. The contract checkout is optional.
15
15
  metadata:
16
16
  author: PassionCode.ai
17
- version: "0.55.0"
17
+ version: "0.56.0"
18
18
  contract-version: "0.1.0"
19
19
  extension: "fabric-service/0.1"
20
20
  extension-commit: "00e050ba7429c0c60261cec871779e0fd6aeeb28"
@@ -104,8 +104,8 @@ responder in its own process: [`scripts/fabric_hub.py`](scripts/fabric_hub.py) f
104
104
  every step and stops on `fenced`, `lease_expired` or `authority_changed` (Node: `fabric-hub.mjs`, `fabric-board.mjs`).
105
105
  The request body is data behind the service's own input gate. From 0.53.0 a slot the operator moved to another
106
106
  session stays moved until `take_back` (`takeBack`): give it an operator action with its tool and gate `relayed`, or it
107
- stays moved for good (until Fabric's hub forwards decisions, CT-125, the operator reaches it through the service's page
108
- or its elicitation). Read [the board reference](references/board.md).
107
+ stays moved for good (the operator reaches it through an agent that relays their decision in `_meta`, the service's
108
+ page or its elicitation; Claude Code and Codex wait for CT-125's forwarding). Read [the board reference](references/board.md).
109
109
 
110
110
  ## Step 2: the non-negotiables
111
111
 
@@ -260,8 +260,9 @@ or `human` with its `humanStep`) and declared with `build_well_known(actions=ser
260
260
  `extensions/actions/0.1`. A `relayed` action called by the operator's own agent with their decision in
261
261
  `_meta.decision` is the operator's act: never answer it "operator session required" or send them to the page.
262
262
  A service adopts the relayed gate once Fabric's hub forwards decisions (CT-125), as the contract says; until then its tools
263
- answer as today, except the board responder's take-back action, declared `relayed` now and reached through the
264
- page or elicitation until CT-125 ([board.md](references/board.md)). Read the interop reference's *Actions* section for when to adopt, the gate, its hooks and its
263
+ answer as today, except the board responder's take-back action, declared `relayed` now: an agent whose client sets
264
+ `_meta` calls it with the operator's decision; Claude Code and Codex reach it through the page until CT-125
265
+ ([board.md](references/board.md); the reference service's `board.take-back`). Read the interop reference's *Actions* section for when to adopt, the gate, its hooks and its
265
266
  refusals.
266
267
 
267
268
  **Say what the agent uses** (`fabric-connections/0.1`, DEC-0096) so Fabric Dashboards draws its chain: the other