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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/README.md +31 -11
  3. package/SECURITY.md +17 -3
  4. package/package.json +1 -1
  5. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +2 -2
  6. package/plugins/fabric-agent-adapter/hooks/hooks.json +1 -1
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +15 -5
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +37 -5
  9. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/operator-channel-and-repository.md +13 -3
  10. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +2 -2
  11. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/when-to-use.md +5 -0
  12. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +14 -7
  13. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +27 -39
  14. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +6 -3
  15. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +34 -8
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +8 -5
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +2 -2
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +1 -1
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +37 -5
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +4 -1
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +2 -1
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +74 -3
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +8 -3
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +5 -0
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +43 -1
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/when-to-use.md +5 -0
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +3 -2
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +5 -0
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +2 -0
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +12 -6
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +2 -2
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +30 -2
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +7 -2
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +54 -20
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +4 -0
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +3 -1
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +4 -1
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +6 -0
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +339 -26
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet.mjs +198 -0
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +6 -0
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +3 -0
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +13 -5
  46. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +2 -2
  47. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +22 -4
  48. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +14 -1
  49. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +41 -1
  50. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +6 -0
  51. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +6 -0
  52. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +367 -27
  53. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet.py +208 -0
  54. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +323 -25
  55. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +19 -6
  56. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +37 -5
  57. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +6 -3
  58. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/operator-channel-and-repository.md +13 -3
  59. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +6 -3
  60. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/when-to-use.md +5 -0
  61. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +4 -0
  62. package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +179 -0
  63. package/plugins/fabric-agent-adapter/skills/observing-products/references/channels.md +129 -0
  64. package/plugins/fabric-agent-adapter/skills/observing-products/references/cycle.md +129 -0
  65. package/plugins/fabric-agent-adapter/skills/observing-products/references/when-to-use.md +5 -0
  66. package/plugins/fabric-agent-adapter/skills/observing-products/scripts/observer_inventory.py +259 -0
  67. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +30 -39
  68. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/native-host-hooks.md +2 -0
  69. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +57 -37
  70. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +50 -25
  71. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +1 -1
  72. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/when-to-use.md +5 -0
  73. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +26 -10
  74. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +11 -5
@@ -120,7 +120,11 @@ A request to a Project whose owner is not working now would wait for nobody. Aft
120
120
  change nothing.
121
121
  2. **Live:** wait under a watcher (below) and work on independent tasks meanwhile; never touch files a live session
122
122
  holds.
123
- 3. **Nobody live, and the change is small.** Small means all three:
123
+ 3. **Nobody live, the machine is the operator's, and the change is small.** The machine's trust decides first
124
+ (ADR-0176): `fabric_whoami` names it as `protocol.machine` (`trust: mine | organization | unknown`, from Fabric
125
+ 0.3.12, set by the person in Settings). Only `mine` (the operator's own computer, or a remote one of theirs) allows
126
+ this step; on `organization` or `unknown` never edit another Project: step 4. Before Fabric reports trust, the
127
+ operator's own computer reads as `mine`, any other as `unknown`. Small means all three:
124
128
  - it carries out what the owner's own records already say (its contract, its specification, a backlog row, a
125
129
  finding with its evidence) or fixes a defect with a test; it decides nothing new: no ADR or DEC, no change to
126
130
  what a public interface means, nothing recorded as the owner's or the operator's decision;
@@ -130,32 +134,50 @@ A request to a Project whose owner is not working now would wait for nobody. Aft
130
134
  Make it in your own worktree of the owner's repository by its AGENTS.md (its branch naming, its agent-sync leases,
131
135
  its gate green, its handoff and a backlog row closed with the receipt), and integrate the pull request as its rules
132
136
  allow. Then reply on the request's thread, one line of body text (not a `reaction`, which only an announcement's
