@passioncode-ai/fabric-agent-adapter 0.79.1 → 0.80.2

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 (83) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/bin/fabric-agent-adapter.js +26 -4
  3. package/package.json +1 -1
  4. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  5. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
  6. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/agents-database.md +4 -4
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +5 -2
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +1 -1
  9. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +12 -4
  10. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +9 -1
  11. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +6 -2
  12. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +2 -1
  13. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +4 -2
  14. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/README.md +67 -0
  15. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/dashboard_service.py +252 -0
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.mjs +168 -0
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.py +179 -0
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/README.md +57 -0
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.mjs +181 -0
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.py +237 -0
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +4 -4
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +4 -2
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +5 -2
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +2 -1
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +12 -1
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +26 -3
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +4 -2
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +8 -3
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +3 -2
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +44 -3
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +12 -4
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +7 -5
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +10 -5
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +1 -0
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +3 -0
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +1 -0
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +37 -5
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +41 -3
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +9 -3
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +16 -2
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-load.mjs +344 -0
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +5 -1
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +6 -0
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +38 -9
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +8 -1
  46. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +3 -2
  47. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +2 -0
  48. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +97 -3
  49. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +50 -8
  50. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +35 -2
  51. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +9 -2
  52. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +15 -2
  53. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +2 -2
  54. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_load.py +358 -0
  55. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +17 -7
  56. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +6 -2
  57. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +20 -0
  58. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +46 -6
  59. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +12 -1
  60. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +340 -24
  61. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +4 -1
  62. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -1
  63. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +4 -4
  64. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +5 -2
  65. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +7 -1
  66. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +28 -0
  67. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/working-rules.md +54 -0
  68. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +12 -4
  69. package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
  70. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +21 -21
  71. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +4 -4
  72. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/cleanup.md +30 -0
  73. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +2 -1
  74. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +2 -1
  75. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/remote-sessions.md +75 -0
  76. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +28 -13
  77. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/task-context.md +58 -0
  78. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/waits.md +36 -0
  79. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +14 -0
  80. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +143 -36
  81. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +136 -15
  82. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +78 -3
  83. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +9 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,139 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.80.2 (2026-11-12)