133
- reply carries): **adapted by** your Project, the merge commit, and what the owner should look at. The owner's next
134
- session reads it and may revert it. One such change per target repository at a time. Your own reply does not answer
135
- your own request: a queued one stays on the board, on its thread and on Fabric's home ("requests no agent answers
136
- now"), for the owner to see until its deadline; do not cancel it.
137
- 4. **Not small** (a decision, a contract, a release, a large change): the request stays queued for its owner until its
137
+ reply carries): **adapted by** your Project, the merge commit, and what the owner should look at. Then cancel your own
138
+ request (`com.cancel`, the reason "done by the asker"): your reply as the asker does not answer it (only the target's
139
+ does), so without the cancel it stays queued and expires. The owner's next session reads the thread and may revert
140
+ the change. One such change per target repository at a time (the knowledge base's agents-first.md, "When nobody
141
+ answers", step 3).
142
+ 4. **Not small** (a decision, a contract, a release, a large change), **or the machine is not the operator's**: the request stays queued for its owner until its
138
143
  deadline (seven days by default); never take a decision that is the owner's, record the wait in one line (what it
139
144
  waits on, which watcher acts) and go on with your own work. An expiry posts no message, so the mail watcher does
140
145
  not see it: at each idle look, read your open request's state (`com.get`) before its deadline. If it expires and the
141
146
  ask still stands, submit it again.
142
147
 
143
- **A decline from a run Fabric started** (adapter 0.68.0; the question to the operator since 0.69.0): where Fabric's dispatcher runs, a request to a Project with no live
144
- session and whose capability slot no service ever held (`absent` at generation 0), whose session holds no
145
- `board.respond` grant (every coding-session Project before Fabric ships CO-506), is answered within minutes (after a
146
- two-minute grace) by a run that declines it until CO-506, asks for a resend and asks the operator about the gate
147
- (`fabric_question_ask`, `about: board.gate.<capability>`) (where the run carries this adapter's exit
148
- contract; a run under Switchboard's or Fabric 0.3.16's own text alone has no such branch, and the request may stay
149
- queued) ([on-demand session](on-demand-session.md), step 3).
148
+ The rule is the operator's (2026-10-22); the knowledge base's agents-first.md, "When nobody answers", records it. The
149
+ decline below is the adapter's own.
150
+
151
+ **A decline from a run Fabric started** (adapter 0.68.0; the question to the operator since 0.69.0): where Fabric's
152
+ dispatcher runs, a request to a Project with no live session goes, after a two-minute grace, to a run Fabric starts.
153
+ From Fabric 0.3.17 (CO-506) that run's own Project is granted `board.respond` at `com.enroll`, so the run takes the
154
+ request. Before 0.3.17, and from it only where CO-506 grants nothing (a machine marked as an organization's, no live
155
+ credential, a standing denial, a full ask queue), its `com.enroll` is refused; when the capability's slot is `absent`
156
+ at generation 0 (no service of the project ever held it), the run declines the request once with the reason and what
157
+ opens it (before 0.3.17: a Fabric with CO-506), asks for a resend and asks the operator about the gate
158
+ (`fabric_question_ask`, `about: board.gate.<capability>`) (where the run carries this adapter's exit contract or Fabric
159
+ 0.3.17's packet text; a run under Switchboard's or Fabric 0.3.16's own text alone has no such branch, and the request
160
+ may stay queued) ([on-demand session](on-demand-session.md), step 3).
150
161
  The request is then `answered`: read the decline and `com.read_ack` it (unread, it shows in «На проверке»); it no