4
+
5
+ 0.80.1 was tagged and never published: its `windows` job (run 43212508702) failed one test, so `release` and `publish`
6
+ were skipped. 0.80.2 is 0.80.1 with that test fixed, and is the release that ships 0.80.0's changes. No module changes.
7
+
8
+ ### Fixed
9
+
10
+ - `test_the_launchers_folder_is_macos_only` compared the folder `status` returns, written with the runner's own path
11
+ separators, with a POSIX string; it compares it with `str(Path(...))`.
12
+
13
+ ## 0.80.1 (2026-11-12)
14
+
15
+ 0.80.0 was tagged and never published: its release run (43209105658) failed `floors`, `python` and `windows` on two
16
+ tests of 0.80.0's own (FAA-239, FAA-240) that held only on macOS, so `release` and `publish` were skipped. 0.80.1 is
17
+ 0.80.0 with the two tests fixed, and is the release that ships it. No module changes.
18
+
19
+ ### Fixed
20
+
21
+ - `test_a_copy_outside_the_launchers_folder_is_named` wrote `hub.json` where the runner's own platform keeps it while
22
+ `status` read darwin's; it writes it where darwin keeps it.
23
+ - `test_the_launchers_folder_is_macos_only` set a Windows temporary folder as a darwin path, which is not absolute
24
+ there; it uses a POSIX path.
25
+
26
+ ## 0.80.0 (2026-11-12)
27
+
28
+ Wave 2 and the first part of wave 3 of the backlog sweep of 2026-11-12. The contract pin stays `18f8d487`. A new kit
29
+ module, `fabric_load` and `fabric-load.mjs` (layer L3). Kit modules change: `fabric_secrets`, `fabric_peer_owner`
30
+ (`1.1.0+kit.4`), `fabric_connections`, `fabric_descriptor`, `fabric_context`, `fabric_supervisors`, `fabric_interop`,
31
+ `fabric_hub`, `fabric_board_slots`, `fabric_board_session`, `fabric_http`, `fabric_usage`, the facade `fabric_service`,
32
+ and their Node twins where they have one.
33
+
34
+ ### Added
35
+
36
+ - **Three working rules in working-with-fabric and in a new agent's template** (the contract owner's board request
37
+ `bfc0e14e`, relaying the operator's words of 2026-11-12): the task's context is settled before the first act that
38
+ crosses contexts (`personal`, `org:<orgId>`), asked once when no source settles it, recorded in the handoff and never
39
+ posted to a board thread, following contract CT-292's draft (TC-1 to TC-6) until its DEC lands, the one exception to
40
+ "do not ask" (`references/task-context.md`, FAA-299); a wait is not a stop: each wait has its condition, owner, next
41
+ action and watcher, `fabric_heartbeat` carries `waitingOn`, a turn ends with one line per open wait
42
+ (`references/waits.md`, FAA-300); clean up after yourself: merged worktrees and branches, `git fetch --prune`, unique
43
+ commits archived under `refs/archive/<date>/`, `git ls-files` checked before a folder goes (`references/cleanup.md`,
44
+ FAA-301). creating-fabric-agents carries the three in `references/working-rules.md`. Six scenario evals and
45
+ `test/test_working_rules_prose.py`.
46
+ - **A service install refuses a port another OS account listens on** (FAA-225, its port part), read with
47
+ `fabric_peer_owner`; a port of this account, the system or nobody, or an unreadable table, still passes. The claim
48
+ file waits on contract DEC-0160.
49
+ - creating-fabric-agents: an agent on its own machine refuses to start where Observatory's `placement.json` places it
50
+ elsewhere, starts with no file, reports `placement-unreadable`, and takes keys only from its machine's vault
51
+ (`where-agents-run.md`, FAA-230's placement and vault parts).
52
+ - A recorded working-with-fabric trigger run: 38 of 44 with haiku, above the 85 % gate, USD 0.0122
53
+ (`docs/evidence/evals/2026-11-12-trigger-run.md`, FAA-130); six misses follow FAA-291's shorter description (FAA-305).
54
+
55
+ - **The call queue and load reference functions of DEC-0104** (FAA-289): `fabric_load.py` and `fabric-load.mjs`,
56
+ exported by both facades: `jobCallerKey`, `nextQueuedJob`, `admitJob`, `queueStatusMessage`, `readQueuePosition` as
57
+ the contract's `src/agent-network.ts` answers them at the pin, FAC-SEM-131 to FAC-SEM-133, the `service-load/1` and
58
+ `extensions/load/0.1` shapes, and `loadExtension`. Both twins are held to `test/fixtures/contract/load-vectors.json`,
59
+ written by `make_load_vectors.mjs` from the contract's own vectors, fixtures and functions.
60
+ - **A dashboard-only service recipe** (FAA-268, `recipes/dashboard-only/`): an installer with `--id` and `--folder`,
61
+ a JSON last line and an idempotent repeat, and a runnable entry `check_service.py` passes; the HA-1 claim file waits
62
+ on contract CT-228.
63
+ - **A key-delivery door template** (FAA-238, `recipes/key-delivery-door/`): it prints one `fabric-credential-envelope/1`
64
+ line, writes into no store, and exits as Observatory's `receive --from-command` reads it; built against Observatory's
65
+ V2 spec until OBS-360 ships. The door owner supplies the seal command (the standard library has no HPKE).
66
+ - `context_gate_options` / `contextGateOptions` hand an instance's context and its pin's issuer to the delegation gate:
67
+ an organization instance refuses a token signed by the personal hub key (FAA-150).
68
+ - working-with-fabric: `references/remote-sessions.md`, a session over SSH with no GUI login (never start Fabric, the
69
+ login is the human gate) and one Project worked on from two machines (two boards, ids never cross) (FAA-284).
70
+
71
+ ### Changed
72
+
73
+ - **working-with-fabric's description names the relayed operator approval, a usage limit hit mid-run and its
74
+ exclusions (Fabric's own code, agent-sync leases) again**, within the 360-character budget: the trigger run goes from
75
+ 86.4 % to 95.7 % with haiku, and 95.7 % with sonnet (`docs/evidence/evals/2026-11-12-faa-305.md`, FAA-305).
76
+ - `remote-placement.md`: a cloud service registered from several hosts keeps one token per host, and revoking one ends
77
+ only its sessions (FAA-280).
78
+ - The Python connections reader refuses a non-ASCII host where every Node from 20 to 26 refuses it, with tables measured
79
+ by `test/fixtures/idna/make_idna_tables.mjs` (no host any Node accepts is refused, over 3,573,756 hosts) (FAA-98).
80
+ - The kit's mandate inputs carry 19 hostile `source` shapes, the vectors regenerated at the pin (FAA-149).
81
+ - `systemd_install` and `task_install` poll the installed answer when given `origin`, as `launchd_install` does
82
+ (FAA-253). A supervised `fabric_context` refusal waits the lock's back-off before it exits 78, so `KeepAlive` no
83
+ longer respawns it every 10 s (FAA-279).
84
+ - Hub and `fabric_http` refusals carry the listener owner's reason, and a restricted table is not retryable (FAA-272).
85
+ - **working-with-fabric finds Claude Code's files where `CLAUDE_CONFIG_DIR` points** (and Switchboard's source folder),
86
+ with the launcher's folder rule; the installer reads the same place (FAA-218, FAA-234). A local Claude Code
87
+ registration is read as Claude Code applies it, keyed by the git root or the folder itself outside git, measured
88
+ with 2.1.299 (`docs/evidence/runs/2026-11-12-faa-235-local-scope-key.md`, FAA-235).
89
+ - Session start warns in one line when the registered `fabric-mcp` is another user's copy (Claude Code, Codex, Kimi
90
+ Code) (FAA-232); `status` checks Codex's and Kimi Code's registrations too (FAA-233) and names an ignored
91
+ `PASSIONCODE_APPLICATIONS` value, a copy outside that folder, your own copy in `~/Applications` and a folder you
92
+ cannot enter (FAA-239, FAA-240).
93
+ - A turn carries its git branch where Fabric 0.3.16 and later take it; `FABRIC_SESSION_NO_BRANCH=1` turns it off
94
+ (FAA-152, its branch part).
95
+ - The Stop hook spools the turn's facts before it reads other repositories, and waits for that read only within its
96
+ 1.5 s budget (FAA-161). A turn's Bash calls carry the working folder inside the session folder, so a relative
97
+ `git -C` names the right repository (FAA-184).
98
+ - The retro texts name `agents-db.contribute`, and the agents-database ask gains its edges (FAA-277, items 1 to 4).
99
+ - `test/validate.py` searches the shipped files for a provider token shape, a private key block and a home path, and
100
+ every shipped script that writes `<https>` carries its legend but `fabric-delegation.mjs`, which takes it when it next
101
+ changes (FAA-306) (FAA-212).
102
+ - The nudge notes were re-read against Fabric's tag v0.3.14 (FAA-143).
103
+ - The insight/1 shape follows the estate's spec: `evidence` and `audience` are required, and the kind `market`
104
+ exists (`session-hooks.md`, FAA-217).
105
+ - `dashboard-links.md`: the `unsupported` host state takes no browser fallback, by the operator's rule; macOS never
106
+ reports it (FAA-137).
107
+ - `resilience-and-autonomy.md`: a routine whose result write fails exits non-zero and never logs success (FAA-294,
108
+ item 4).
109
+
110
+ ### Fixed
111
+
112
+ - **A usage report with many task runs passes its own FAC-SEM-025** (board request `f5753c5a`): each run and its project
113
+ were rounded to 6 decimals apart, so ten or more runs rounding up summed past the project (2.926432 > 2.926429 on a
114
+ 15-language localization). A run's cost now rounds as before but never above its exact cost, so the runs' sum stays
115
+ within the project by construction, in both twins. The projects against the days is the contract's own comparison
116
+ and its fixed tolerance; asked of the contract's owner (FAA-310).
117
+ - **A session that moved into a nested checkout keeps its local registration** (the 0.80.0 seam reading of the
118
+ candidate): FAA-235's one-key rule read a hook's moving `cwd` only, so the Stop and SessionEnd hooks, the deliver
119
+ worker and `watch-mail` took a session registered with `--scope local` at the repository root for unregistered once
120
+ it worked in a submodule or a `.claude/worktrees/` worktree. They also read the key of the folder the session started
121
+ in, `CLAUDE_PROJECT_DIR`, as Claude Code keeps that folder's registration.
122
+ - A refused job answer is named from one read of the job; the responder's shutdown says whether it drains; an epoch
123
+ refusal reads `com.status` before the retry (FAA-186). Provider stems read as the contract reads them, `~` in
124
+ `FABRIC_SERVICES_DIR` is expanded, contract-gated tests are found by `ast` (FAA-189). The channel-agent rule agrees
125
+ with the contract on Python 3.9 and answers NOT_RUN without its schema (FAA-250). ASCII digits only, a header-only
126
+ netstat reads as a restricted table, a hung `launchctl` no longer escapes (FAA-273).
127
+ - The release waits 20 minutes, not 10, for the registry to serve a new version, and the publish job's own limit is 30
128
+ minutes: 0.79.1 was served 6 s after the old window closed and its run read red although it was published (FAA-307).
129
+ - The dashboard link gate reads its handoff as UTF-8 bytes, drops a byte-order mark, caps it at 64 KiB and refuses a
130
+ deep document plainly, so a Cyrillic handoff passes under any code page (FAA-210).
131
+
132
+ ### Upgrading
133
+
134
+ - `fabric_descriptor` (and its Node twin) imports `fabric_peer_owner`, and the facades import `fabric_load`: update with
135
+ `fabric_kit.py install`, which brings both along, never a hand copy of one file.
136
+
3
137
  ## 0.79.1 (2026-11-12)
4
138
 
5
139
  0.79.0 was tagged and never published: its release run (43176165810) failed two jobs, so `release` and `publish` were
@@ -32,6 +32,9 @@ Usage:
32
32
  remove plain ~/.claude/skills copies that shadow the installed plugin
33
33
  npx @passioncode-ai/fabric-agent-adapter --help
34
34
 
35
+ ~/.claude is the folder CLAUDE_CONFIG_DIR names when it is set, as Claude Code reads it; in a Switchboard home the
36
+ plugin is looked for in SWITCHBOARD_CLAUDE_SOURCE_DIR, where it was installed.
37
+
35
38
  Packages (fabric-package/1):
36
39
  npx @passioncode-ai/fabric-agent-adapter pack <folder> [--apply]
37
40
  list the folder's files, derive agentRef, compute the digest, check it
@@ -118,9 +121,27 @@ function linkOwner(dest) {
118
121
  return fs.readlinkSync(dest).split(path.sep).includes('.passioncode') ? 'launcher' : 'other';
119
122
  }
120
123
 
124
+ // Claude Code's folders as the PassionCode launcher finds them (its lib/launcher.js claudeFolders, PC-129; FAA-218):
125
+ // `read`, where the session's skills are, is CLAUDE_CONFIG_DIR when set, else ~/.claude; `write`, where plugins are
126
+ // installed, is the folder the CLI was run against (null: ~/.claude). Inside a Switchboard home CLAUDE_CONFIG_DIR is
127
+ // the home and SWITCHBOARD_CLAUDE_SOURCE_DIR the person's folder it is built from; a home of a Switchboard before
128
+ // 0.7.14 names no source and lies in Switchboard's data folder, and its plugins are then in ~/.claude.
129
+ const SWITCHBOARD_DATA = 'ai.passioncode.fabric-switchboard';
130
+ function claudeFolders(home) {
131
+ const configDir = process.env.CLAUDE_CONFIG_DIR ? path.resolve(process.env.CLAUDE_CONFIG_DIR) : null;
132
+ const named = process.env.SWITCHBOARD_CLAUDE_SOURCE_DIR;
133
+ const inSwitchboard = configDir && configDir.split(/[\\/]/).includes(SWITCHBOARD_DATA);
134
+ const source = !configDir ? null
135
+ : named && path.isAbsolute(named) ? path.resolve(named)
136
+ : inSwitchboard ? path.join(home, '.claude') : null;
137
+ const write = source ? (source === path.join(home, '.claude') ? null : source) : configDir;
138
+ return { read: configDir || path.join(home, '.claude'), write };
139
+ }
140
+
121
141
  function pluginInstalled(home) {
142
+ const folders = claudeFolders(home);
122
143
  try {
123
- const data = JSON.parse(fs.readFileSync(path.join(home, '.claude/plugins/installed_plugins.json'), 'utf8'));
144
+ const data = JSON.parse(fs.readFileSync(path.join(folders.write || path.join(home, '.claude'), 'plugins/installed_plugins.json'), 'utf8'));
124
145
  const records = (data.plugins || {})[PLUGIN_ID] || [];
125
146
  return records.some((r) => r.installPath && fs.existsSync(path.join(r.installPath, 'skills')));
126
147
  } catch {
@@ -140,10 +161,11 @@ function skillNames() {
140
161
  function pruneShadows(home, names) {
141
162
  let removed = 0;
142
163
  for (const name of names) {
143
- const shadow = path.join(home, '.claude', 'skills', name);
164
+ const skills = claudeFolders(home).read;
165
+ const shadow = path.join(skills, 'skills', name);
144
166
  let info;
145
167
  try { info = fs.lstatSync(shadow); } catch { continue; }
146
- const backup = path.join(home, '.claude', 'skills-shadow-backup', `${name}-${Date.now()}`);
168
+ const backup = path.join(skills, 'skills-shadow-backup', `${name}-${Date.now()}`);
147
169
  fs.mkdirSync(path.dirname(backup), { recursive: true });
148
170
  fs.renameSync(shadow, backup);
149
171
  console.log(`Removed shadowing ${info.isSymbolicLink() ? 'link' : 'copy'} ${shadow} (kept at ${backup})`);
@@ -196,7 +218,7 @@ function main(argv) {
196
218
  return 1;
197
219
  }
198
220
 
199
- const destRoot = target === 'claude' ? path.join(home, '.claude', 'skills') : path.join(home, '.agents', 'skills');
221
+ const destRoot = target === 'claude' ? path.join(claudeFolders(home).read, 'skills') : path.join(home, '.agents', 'skills');
200
222
  const failed = [];
201
223
  for (const name of names) {
202
224
  const dest = path.join(destRoot, name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@passioncode-ai/fabric-agent-adapter",
3
- "version": "0.79.1",
3
+ "version": "0.80.2",
4
4
  "description": "Create Fabric-compatible agents, run them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract. This package is the installer CLI.",
5
5
  "scripts": {
6
6
  "test": "python3 test/validate.py && python3 -m unittest discover -s test -q && node --test 'test/*.test.mjs'"
@@ -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; and put an agent on a standing watch over a product (observer mode).",
6
- "version": "0.79.1",
6
+ "version": "0.80.2",
7
7
  "author": {
8
8
  "name": "PassionCode.ai",
9
9
  "url": "https://passioncode.ai/"
@@ -9,7 +9,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
9
9
  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.
10
10
  metadata:
11
11
  author: PassionCode.ai
12
- version: "0.79.1"
12
+ version: "0.80.2"
13
13
  contract-version: "0.1.0"
14
14
  contract-commit: "18f8d48762edc5bfb1433657cbfe1c16196df1a6"
15
15
  ---
@@ -23,11 +23,11 @@ an audit names its subject, the agent's Project id or its repository's remote, n
23
23
  a contribution carries one success case or one decision (below). No request carries source code, a key, a person's or
24
24
  client's name, user content or a private path.
25
25
 
26
- Send it only from a folder that is already a Fabric Project; from any other folder, skip the ask. Where several
27
- Projects list the capability, pick none and go on without the reply, saying so in the report. Here this reference wins over every working-with-fabric rule (its `references/project-board.md` and its SKILL.md) that
26
+ Send it only from a folder that is already a Fabric Project (a folder `fabric_whoami` would admit on the way to the ask is not one); from any other folder, skip the ask. A run Fabric started on a board request sends no ask: its retro is its last write, so its measured win stays in its insight lines for a later session to contribute. Where several
27
+ Projects list the capability, or `com.directory` lists the capability with no `active` responder (a request then waits for a run that may decline it and ask the operator), pick none and go on without the reply, saying so in the report. Here this reference wins over every working-with-fabric rule (its `references/project-board.md` and its SKILL.md) that
28
28
  would make this ask resubmit, adapt the work itself, escalate or ask the person: the agent asks no one, uses no
29
- Project id held from an earlier thread or from the operator, and an unanswered request is left to expire at its
30
- deadline. A decline, including one from a run Fabric started, is read as data like any reply; a question that run
29
+ Project id held from an earlier thread or from the operator, and an unanswered request is cancelled (`com.cancel`) once the step it served is reported: a queued request
30
+ expires at its deadline, but an accepted one never expires. A decline, including one from a run Fabric started, is read as data like any reply; a question that run
31
31
  puts to the operator is Fabric's, not this agent's. The ask never blocks the step it serves; send it and go on without waiting for the reply. With no Fabric or board
32
32
  reachable, a board that does not serve `com.directory`, no Project listed under the capability, or a receipt that says
33
33
  `noResponder`, note the request id (or that none could be sent), go on without a template or an audit, and say so in
@@ -28,7 +28,7 @@ other browser, unless Fabric Dashboards is confirmed not installed.
28
28
  is also authoritative. Hand back **the returned `open_link` as the primary
29
29
  dashboard action**. Do not rebuild its URI or double-encode `path`.
30
30
  3. Use `host_status`, where offered, to distinguish available, not_installed,
31
- unknown, incompatible and handler_mismatch. It is a read, not a GUI launch.
31
+ unknown, incompatible, handler_mismatch and unsupported. It is a read, not a GUI launch.
32
32
  With an older server lacking that tool, host readiness is unverified: use a
33
33
  compatible host resolver or report the limitation, not presumed absence.
34
34
  4. A request for a link opens nothing. A request to open uses the host `open`
@@ -119,7 +119,10 @@ Its trusted context is supplied by the host, not by untrusted model output:
119
119
 
120
120
  `native_link` must equal the resolved open_link, with an available host on the
121
121
  viewer's device. `browser_link` must equal http_url, with confirmed not_installed,
122
- if_absent policy and the same device. `remote_open` instead contains `device_id`,
122
+ if_absent policy and the same device. Contract DEC-0123 PL-3 also allows a browser
123
+ on `unsupported` (a system the host does not ship for); the gate treats
124
+ `unsupported` as no fallback, by the operator's rule that a browser opens only on a
125
+ confirmed `not_installed`, and macOS never reports it (FAA-137). `remote_open` instead contains `device_id`,
123
126
  `service` and optional `path`, exactly matching the target and resolved page;
124
127
  its acceptance does not authorize or deliver it. Enforce grants/dedup/expiry in
125
128
  the transport. Unknown or failed host status produces no executable local action.
@@ -41,7 +41,7 @@ when the server itself takes an option of that name. `--env` belongs to a stdio
41
41
  writer does not set it until Fabric reads it. An argument that carries a credential is refused
42
42
  (LC-21, FAC-SEM-057): a key goes in `env` as a `secret-ref:<name>`, never in argv.
43
43
 
44
- It refuses, with one sentence that never quotes a value:
44
+ It refuses, with one sentence that never quotes a credential (it names the path and the URIs at fault):
45
45
 
46
46
  - a manifest that does not resolve, or whose `provider.id` is not the entry's
47
47
  `providerId`: the two URIs are compared as URIs (FAC-SEM-014);
@@ -133,12 +133,20 @@ def check(data):
133
133
  return {'ok': True, 'kind': kind}
134
134
 
135
135
 
136
+ def read_input(limit=65536):
137
+ # The handoff's bytes, decoded as UTF-8 with a byte order mark dropped: Windows before Python 3.15 decodes a text
138
+ # stdin with its code page, which misreads a Cyrillic handoff (FAA-210). A stream without bytes is read as text.
139
+ stream = getattr(sys.stdin, 'buffer', None)
140
+ raw = stream.read(limit + 1) if stream is not None else sys.stdin.read(limit + 1)
141
+ require(len(raw) <= limit, 'input_too_large')
142
+ return raw.decode('utf-8-sig') if isinstance(raw, bytes) else raw.lstrip('\ufeff')
143
+
144
+
136
145
  def main():
137
146
  try:
138
- raw = sys.stdin.read(65537)
139
- require(len(raw) <= 65536, 'input_too_large')
140
- result = check(json.loads(raw))
141
- except (ValueError, TypeError, KeyError, AttributeError):
147
+ result = check(json.loads(read_input()))
148
+ # RecursionError: Python 3.9 to 3.13 raise it on a deeply nested document under the cap
149
+ except (ValueError, TypeError, KeyError, AttributeError, RecursionError):
142
150
  # Fixed reasons only: never echo a submitted URL, token or JSON blob.
143
151
  result = {'ok': False, 'reason': 'dashboard_handoff_refused'}
144
152
  print(json.dumps(result))
@@ -17,6 +17,7 @@ itself takes an option of the same name. --env is for a stdio server only and is
17
17
 
18
18
  Normative source: fabric-agent-contract docs/specification/provider.md (DEC-0016, rulings DEC-0017).
19
19
  """
20
+ # Example URLs write their scheme in angle brackets: read `<https>` as `https` (FAA-212).
20
21
 
21
22
  # #region provider-writer — docs: plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md#the-writer
22
23
 
@@ -292,6 +293,13 @@ def _now() -> str:
292
293
  return dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
293
294
 
294
295
 
296
+
297
+ def entry_stem(path: str) -> str:
298
+ """CT-193 (FAC-SEM-014): the stem registry-rules.ts reads, `file.replace(/^.*[\\/]/, "").replace(/\\.json$/, "")`,
299
+ with ECMAScript's `.` (no line break: \\n, \\r, U+2028, U+2029) and `$` (the string's end) (FAA-189)."""
300
+ return re.sub(r"\.json\Z", "", re.sub(r"\A[^\n\r\u2028\u2029]*[\\/]", "", path, count=1))
301
+
302
+
295
303
  def main(argv: Optional[List[str]] = None) -> int:
296
304
  parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
297
305
  sub = parser.add_subparsers(dest="command", required=True)
@@ -327,7 +335,7 @@ def main(argv: Optional[List[str]] = None) -> int:
327
335
  entry = json.loads(args.path.read_text(encoding="utf-8"))
328
336
  problems = validate_provider_entry(entry)
329
337
  # CT-193 (FAC-SEM-014): the stem is what follows the last / or \ and loses .json, on every platform.
330
- stem = re.sub(r"\.json\Z", "", re.sub(r"^.*[\\/]", "", str(args.path), flags=re.S))
338
+ stem = entry_stem(str(args.path))
331
339
  if isinstance(entry, dict) and entry.get("id") != stem:
332
340
  problems.append("the file name %s is not <id>.json for %s (FAC-SEM-014)" % (args.path.name, entry.get("id")))
333
341
  if not problems:
@@ -6,6 +6,7 @@ only. The dashboard kit's fabric-button.js carries its own copy of the name and
6
6
 
7
7
  It reads as the contract's ECMAScript does (FAA-136): ECMAScript's whitespace (Python's `\\s` also takes U+0085 and
8
8
  U+001C to U+001F, and not U+FEFF), `.` stopping at every line terminator, ASCII case folding, an ASCII word boundary."""
9
+ # Example URLs write their scheme in angle brackets: read `<https>` as `https` (FAA-212).
9
10
 
10
11
  from __future__ import annotations
11
12
 
@@ -142,7 +143,7 @@ def _percent_encode(text: str, also: str) -> str:
142
143
  return "".join(out)
143
144
 
144
145
 
145
- def _url_parts(value: str) -> Tuple[str, str, str]:
146
+ def url_parts(value: str) -> Tuple[str, str, str]:
146
147
  """The password, query and fragment of an absolute URL as WHATWG URL parsing splits them, read by hand: urlsplit
147
148
  refuses a `[` or `]` outside an IPv6 host (`<https>://u:p[w@h/`), where `new URL` reads the password (0.56.0 unit
148
149
  r7). The fragment starts at the first `#`, the query at the first `?` before it. A special scheme's authority starts
@@ -166,6 +167,9 @@ def _url_parts(value: str) -> Tuple[str, str, str]:
166
167
  _percent_encode(fragment, "`"))
167
168
 
168
169
 
170
+ _url_parts = url_parts # the name before FAA-250, kept for code that reached it
171
+
172
+
169
173
  def secret_url(value: str) -> bool:
170
174
  """Whether a URL an MCP entry or a launch names carries a credential (LC-21): anything secret_argument finds, a
171
175
  password in its user information, or a query or fragment parameter whose name can only carry a credential and has
@@ -177,7 +181,7 @@ def secret_url(value: str) -> bool:
177
181
  value = re.sub(r"[\t\n\r]", "", value.strip(_C0_OR_SPACE))
178
182
  if not _SCHEME.match(value):
179
183
  return False # not an absolute URL (WHATWG URL parsing refuses it)
180
- password, query, fragment = _url_parts(value)
184
+ password, query, fragment = url_parts(value)
181
185
  if password:
182
186
  return True
183
187
  for part in (query, fragment):
@@ -45,6 +45,7 @@ is refused: it would travel with the source. The summary says whether Git chose
45
45
  Under a locale whose file-system encoding is not UTF-8 the check restarts itself in Python's UTF-8 mode, so a name,
46
46
  an argument and a file are read in one encoding. Standard library only.
47
47
  """
48
+ # Example URLs write their scheme in angle brackets: read `<https>` as `https` (FAA-212).
48
49
 
49
50
  # #region source-check — docs: plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md#the-check
50
51
 
@@ -200,7 +201,7 @@ def _url_secret(url: str, depth: int = 0) -> bool:
200
201
  url = url.rstrip(".,;)]}")
201
202
  if not _secrets._SCHEME.match(url):
202
203
  return False
203
- password, query, fragment = _secrets._url_parts(url)
204
+ password, query, fragment = _secrets.url_parts(url)
204
205
  # what says it is synthetic is the value, never the host or the path (`examples.blob.core.windows.net`)
205
206
  if len(password) >= 8 and re.search(r"[0-9]", password) and re.search(r"[A-Za-z]", password) and _real(unquote(password)):
206
207
  return True
@@ -9,7 +9,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
9
9
  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.
10
10
  metadata:
11
11
  author: PassionCode.ai
12
- version: "0.79.1"
12
+ version: "0.80.2"
13
13
  contract-version: "0.1.0"
14
14
  extension: "fabric-service/0.1"
15
15
  extension-commit: "18f8d48762edc5bfb1433657cbfe1c16196df1a6"
@@ -168,7 +168,9 @@ Calling out: [the connectors reference](references/connectors.md) (`fabric_http`
168
168
  deadline, safe retries and a size cap; `fabric_openrouter` with the agent's own key, a
169
169
  spend ceiling before every attempt, receipts and a fallback). The draft-and-check step of a
170
170
  support agent on the kit: [`recipes/support-agent/`](recipes/support-agent/README.md); the whole agent:
171
- creating-fabric-agents' `references/agents-that-answer-people.md`.
171
+ creating-fabric-agents' `references/agents-that-answer-people.md`. A service that is only a dashboard:
172
+ [`recipes/dashboard-only/`](recipes/dashboard-only/README.md); a door that hands a key on sealed:
173
+ [`recipes/key-delivery-door/`](recipes/key-delivery-door/README.md).
172
174
 
173
175
  Startup order is the part that goes wrong; keep it exactly:
174
176
 
@@ -0,0 +1,67 @@
1
+ # Recipe: a service that is only a dashboard
2
+
3
+ A `fabric-service/0.1` service with a dashboard and nothing else: no capabilities, no `mcp` surface, no `usage`
4
+ surface, no provider key and no token of anybody else. Fabric Dashboards asked for it (FD-171, board thread
5
+ `38c35e11`) for a page that shows what the agents of this computer are doing; this recipe is the shape agreed there
6
+ (FAA-268). Built only on the service kit (`fabric_service.py`, `fabric-service.mjs`). Tests:
7
+ `test/test_recipe_dashboard_only.py` and `test/recipe-dashboard-only.test.mjs`.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ python3 recipes/dashboard-only/install.py --id example-dashboard --folder ~/.local/share/example-dashboard
13
+ node recipes/dashboard-only/install.mjs --id example-dashboard --folder ~/.local/share/example-dashboard
14
+ ```
15
+
16
+ Both twins take the same flags and write the same descriptor bytes (but `installedAt`). Optional flags: `--instance`
17
+ (default `default`), `--name`, `--repository` (the repository its code is built from; by default the folder's own
18
+ `https://` origin, and a new install without one is refused by the kit, FAA-198), `--port` (this port and no other),
19
+ `--label` with `--plist` (a launchd job, below), `--services-dir` (default `FABRIC_SERVICES_DIR`, else the OS's),
20
+ `--json`.
21
+
22
+ | Output | Rule |
23
+ |---|---|
24
+ | last stdout line | `{"descriptor": "<path>", "key": "<id>.<instance>", "port": <n>}`; `--json` prints only it |
25
+ | a repeat with the same `--id` | changes nothing and answers the same line; a descriptor of that key another installer wrote, or one with another `--folder`, is refused and left as it is |
26
+ | exit codes | 0 installed or already installed, 1 refused (the reason on stderr, never a value), 2 usage |
27
+
28
+ The folder gets the token file (`service.token`, 0600, never printed) and later the event log, the login state and
29
+ `snapshot.json`. A refused install writes nothing.
30
+
31
+ ## The port
32
+
33
+ The installer takes the first port of 47300 to 47999 that no other descriptor of this account declares and that
34
+ `127.0.0.1` binds now, and writes the descriptor through the kit's `write_descriptor` / `writeDescriptor`, whose port
35
+ claim check refuses a port another descriptor declares; a port taken between the two is retried on the next one, up to
36
+ eight. That is the kit's port check as it stands, the older sense of a port claim
37
+ ([lifecycle.md](../../references/lifecycle.md), *Install, upgrade, uninstall*): a port another OS account listens on
38
+ or claims still passes (FAA-225). The contract's HA-1 claim file per OS account is not written, because its schema
39
+ waits on the contract's CT-228; the recipe writes it the day the kit does, with no change to its flags or its output.
40
+
41
+ ## Running it
42
+
43
+ ```bash
44
+ python3 recipes/dashboard-only/dashboard_service.py serve --descriptor <path the installer printed>
45
+ ```
46
+
47
+ It serves `/.well-known/fabric-service` (surfaces `dashboard` and `events` only), the events feed and the login code
48
+ for the service token, the one-time login and the signed-in page. Under launchd, pass `--label` and `--plist` on the
49
+ first install and install the job with the kit's `launchd_plist` and `launchd_install`
50
+ ([lifecycle.md](../../references/lifecycle.md)), whose `ProgramArguments` run the line above from a release
51
+ directory. `check_service.py <id>` passes every rule it can run on it (the test runs it).
52
+
53
+ ## Where its data comes from
54
+
55
+ The page shows `<folder>/snapshot.json`, which the person's agent writes from what it reads through the Fabric
56
+ Dashboards MCP (`list_services`, `spend`, `connections`) and Project Observatory's tools. The service itself calls
57
+ nobody, so it holds no token but its own:
58
+
59
+ ```json
60
+ {"takenAt": "2026-11-12T08:00:00Z",
61
+ "sections": [{"title": "Services", "source": "fabric-dashboards list_services",
62
+ "columns": ["Service", "State"], "rows": [["example-agent.default", "ready"]]}]}
63
+ ```
64
+
65
+ A snapshot that is missing, cannot be read, does not have this shape, is over 1 MB or is older than a day is
66
+ reported in `degraded` (source `snapshot`) and never shown as data. The well-known document declares the snapshot as
67
+ its one connection (`kind: store`), so a host draws where the page's numbers come from.