151
- longer waits for its owner, so steps 1, 2 and 4 no longer apply to it, while a
152
- small change may still be adapted under step 3. Record the wait on CO-506 (what it waits on, which watcher acts); once
153
- it ships and the ask still stands, cancel the answered request (`com.cancel`) and, once the cancel's receipt reads it
154
- `cancelled`, submit it again with a new key, once: a second decline after CO-506 shipped (a machine CO-506 grants
155
- nothing on) goes to the person. A live session's answer that the request cannot be taken now (its own refused `com.enroll`,
156
- below) is read the same way: `com.read_ack`, record the wait on the reason it names, one resend once that clears.
157
-
158
- The rule is the operator's (2026-10-22); the knowledge base's agents-first.md, "When nobody answers", records it.
162
+ longer waits for its owner, so steps 1, 2 and 4 no longer apply to it, while a small change may still be adapted under
163
+ step 3. Record the wait on the reason the decline names (what it waits on, which watcher acts); once that clears (an
164
+ upgrade to Fabric 0.3.17 or later, or the grant the reason names) and the ask still stands, cancel the answered request
165
+ (`com.cancel`) and, once the cancel's receipt reads it `cancelled`, submit it again with a new key, once: a second
166
+ decline goes to the person. A live session's answer that the request cannot be taken now (its own refused
167
+ `com.enroll`, below) is read the same way: `com.read_ack`, record the wait on the reason it names, one resend once that
168
+ clears.
169
+
170
+ **A gate question is moot once the request is answered or taken.** The usual end of a refusal at a slot a service held is that
171
+ service rejoining and taking the request; then the gate question a refused run or session filed stays open though
172
+ nothing waits on it (Fabric settles no question by its `about`, CO-519, and no Fabric through v0.3.19 lets an agent close
173
+ one). A run or session that later finds the request answered, or taken and completed, says so once, naming the question (`about:
174
+ board.gate.<capability>` and the `commandId` that `gate-id` prints), in its closing `notes` or to the person (FAA-248).
175
+
176
+ **A dispatched Claude Code run that sees only `fabric_whoami`** among Fabric's tools (a headless `claude -p` session
177
+ reads its list of tools once, and `fabric-mcp` offered the rest only after the call admitted it) is Fabric's CO-563,
178
+ fixed on Fabric's main at `780022af` and expected in the release after v0.3.19. Until then such a run cannot read or
179
+ answer the board: it says so in its exit report (an `in_progress` checkpoint naming CO-563 when it holds the lease, then the retro) and never
180
+ claims the board was read; the request waits for the next run or a live session (FAA-296).
159
181
 
160
182
  ## The project directory
161
183
 
@@ -177,11 +199,12 @@ The rule is the operator's (2026-10-22); the knowledge base's agents-first.md, "
177
199
 
178
200
  Answering it as a responder is `com.enroll` then `com.claim`, under a `board.respond` grant: a service asks it with
179
201
  `fabric.access.request` and the operator decides it in Fabric (Agent access); a coding session cannot ask it, and
180
- gets its own Project's at `com.enroll` once Fabric ships CO-506. The request then moves with `com.accept` and `com.complete`. A product agent that answers
202
+ gets its own Project's at `com.enroll` from Fabric 0.3.17 (CO-506). The request then moves with `com.accept` and `com.complete`. A product agent that answers
181
203
  requests as a service uses the service kit's responder (building-fabric-services, `references/board.md`).
182
204
 
183
205
  **A request whose thread holds an *adapted by* reply** was carried out by another Project under "When nobody answers".
184
- Review the merged change; then reply on the request (that answers it): kept, or reverted and why.
206
+ Review the merged change; then reply on its thread: kept, or reverted and why (the asker cancels its request once it
207
+ adapted, so the reply informs; on a request still open, your reply as its target answers it).
185
208
 
186
209
  **A participant reply answers the request** (DEC-0098, DEC-0102). Your Project's `com.reply` (any message of yours
187
210
  whose `replyTo` is the request) moves a queued request to `answered`: no responder claims it any more and its
@@ -191,7 +214,9 @@ nearest date (Answering at once, below); the result then follows in the same thr
191
214
  service responder should still do the work (`com.status`'s slot for the capability is `active` or `stale`, or `absent` at a
192
215
  generation of 1 or more: a holder enrolled on it), do
193
216
  not reply on the request: tell the person and leave it queued. A session whose own `com.enroll` is refused
194
- `not_authorized` has no route to the grant before Fabric's CO-506: when its capability's slot is `absent` at generation 0,
217
+ `not_authorized` has no route to the grant (before Fabric 0.3.17 every session; from it, where CO-506 grants nothing:
218
+ an organization's machine, no live credential, a standing denial, a full ask queue): when its capability's slot is
219
+ `absent` at generation 0 (no service of the project ever held it),
195
220
  it answers the request once that it cannot be taken now and why, does no work on it without the slot, then asks the
196
221
  operator once (at any other slot state it sends no reply and still asks, FAA-241) with `fabric_question_ask` (`kind: access`, `about: board.gate.<capability>`, the `commandId` that
197
222
  `python3 <skill-dir>/scripts/fabric_session.py gate-id <request>` prints, the same a run uses, and a `text` naming the request, the gate and what
@@ -199,7 +224,7 @@ opens it; without Python, no `commandId` can be made: tell the person only), tel
199
224
  filed even when that is the operator: Fabric keeps its `about` and settles nothing by it yet, CO-519), and stops handling
200
225
  that request without waiting (Fabric's `board.read-at-start` rule and `com.enroll`'s own tool description say the
201
226
  same since main `4f3c742a`: the question since `603da12f` (CO-517) in the rule and `12d57bdd` in the description, the slot condition since
202
- `dbfbfce4` and `12d57bdd`; in no Fabric release yet, and
227
+ `dbfbfce4` and `12d57bdd`, all in Fabric 0.3.17;
203
228
  on Fabric 0.3.16 this rule wins). Never write "it stays queued" in a reply: the reply itself ends the queue. A
204
229
  run Fabric started on a board request has no person in its chat to tell: with the slot `absent` at generation 0 it declines,
205
230
  asks for a resend and asks the operator with `fabric_question_ask`; otherwise it leaves the request queued and still asks ([on-demand session](on-demand-session.md), step 3).
@@ -28,7 +28,7 @@ session start and turn; FAA-183 tracks making the plugin find the interpreter it
28
28
  | Event | When it acts | What it does |
29
29
  |---|---|---|
30
30
  | SessionStart (`startup`, `clear`; the plugin's matcher, and the script ignores `compact` and `resume` for every host) | Claude Code has an MCP server named `fabric` (local, project or user scope) | Fabric running, in a project that uses Fabric (below): adds the preamble: `fabric_whoami` first; `com.status` and `com.list`, `com.read_ack` only what was read, and what waits on the Project answered in the same turn with its state, the result later (FAA-205); `fabric_work_list`, then `fabric_work_claim`; `fabric_work_release` at the end, and, when a piece of work or the session ends, its retro (`observatory_lesson`, insight/1 lines in `insights/` (below), or one line that there is nothing; FAA-206); ask another Project only through `com.submit`. Fabric running in a folder with no such sign (a user-scope registration reaches every folder): one line pointing at `fabric_whoami`, which names the project when Fabric serves the folder anyway (FAA-60). Fabric down, in a project that uses Fabric: one line, skip `fabric_whoami` and the board, start Fabric through the lifecycle broker only if the task needs it. Fabric down anywhere else, or not registered: nothing. |
31
- | Stop (each turn) | as for SessionStart, and the running Fabric's `hub.json` lists `"session-hooks/1"` in `features` | measures the turn's facts (HEAD, files changed, lines added and removed: counts and ids only, git sharing 1.5 s) and names the other Git repositories the turn touched (FAA-140, below: only once Fabric takes them), spools them in one entry and starts a detached worker that sends `fabric_session_observe {event: "turn", head, filesChanged, linesAdded, linesRemoved, …}` as stored (with `repositories` only where that tool's input schema lists the field as an array of items with host, owner and name requiring nothing else, at most its `maxItems`, each item meeting what the schema says of its fields or left out; a turn spooled with the mark and delivered to a Fabric whose schema no longer takes them goes without them, and the log says so once per session; with a Fabric that never took them nothing is read and nothing is logged), keeps the `nudges` its answer carries (Fabric 0.3.14: each `{kind, says, task_id?, request_id?}`, shown on a turn once, at most five; it stays for `fabric_work_list` and `fabric_heartbeat`, so it may come again there) in `nudges.json`, then reads `com.status` and keeps the unread count in the session's spool (`board.json`) |
31
+ | Stop (each turn) | as for SessionStart, and the running Fabric's `hub.json` lists `"session-hooks/1"` in `features` | measures the turn's facts (HEAD, files changed, lines added and removed: counts and ids only, git sharing 1.5 s) and names the other Git repositories the turn touched (FAA-140, below: only once Fabric takes them), spools them in one entry and starts a detached worker that sends `fabric_session_observe {event: "turn", head, filesChanged, linesAdded, linesRemoved, …}` as stored (with `repositories` only where that tool's input schema lists the field as an array of items with host, owner and name requiring nothing else, at most its `maxItems`, each item meeting what the schema says of its fields or left out; a turn spooled with the mark and delivered to a Fabric whose schema no longer takes them goes without them, and the log says so once per session; with a Fabric that never took them nothing is read and nothing is logged), keeps the `nudges` its answer carries (Fabric 0.3.14: each `{kind, says, task_id?, request_id?}`, shown on a turn once, at most five; it stays for `fabric_work_list` and `fabric_heartbeat`, so it may come again there: act on it once) in `nudges.json`, then reads `com.status` and keeps the unread count in the session's spool (`board.json`) |
32
32
  | UserPromptSubmit | the session's spool holds a count or nudges, and the session was not started on a board request (nor runs inside such a run) | adds ONE line, `N unread on the project board: com.list; answer what waits on you in this turn: ...` (FAA-205: its state now, the result later), only when the count rose since the last line (a fall or the same count says nothing), then each nudge's `says` once, as Fabric writes it (Fabric 0.3.14 starts each with `Fabric:`; any other line gets the tag `Fabric reminder: `), one line each, at most five lines of at most 400 characters; the nudges file is taken by renaming it away before they are printed (FAA-142). It reads, renames and writes small files in the session's folder and never calls Fabric. |
33
33
  | SessionEnd | as for Stop | spools the end; the worker sends `fabric_work_release {claims: "session", outcome: "let-go", …}` (with `observedAt`, the time the hook fired, where the tool's input schema names it: Fabric 0.3.9 then refuses an end from before the session enrolled), then `fabric_session_observe {event: "end", reason}`, and removes the session's spool. Reason `clear` ends nothing: `/clear` keeps the agent process and its fabric-mcp, so no claim is released and no end observed; turns already spooled still go out (since 0.42.0). Their nudges stay in the old id's folder and are not printed here; Fabric 0.3.14 only shows a nudge on a turn's answer and keeps it for `fabric_work_list` and `fabric_heartbeat`, so the new session gets it there (board `b605a636`; decision of 0.51.2, FAA-142) |
34
34
  | PostToolUse (the plugin's matcher: Fabric's `fabric_work_release` and `fabric_task_release` only) | the call released a claim as done (`how`, or `outcome` when `how` is absent, done; the older tool's `outcome` succeeded) and Fabric's answer did not refuse it or release nothing (`released: false`, Fabric's refusal; `already-released`; a `claims: "session"` release with no live claim, `claims: []`; a repeat, which Fabric answers with the first release's `at`; a claim the operator released first, `released-by-operator`; a repeat after a first answer the agent never saw (lost in the client, or `unconfirmed`), and a session release answered `partial` or `unconfirmed`; in each the session-end retro still stands; a payload over 4 MiB is read as nothing and gets no line, as on every hook (below); or `isError`; an answer the hook cannot read keeps the line) | adds ONE line naming the retro in this turn (`observatory_lesson`, or the turn's answer without it; insight/1 lines in `insights/` (below); or one line that there is nothing; FAA-206, Fabric's `retro-after-work`, since Fabric 0.3.16, `e1eecec0`); reads no file, calls nothing, also in a session Fabric launched |
@@ -0,0 +1,5 @@
1
+ # When to use working-with-fabric
2
+
3
+ The skill's full trigger description, kept here so the front matter's `description` stays within the client's skill listing budget (about 350 characters a skill). Read it when choosing between this skill and a neighbour.
4
+
5
+ Use when a coding agent (Claude Code, Codex, Kimi Code, Kilo, Hermes) works in a project through Fabric: finding Fabric, having the lifecycle broker start Fabric, registering fabric-mcp once with no secret, fabric_whoami first, the project board (com.status, com.list, com.read_ack, com.submit to another Project, watch-mail), claiming and releasing work (fabric_work_claim, fabric_work_release), relaying the operator's decision or mandate (operator-decision-required), reports, handoffs, refusals. Triggers: "connect to Fabric", "register fabric-mcp", "claim the task in Fabric", "check the project board", "ask the other project through Fabric", "Fabric is not running", «подключись к Fabric», «возьми задачу в Fabric», «проверь доску проекта», «Fabric не запущен», "hit your limit, resets at". Explains the plugin's session hooks. NOT for building or adapting an agent or a board responder (creating-fabric-agents, adapting-projects-to-fabric, building-fabric-services), changing Fabric's own code, or messaging people.
@@ -74,7 +74,10 @@ PREAMBLE = (
74
74
  "Call `fabric_whoami` first. Then read the project board: `com.status` (is it ready, how many unread), "
75
75
  "`com.list` for the messages, and `com.read_ack` only for what you actually read. A request, announcement or "
76
76
  "question waiting on your Project gets an answer in the same turn: its state now (done, where, when, what "
77
- "blocks), the result later in a second message; a request a responder should take stays queued. "
77
+ "blocks), the result later in a second message; a request your Project's service should take stays queued for "
78
+ "it. Before Fabric 0.3.17 a run Fabric starts on a request no session can take declines it and the asker resends; "
79
+ "from 0.3.17 (CO-506) the session's own Project is granted `board.respond` at `com.enroll`, so you or that run "
80
+ "can take it (`com.enroll`, `com.claim`). "
78
81
  "Before you start work, call `fabric_work_list` (who is working on what here) and "
79
82
  "claim your task with `fabric_work_claim`; release it with `fabric_work_release` when you stop. When a piece of "
80
83
  "work ends (released as done, or the session about to end), write its retro in the same turn: `observatory_lesson` "
@@ -137,12 +140,12 @@ DISPATCH_IF = (
137
140
  " Other values: no run; say so in the request's thread and stop. The run:")
138
141
  #: The conditional contract at its longest (a 64-character handoff and a session id) is held under Hermes' 4000
139
142
  #: characters by test_a_native_bridge_is_always_told_the_contract_and_writes_no_record; measure before adding words
140
- #: (3996 in 0.68.0 and in 0.69.0, 3999 in 0.71.0, 3996 in 0.72.0: a new rule takes its characters back from the text around it).
143
+ #: (3996 in 0.68.0 and in 0.69.0, 3999 in 0.71.0, 3996 in 0.72.0, 3994 in 0.79.0: a new rule takes its characters back from the text around it).
141
144
  DISPATCH_CONTRACT = (
142
145
  " Do that "
143
146
  "request and end; never wait for a person. 1. `fabric_whoami`, then the packet "
144
147
  "(`observatory_checkpoint_latest` {{workflowId: {workflow}}}): obey its constraints first; its goal describes "
145
- "the request; artifacts: capability, deadline, run-ends-at. Read the request (`com.get` "
148
+ "the request; artifacts: capability, deadline, run-ends-at (ids: the line's first). Read the request (`com.get` "
146
149
  "{{message: <request>}}) and its thread (`com.list` {{thread: <the `thread` com.get answered>}}). 2. "
147
150
  "`observatory_handoff_accept` with {handoff}, the `owner`, `idempotencyKey` and `executor` the launcher's prompt names "
148
151
  "(when it names none: your `owner` `agent:<name>`, the key `accept-<handoff>`, no executor) and this session's own "
@@ -150,7 +153,7 @@ DISPATCH_CONTRACT = (
150
153
  "no lease: end the run without touching the request. 3. `com.status` once ("
151
154
  "epoch, generation); `com.enroll` (the capability), `com.claim` (the request), "
152
155
  "`com.accept` and `com.effect_begin` before an effect; a not_authorized enroll: no slot, no work and, if queued and its `com.status` slot `absent` at generation 0, one `com.reply` (replyTo it) "
153
- "declining until CO-506 (resend then), either way `fabric_question_ask` {{kind: access, about: board.gate.<capability>, commandId: {gate}, text: the request, its gate, what opens it}}, then step 6 (done) once `com.get` reads answered, and 7; left accepted by an earlier run: `com.responder_replace` (`expectedGeneration`) and its transfer; resolve `outcome_unknown` only with "
156
+ "declining with the reason (resend then), either way `fabric_question_ask` {{kind: access, about: board.gate.<capability>, commandId: {gate}, text: the request, its gate, what opens it}}, then step 6 (done) once `com.get` reads answered, and 7; left accepted or in_progress by an earlier run: `com.responder_replace` (`expectedGeneration`) and its transfer; resolve `outcome_unknown` only with "
154
157
  "`com.reconcile` and an observed result. After the lease, claim and reply: an in_progress checkpoint (`workflowId`, "
155
158
  "`leaseId`). Every checkpoint repeats `goal`, `constraints`, `artifacts` and `credentials` of "
156
159
  "`checkpoint.body` (not the answer's top-level `credentials` or the accept's "
@@ -165,7 +168,7 @@ DISPATCH_CONTRACT = (
165
168
  "`com.complete` (observed: true): completed, or failed_known with the reason. 6. Once the board took it, "
166
169
  "`observatory_checkpoint_write` with `workflowId` {workflow}, a `stepId`, "
167
170
  "the `leaseId`, `close: true`, a status, the packet's body plus `done` and `notes`: done for completed; for "
168
- "failed_known blocked with the reason in notes (failed once Observatory takes it, OBS-323). 7. Write the retro "
171
+ "failed_known blocked with the reason in notes. 7. Write the retro "
169
172
  "(`observatory_lesson` and insight/1 lines in `insights/`, or one line that there is nothing; no "
170
173
  "values), then end: no board sweep, work claim or watch-mail. On an error you cannot get past: complete the request failed_known if you hold its fence; close the workflow "
171
174
  "only once the request is settled (`com.get`: answered, terminal or outcome_unknown), else leave it open; then the retro. Details: working-with-fabric references/on-demand-session.md."
@@ -331,7 +334,7 @@ def dispatch_cleared_from(dispatch: Dict[str, str], env: Optional[Dict[str, str]
331
334
  folder, key = _dispatch_record(dispatch, env)
332
335
  try:
333
336
  held = json.loads(read_small(folder / (key + ".json")) or "null")
334
- except ValueError:
337
+ except (ValueError, RecursionError): # 3.9 raises RecursionError on a deep record (FAA-208)
335
338
  return None
336
339
  if not isinstance(held, dict) or "cleared_from" not in held:
337
340
  return None
@@ -356,7 +359,7 @@ def dispatch_holder(dispatch: Dict[str, str], session_id: Any, env: Optional[Dic
356
359
  if take_over:
357
360
  try:
358
361
  held = json.loads(read_small(folder / (key + ".json")) or "null")
359
- except ValueError:
362
+ except (ValueError, RecursionError):
360
363
  held = None
361
364
  # no record: no prompt of this process confirmed the run (Fabric's spec 3.5), so the clear holds nothing. On
362
365
  # Windows the hook's parent is the Git Bash a hook runs in, a new one each time, so a /clear cannot tell the
@@ -381,18 +384,28 @@ def dispatch_holder(dispatch: Dict[str, str], session_id: Any, env: Optional[Dic
381
384
  return True
382
385
  # another agent process holds it: this is a session inside the run that cleared, still nested
383
386
  try:
387
+ # a `dispatch` that is not a folder raises FileExistsError here: a folder that cannot be written, never an
388
+ # existing record (FAA-208)
384
389
  folder.mkdir(parents=True, exist_ok=True, mode=0o700)
390
+ except OSError:
391
+ return True
392
+ try:
385
393
  fd = os.open(str(folder / (key + ".json")), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
386
394
  except FileExistsError:
387
395
  held = read_small(folder / (key + ".json")) or ""
388
396
  try:
389
397
  return json.loads(held).get("session") == session_id
390
- except (ValueError, AttributeError):
398
+ except (ValueError, AttributeError, RecursionError):
391
399
  return False
392
400
  except OSError:
393
401
  return True
394
402
  try:
395
- with os.fdopen(fd, "w", encoding="utf-8") as handle:
403
+ try:
404
+ handle = os.fdopen(fd, "w", encoding="utf-8")
405
+ except BaseException: # the descriptor is still open: Windows refuses to unlink an open file (0.79.0 seam)
406
+ os.close(fd)
407
+ raise
408
+ with handle:
396
409
  handle.write(json.dumps(record))
397
410
  except OSError: # a full disk: no half record that would make this session a stranger later
398
411
  try:
@@ -415,7 +428,7 @@ def dispatch_held_elsewhere(dispatch: Dict[str, str], session_id: Any, env: Opti
415
428
  return False
416
429
  try:
417
430
  return json.loads(read_small(folder / (key + ".json")) or "null").get("session") != session_id
418
- except (ValueError, AttributeError):
431
+ except (ValueError, AttributeError, RecursionError):
419
432
  return True
420
433
 
421
434
 
@@ -839,6 +852,9 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
839
852
  p_status = sub.add_parser("status", help="where Fabric is and what to do next")
840
853
  p_status.add_argument("--json", action="store_true")
841
854
  p_status.add_argument("--cwd", default=".")
855
+ # FAA-236: named in --help; the command itself is answered above, before argparse
856
+ p_gate = sub.add_parser("gate-id", help="the commandId of a request's gate question, the one a dispatched run uses")
857
+ p_gate.add_argument("request", help="the board request id, a UUID")
842
858
  args = parser.parse_args(argv)
843
859
  if args.command != "status":
844
860
  parser.print_help()
@@ -58,7 +58,7 @@ SESSION_ID = re.compile(r"[A-Za-z0-9._:-]{1,128}")
58
58
  LOOPBACK_ORIGIN = re.compile(r"http://127\.0\.0\.1:(\d{1,5})")
59
59
 
60
60
 
61
- VERSION = "0.77.0"
61
+ VERSION = "0.79.1"
62
62
 
63
63
 
64
64
  HOSTS = {"claude": "Claude Code", "codex": "Codex", "kimi": "Kimi Code",
@@ -280,7 +280,9 @@ def read_small(path: Path, cap: int = READ_CAP) -> Optional[str]:
280
280
  return None
281
281
 
282
282
 
283
- def pid_alive(pid: int) -> bool:
283
+ def pid_alive(pid: int) -> Optional[bool]:
284
+ """True: a process of this OS account runs under `pid`; False: none does. None (falsy): a process of another
285
+ account does (EPERM), as when the id hub.json names was reused: not verified, never called running (FAA-254)."""
284
286
  if pid <= 0:
285
287
  return False
286
288
  if sys.platform == "win32":
@@ -298,7 +300,7 @@ def pid_alive(pid: int) -> bool:
298
300
  try:
299
301
  os.kill(pid, 0)
300
302
  except PermissionError:
301
- return True
303
+ return None
302
304
  except OSError:
303
305
  return False
304
306
  return True
@@ -324,7 +326,11 @@ def locate_hub(root: Path, alive=pid_alive) -> Dict[str, Any]:
324
326
  pid = doc.get("pid")
325
327
  if not isinstance(pid, int) or isinstance(pid, bool) or pid <= 0:
326
328
  return {"state": "not-a-hub", "reason": "hub.json names no process"}
327
- if not alive(pid):
329
+ live = alive(pid)
330
+ if live is None: # FAA-254: another account's process under that id, so not this account's Fabric
331
+ return {"state": "not-a-hub", "reason": "hub.json names process %d, which runs under another OS account: "
332
+ "not verified" % pid}
333
+ if not live:
328
334
  return {"state": "not-running", "reason": "hub.json names process %d, which is not running" % pid}
329
335
  features = doc.get("features")
330
336
  features = [f for f in features if isinstance(f, str)] if isinstance(features, list) else []
@@ -699,7 +705,7 @@ def prune_dispatch_records(env: Optional[Dict[str, str]] = None, now: Optional[f
699
705
  records = []
700
706
  for record in records:
701
707
  try:
702
- if now - record.stat().st_mtime > SESSION_STALE_S:
708
+ if now - record.lstat().st_mtime > SESSION_STALE_S: # the link's own age: a dangling one goes too (FAA-208)
703
709
  record.unlink()
704
710
  removed += 1
705
711
  except OSError: