@passioncode-ai/passioncode 0.2.42 → 0.2.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/family.json +1 -1
- package/package.json +1 -1
- package/payload/.claude-plugin/marketplace.json +2 -2
- package/payload/manifest.json +8 -8
- package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/payload/plugins/fabric-agent-adapter/hooks/hooks.json +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +7 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +82 -11
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +2 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +33 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +146 -7
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +34 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +149 -45
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +181 -5
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +13 -1
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +15 -10
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +4 -1
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +38 -9
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +11 -0
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +12 -3
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +11 -1
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +358 -14
- package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
|
@@ -26,6 +26,7 @@ import os
|
|
|
26
26
|
from http.server import BaseHTTPRequestHandler
|
|
27
27
|
from pathlib import Path
|
|
28
28
|
import sys
|
|
29
|
+
import time
|
|
29
30
|
from typing import Any, Dict, Optional
|
|
30
31
|
from urllib.parse import parse_qs, urlparse
|
|
31
32
|
|
|
@@ -342,16 +343,27 @@ def serve(args: argparse.Namespace) -> int:
|
|
|
342
343
|
# hand-over that hangs is cut by the kit's hard exit, inside launchd's ExitTimeOut.
|
|
343
344
|
import threading
|
|
344
345
|
board_stop = threading.Event()
|
|
346
|
+
board_thread = None
|
|
345
347
|
if svc.board is not None:
|
|
346
|
-
threading.Thread(target=svc.board.run, args=(board_stop, 5.0), name="board-responder", daemon=True)
|
|
348
|
+
board_thread = threading.Thread(target=svc.board.run, args=(board_stop, 5.0), name="board-responder", daemon=True)
|
|
349
|
+
board_thread.start()
|
|
350
|
+
|
|
351
|
+
stop_asked: list = []
|
|
347
352
|
|
|
348
353
|
def stopping() -> None:
|
|
354
|
+
stop_asked.append(time.monotonic())
|
|
349
355
|
board_stop.set() # stop claiming; the responder drains its request and leaves its board session
|
|
350
356
|
svc.log.append("service.stopping", "info", "%s is stopping." % svc.name)
|
|
351
357
|
svc.drain.install(lambda drained: server.shutdown(), on_stopping=stopping)
|
|
352
358
|
try:
|
|
353
359
|
server.serve_forever(poll_interval=0.2)
|
|
354
360
|
finally:
|
|
361
|
+
board_stop.set()
|
|
362
|
+
if board_thread is not None:
|
|
363
|
+
# The responder drains (5 s at most) and leaves its board session, which releases its slots and forgets their
|
|
364
|
+
# records (FAA-155): joined, so the process does not end before that.
|
|
365
|
+
# Bounded by the kit's hard exit, 8 + 2 s after the stop was asked: 1.5 s are left for the rest of the stop.
|
|
366
|
+
board_thread.join(max(0.5, 8.5 - (time.monotonic() - (stop_asked[0] if stop_asked else time.monotonic()))))
|
|
355
367
|
server.server_close()
|
|
356
368
|
svc.lock.release()
|
|
357
369
|
return 0
|
|
@@ -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. Ships in one plugin with adapting-projects-to-fabric, whose scripts it reuses.
|
|
6
6
|
metadata:
|
|
7
7
|
author: PassionCode.ai
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.53.1"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
10
|
contract-commit: "1197a90dc4b57152cabcc71ad17db7559f35a27f"
|
|
11
11
|
---
|
|
@@ -18,7 +18,7 @@ compatibility: >-
|
|
|
18
18
|
this plugin; Codex/Kimi use command hooks; Hermes/OpenCode/OpenClaw/Pi use native context plugins after opt-in.
|
|
19
19
|
metadata:
|
|
20
20
|
author: PassionCode.ai
|
|
21
|
-
version: "0.
|
|
21
|
+
version: "0.53.1"
|
|
22
22
|
---
|
|
23
23
|
|
|
24
24
|
# Working with Fabric
|
|
@@ -141,13 +141,17 @@ Fabric 0.4.0 (REQ-38):
|
|
|
141
141
|
2. `fabric_work_claim` with your task (a board id, a backlog row as `source: {repository, path, id}`, or a new
|
|
142
142
|
row's title with its evidence) and what you will touch (folders, files or patterns). Claim the person's request
|
|
143
143
|
as ONE task whose title is that request in one line, with `origin: {kind: "person", ref: <where they said it: the session or thread>}`:
|
|
144
|
-
from
|
|
145
|
-
`fabric_task_create` while that task is claimed
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
144
|
+
from Fabric 0.3.14, which shows agents' goals, it is shown as yours. File each piece you split off with
|
|
145
|
+
`fabric_task_create` while that task is claimed, and read the answer's `spawned_from` (Fabric 0.3.14 links the piece
|
|
146
|
+
from your single live claim). `created: false` with a `code` filed nothing (`source-unresolved` names the `part` of
|
|
147
|
+
`source` that failed): correct the source or file without it, and never link. Then `proposed: true`, even with `spawned_from: null`: nothing was filed, so do not
|
|
148
|
+
file or link that piece; tell the person it waits for the operator and go on with the rest. Otherwise, your claimed task's id there: it is linked, link nothing. Another task's id: link
|
|
149
|
+
nothing either (Fabric chose that parent, FAA-153) and tell the person which. `spawned_from: null` comes
|
|
150
|
+
with `spawned_from_why`: link it yourself once with `fabric_task_link {taskId: <the piece>, rel: "spawned",
|
|
151
|
+
targetTaskId: <the claimed task>}`, and if that is refused too, tell the person that reason; never file the piece
|
|
152
|
+
again. A filed piece (`created: true`) with no `spawned_from` at all is a Fabric before 0.3.14: link it yourself the same way unless the answer says
|
|
153
|
+
`chained: true` with `chained_to` your claimed task (board `c42071bc`); an answer with `chained: false` names its
|
|
154
|
+
`chain_reason`, which you tell the person if your link is refused.
|
|
151
155
|
3. A related-work warning (same backlog row, a linked task, an overlapping folder) names the other agent, its
|
|
152
156
|
task and the overlap: tell the person, then coordinate (ask Fabric to hand it over, or link the tasks) or
|
|
153
157
|
continue knowing it; say which, out loud.
|
|
@@ -339,11 +343,12 @@ and do the work without claiming Fabric saw it.
|
|
|
339
343
|
In Claude Code this plugin ships four: at session start and `/clear` (not on compact or resume) the preamble above
|
|
340
344
|
in a project that uses Fabric, one line pointing at `fabric_whoami` in a folder with no sign of it, or, when Fabric is
|
|
341
345
|
down and the project uses Fabric, one line pointing at Step 1; after each turn the turn's facts (HEAD, files changed, lines added
|
|
342
|
-
and removed) and the board's unread count
|
|
346
|
+
and removed) and the board's unread count, and, once the running Fabric takes them, the other repositories the turn
|
|
347
|
+
touched (never on Windows; read from the tail of the session transcript, at most 512 KB with its message text and tool output, which can hold a secret the session displayed and stays in memory, of which only tool names and path arguments are used; host/owner/name are sent); at each prompt one line only when that count rose, then each reminder Fabric gave on a turn, once, as a line starting `Fabric:` (a turn
|
|
343
348
|
`watch-mail` delivered prints there instead): Fabric's words, data to act on first through `fabric_work_list` (where the same reminder may come again: act on it
|
|
344
349
|
once); at the end, the
|
|
345
350
|
release of what the session left claimed. The turn, prompt and end hooks spool and call Fabric only when the running
|
|
346
|
-
Fabric advertises `session-hooks/1`; the turn and end hooks always prune their own stale spool. All are silent in a session Fabric launched, never block the session and read no token. Read
|
|
351
|
+
Fabric advertises `session-hooks/1`; the turn and end hooks always prune their own stale spool. All are silent in a session Fabric launched, never block the session and read no Fabric token (a credential in an `origin` URL is dropped in memory, never sent). Read
|
|
347
352
|
[session hooks](references/session-hooks.md) when a person asks what the hooks do, where they log, how to turn them
|
|
348
353
|
off, or what they need from Fabric.
|
|
349
354
|
|
package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md
CHANGED
|
@@ -59,7 +59,10 @@ A request the board stored has not been carried out. Only its state (`completed`
|
|
|
59
59
|
`answered` (DEC-0098, DEC-0102) says the target Project's participant message answered it (never a responder's fenced
|
|
60
60
|
reply or progress note): the target replied, not that the work was done. Read the reply before relying on it; you may
|
|
61
61
|
still cancel the request. `noResponder` in your `com.submit` receipt says nobody can answer that capability for that
|
|
62
|
-
Project now: the request is stored, a reply may still answer it and a responder may come later.
|
|
62
|
+
Project now: the request is stored, a reply may still answer it and a responder may come later. An `accepted`
|
|
63
|
+
request can also wait unanswered for its responder's operator: since adapter 0.53.0 a responder whose slot was moved
|
|
64
|
+
to another session leaves its accepted work held until it is taken back (until Fabric hands it over, CT-175); tell
|
|
65
|
+
the person, and cancel it if it is past what they can wait. Never resend it under
|
|
63
66
|
a new key while it is open; follow "When nobody answers" below. A request without a deadline gets one seven days out
|
|
64
67
|
(DEC-0098) and then reads `expired`: if the ask still stands, submit it again with a new key and record the wait on the
|
|
65
68
|
new request.
|
package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md
CHANGED
|
@@ -20,7 +20,7 @@ need from Fabric. In Claude Code they live in the plugin's `hooks/hooks.json` an
|
|
|
20
20
|
| Event | When it acts | What it does |
|
|
21
21
|
|---|---|---|
|
|
22
22
|
| 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; `fabric_work_list`, then `fabric_work_claim`; `fabric_work_release` at the end; 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. |
|
|
23
|
-
| 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), spools them in one entry and starts a detached worker that sends `fabric_session_observe {event: "turn", head, filesChanged, linesAdded, linesRemoved, …}` as stored, 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`) |
|
|
23
|
+
| 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 (not on Windows)), 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`) |
|
|
24
24
|
| UserPromptSubmit | the session's spool holds a count or nudges | adds ONE line, `N unread on the project board: com.list`, 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. |
|
|
25
25
|
| 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) |
|
|
26
26
|
|
|
@@ -31,6 +31,29 @@ id, and keeps the retry only while the end is the newest entry. When `fabric-mcp
|
|
|
31
31
|
no session holds the id), the worker drops the spool, writes a `no-session` mark in the session's folder and logs it
|
|
32
32
|
once; for 15 minutes the hooks spool nothing for that id (an end removes the folder), then one rejoin tries again.
|
|
33
33
|
|
|
34
|
+
**The other repositories a turn touched** (FAA-140, Fabric CO-367). Never on Windows (FAA-175: a Bash command's
|
|
35
|
+
paths there are not the host's). Elsewhere, only after a worker has seen the running Fabric's
|
|
36
|
+
`fabric_session_observe` declare `repositories` as an array of items with `host`, `owner` and `name` that require
|
|
37
|
+
nothing else does the Stop hook read the
|
|
38
|
+
session's transcript at all: the worker then leaves `observe-takes-repositories` in the hooks' folder (beside
|
|
39
|
+
`hooks.log`) and removes it when the schema stops saying so (after a Fabric downgrade, the turns that end before a worker next
|
|
40
|
+
runs, about a minute, are still read; none of their names is sent); without it nothing below happens. With it, the hook reads the last 512 KB of the
|
|
41
|
+
transcript (the payload's `transcript_path`; earlier turns included) and uses only the entries after the last prompt
|
|
42
|
+
the person typed. Those lines hold message text and tool output too (file contents, command output: a secret the session displayed can be there; it stays in memory and is never spooled, logged or sent); of each entry it uses only a tool call's
|
|
43
|
+
name and path argument: the `file_path` of Edit, Write and MultiEdit, the `notebook_path` of NotebookEdit, and in a
|
|
44
|
+
Bash command `cd <absolute path>` and `git -C <path>` (absolute, or relative to the session's folder), split only in
|
|
45
|
+
commands that hold `cd` or `git`, at most 64 KB of command text a turn. These are the repositories the turn
|
|
46
|
+
*touched*: a `cd` or a read-only `git -C <path> log` names one as much as an edit does. A subagent's entries are not
|
|
47
|
+
read, so a repository only a subagent touched is not named. Each
|
|
48
|
+
path is resolved with one `git rev-parse --show-toplevel` and its repository's `origin` with one `git config --get
|
|
49
|
+
remote.origin.url`, every call inside the same 1.5 s as the turn's facts (the facts first); a path outside every
|
|
50
|
+
checkout costs no git call, and when the budget runs out the hook spools what it found. Each repository is named
|
|
51
|
+
`{host, owner, name}` from its `origin` (https, `ssh://`, `git@host:owner/name`; credentials, a port and `.git`
|
|
52
|
+
dropped; a nested group stays in `owner`, and on a self-hosted ssh remote `owner` is the folder path on that server
|
|
53
|
+
before the repository's name, which may hold a user name). The repository of the session's folder when the turn ends (the Stop payload's `cwd`; any checkout of it: a worktree, a clone), a repository whose `origin`
|
|
54
|
+
is missing or does not parse, and a second checkout of one repository are not named; at most 10. No local path leaves the hook; a
|
|
55
|
+
missing or unreadable transcript names none and is no error.
|
|
56
|
+
|
|
34
57
|
**A project that uses Fabric** (`project_uses_fabric`), cheapest sign first: `fabric` registered at local or project
|
|
35
58
|
scope for the folder (user scope is every folder's, so it says nothing about this one); a `fabric-agent.json`,
|
|
36
59
|
`fabric-contract.lock.json` or `fabric.package.json` file from the folder up to its checkout's root (the folder alone
|
|
@@ -104,14 +127,19 @@ only variable names are looked at, never a value (two of them carry the session'
|
|
|
104
127
|
## What they never do
|
|
105
128
|
|
|
106
129
|
- **Block or slow the session.** Each hook has a 5 s ceiling in `hooks.json`; the script reads a few small files and
|
|
107
|
-
returns in tens of milliseconds, plus the turn's git facts in Stop and SessionEnd (
|
|
108
|
-
git
|
|
130
|
+
returns in tens of milliseconds, plus the turn's git facts in Stop and SessionEnd and, in Stop once Fabric takes them (not on Windows), the transcript's
|
|
131
|
+
tail and the other repositories' names (the git calls share at most 1.5 s; the tail read is at most 512 KB). Every call to Fabric happens in a detached worker with a 10 s deadline; at its end the worker closes
|
|
109
132
|
`fabric-mcp`'s stdin and then ends its whole process group, so a wrapper (`npx`, a shell script,
|
|
110
133
|
`fabric-mcp.cmd`) cannot keep it waiting. Every hook exits 0, on every path.
|
|
111
|
-
- **
|
|
112
|
-
those. Nothing secret is in argv (the worker's argv is the script and a spool folder) or in the log.
|
|
113
|
-
|
|
114
|
-
|
|
134
|
+
- **Keep a secret.** The hooks never open Fabric's door token or an enrollment credential; `fabric-mcp` holds
|
|
135
|
+
those. Nothing secret is in argv (the worker's argv is the script and a spool folder) or in the log. Once Fabric
|
|
136
|
+
takes repositories (not on Windows), an `origin` URL that embeds a credential (`https://user:token@host/...`) is read by
|
|
137
|
+
`git config` with it; the Stop hook drops the credential in memory before anything is spooled, logged or sent.
|
|
138
|
+
- **Send content, or use more than it reads for.** A turn is the HEAD commit, the number of files that differ from it
|
|
139
|
+
(untracked included) and the lines added and removed against it, and, once Fabric takes them (not on Windows), the other repositories
|
|
140
|
+
it touched by their `origin` name (`{host, owner, name}`); no path, branch name, commit message or diff text. The
|
|
141
|
+
transcript's tail is read, message text and tool output with it, and only tool names and path arguments are used; a sent turn's log
|
|
142
|
+
line counts its repositories, never names them. The board is read
|
|
115
143
|
as a count (`com.status`), never a message. Git runs with `--no-optional-locks`.
|
|
116
144
|
- **Start a second Fabric session.** A `fabric-mcp` started on its own would enroll a new session, and Fabric ends a
|
|
117
145
|
binding's oldest live session past eight (ADR-0134 amendment 2): the agent's own. The worker and `watch-mail` run
|
|
@@ -129,7 +157,8 @@ only variable names are looked at, never a value (two of them carry the session'
|
|
|
129
157
|
One line per event that did something, never content: `~/Library/Caches/ai.passioncode.fabric-agent-adapter/hooks.log`
|
|
130
158
|
(macOS), `$XDG_CACHE_HOME/fabric-agent-adapter/hooks.log` (Linux, default `~/.cache`),
|
|
131
159
|
`%LOCALAPPDATA%\fabric-agent-adapter\hooks.log` (Windows). Mode 0600, rotated at 1 MB to `hooks.log.1`. The
|
|
132
|
-
per-session spool is `sessions/<session id>/` beside it (turn entries capped at 50,
|
|
160
|
+
per-session spool is `sessions/<session id>/` beside it (turn entries capped at 50, each holding the session's folder when
|
|
161
|
+
the turn ended (the Stop payload's `cwd`) and, when the turn touched another repository (only once Fabric takes them (not on Windows)), `repositories` as `{host, owner, name}`; `board.json`, `board.shown`, `repositories.degraded` (the once-a-session note that repositories were left out: the schema no longer takes them, or refused every item), and the nudges not yet printed: `nudges.json` for the prompt
|
|
133
162
|
hook, `nudges.watch.json` for `watch-mail`, each taken by renaming it away; a
|
|
134
163
|
folder untouched for 7 days, whose worker failed or whose session ended without SessionEnd, is removed by the next
|
|
135
164
|
Stop or SessionEnd hook, even while the hooks are inert; an end entry whose release or observation got no answer stays
|
|
@@ -151,6 +180,6 @@ launched-session check exact.
|
|
|
151
180
|
| D1 | `fabric-mcp` enrolls with the runtime session id | for `--runner claude-code`, `runtimeSessionId` = env `CLAUDE_CODE_SESSION_ID` in `fabric.session.enroll {cwd, runtimeSessionId}` (Claude Code 2.1.296 gives MCP stdio servers and Bash commands that variable, equal to the hooks' `session_id`) |
|
|
152
181
|
| D2 | rejoin only | `fabric-mcp --runner claude-code --rejoin-only --runtime-session-id <id>` attaches to that runtime session's live enrolled session; never asks, never mints; otherwise only `fabric_whoami`, as not enrolled; Fabric 0.3.9 exits 3 at once when no session holds the id |
|
|
153
182
|
| D3 | the flag | `hub.json` `"features": ["session-hooks/1"]` once D1, D2, D4 and D5 ship |
|
|
154
|
-
| D4 | `fabric_session_observe` | `{event: "turn" or "end", runtime: "claude-code", runtimeSessionId, observedAt, head, filesChanged, linesAdded, linesRemoved, reason (end only), idempotencyKey}` → `session.turn.observed@1`, `session.ended@1`; from Fabric 0.3.14 a turn's answer is `{recorded, seq, nudges?}`, each nudge `{kind, says, task_id?, request_id?}` shown on a turn once, at most five, the same on a repeated turn, and kept for `fabric_work_list` and `fabric_heartbeat` (FAA-142) |
|
|
183
|
+
| D4 | `fabric_session_observe` | `{event: "turn" or "end", runtime: "claude-code", runtimeSessionId, observedAt, head, filesChanged, linesAdded, linesRemoved, reason (end only), repositories (turn only, optional), idempotencyKey}` → `session.turn.observed@1`, `session.ended@1`; `repositories` (FAA-140, proposed to Fabric on `a8bdda7a`, accepted on `d3f9e6fb`) is at most 10 `{host, owner, name}` and is sent only where the tool's input schema lists it with items carrying host, owner and name, so a Fabric without it gets the call it gets today; from Fabric 0.3.14 a turn's answer is `{recorded, seq, nudges?}`, each nudge `{kind, says, task_id?, request_id?}` shown on a turn once, at most five, the same on a repeated turn, and kept for `fabric_work_list` and `fabric_heartbeat` (FAA-142) |
|
|
155
184
|
| D5 | release a whole session | `fabric_work_release {claims: "session", outcome: "let-go", reason, idempotencyKey}`; `observedAt` too where its input schema names it (Fabric 0.3.9: `stale-end` for an end observed before the session enrolled) |
|
|
156
185
|
| D6 | a launched-session marker | `FABRIC_LAUNCHED_SESSION=1` in the environment of every session Fabric launches, whatever its adapter; until then, the inference above |
|
package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py
CHANGED
|
@@ -76,6 +76,8 @@ from fabric_session_env import ( # noqa: F401
|
|
|
76
76
|
spawn_worker,
|
|
77
77
|
_remove_tree,
|
|
78
78
|
NO_SESSION_MARK,
|
|
79
|
+
REPOSITORIES_MARK,
|
|
80
|
+
repositories_wanted,
|
|
79
81
|
NO_SESSION_HOLD_S,
|
|
80
82
|
no_session_held,
|
|
81
83
|
)
|
|
@@ -100,6 +102,15 @@ from fabric_session_mcp import ( # noqa: F401
|
|
|
100
102
|
_wait_for_session_tools,
|
|
101
103
|
NO_SESSION_EXIT,
|
|
102
104
|
NoSessionError,
|
|
105
|
+
TRANSCRIPT_TAIL_BYTES,
|
|
106
|
+
MAX_REPOSITORIES,
|
|
107
|
+
BASH_BYTES,
|
|
108
|
+
_takes_repositories,
|
|
109
|
+
parse_origin,
|
|
110
|
+
bash_paths,
|
|
111
|
+
turn_paths,
|
|
112
|
+
turn_repositories,
|
|
113
|
+
stored_repositories,
|
|
103
114
|
)
|
|
104
115
|
from fabric_session_mail import ( # noqa: F401
|
|
105
116
|
WATCH_MIN_INTERVAL_S,
|
|
@@ -36,6 +36,7 @@ from fabric_session_env import ( # noqa: E402 - the kit (FAA-65)
|
|
|
36
36
|
prune_stale_sessions,
|
|
37
37
|
read_small,
|
|
38
38
|
rejoined_recently,
|
|
39
|
+
repositories_wanted,
|
|
39
40
|
session_dir,
|
|
40
41
|
spawn_worker,
|
|
41
42
|
spool,
|
|
@@ -44,6 +45,7 @@ from fabric_session_mcp import ( # noqa: E402 - the kit (FAA-65)
|
|
|
44
45
|
FACTS_BUDGET_S,
|
|
45
46
|
deliver,
|
|
46
47
|
git_facts,
|
|
48
|
+
turn_repositories,
|
|
47
49
|
)
|
|
48
50
|
from fabric_session_mail import ( # noqa: E402 - the kit (FAA-65)
|
|
49
51
|
watch_mail_cli,
|
|
@@ -164,7 +166,8 @@ def hook_user_prompt_submit(payload: Dict[str, Any], env: Optional[Dict[str, str
|
|
|
164
166
|
|
|
165
167
|
|
|
166
168
|
def hook_spool(payload: Dict[str, Any], event: str, env: Optional[Dict[str, str]] = None, spawn=spawn_worker) -> str:
|
|
167
|
-
"""Stop (`turn`) and SessionEnd (`end`): spool one entry, with the turn's facts measured now
|
|
169
|
+
"""Stop (`turn`) and SessionEnd (`end`): spool one entry, with the turn's facts measured now (and a turn's other
|
|
170
|
+
repositories, FAA-140), and start the worker.
|
|
168
171
|
Returns what happened, for the tests. Nothing is spooled and no worker runs unless the running Fabric advertises
|
|
169
172
|
`session-hooks/1` and `fabric` is registered: hub.json is read first because it is the cheap check that fails for
|
|
170
173
|
every Fabric before the one that ships the hooks' dependency (`status` says so: "Session hooks: inert"). Stale
|
|
@@ -213,10 +216,16 @@ def hook_spool(payload: Dict[str, Any], event: str, env: Optional[Dict[str, str]
|
|
|
213
216
|
directory.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
214
217
|
_write_private(directory / LAUNCHED_MARK, "")
|
|
215
218
|
return "skipped: launched by Fabric"
|
|
216
|
-
# The turn's own facts, measured when it ended (counts and ids only, never a path), not when a worker runs.
|
|
219
|
+
# The turn's own facts, measured when it ended (counts and ids only, never a path), not when a worker runs. The
|
|
220
|
+
# other repositories the turn changed (FAA-140) share the same budget: git_facts first, then what time is left.
|
|
221
|
+
until = time.monotonic() + FACTS_BUDGET_S
|
|
217
222
|
entry = {"event": event, "runtimeSessionId": session_id, "cwd": str(cwd),
|
|
218
223
|
"observedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
219
|
-
"facts": git_facts(cwd,
|
|
224
|
+
"facts": git_facts(cwd, until=until)}
|
|
225
|
+
if event == "turn" and repositories_wanted(env): # only once Fabric takes them: no transcript read before (FAA-140)
|
|
226
|
+
repositories = turn_repositories(payload.get("transcript_path"), cwd, until)
|
|
227
|
+
if repositories:
|
|
228
|
+
entry["repositories"] = repositories
|
|
220
229
|
if event == "end" and isinstance(payload.get("reason"), str):
|
|
221
230
|
entry["reason"] = payload["reason"][:64]
|
|
222
231
|
spool(directory, entry)
|
|
@@ -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.
|
|
61
|
+
VERSION = "0.53.1"
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
HOSTS = {"claude": "Claude Code", "codex": "Codex", "kimi": "Kimi Code",
|
|
@@ -476,6 +476,10 @@ LAUNCHED_MARK = "launched"
|
|
|
476
476
|
#: (Fabric 0.3.9, TR-11). For NO_SESSION_HOLD_S the hooks spool nothing for that id; after it, one rejoin tries again,
|
|
477
477
|
#: so a Fabric that restarted and enrolled the id again is found (board thread 02bf19c5, ask 4).
|
|
478
478
|
NO_SESSION_MARK = "no-session"
|
|
479
|
+
#: The file in the hooks' own folder (state_root) that says the running Fabric's fabric_session_observe takes
|
|
480
|
+
#: `repositories` as {host, owner, name}: a worker writes it from the tool's schema and removes it when the schema stops
|
|
481
|
+
#: saying so. Until it exists the Stop hook reads no transcript (FAA-140, seam and product readings 1).
|
|
482
|
+
REPOSITORIES_MARK = "observe-takes-repositories"
|
|
479
483
|
NO_SESSION_HOLD_S = 15 * 60.0
|
|
480
484
|
|
|
481
485
|
|
|
@@ -527,6 +531,12 @@ def _write_private(path: Path, text: str) -> None:
|
|
|
527
531
|
os.replace(str(tmp), str(path))
|
|
528
532
|
|
|
529
533
|
|
|
534
|
+
def repositories_wanted(env: Optional[Dict[str, str]] = None) -> bool:
|
|
535
|
+
"""Whether the Stop hook reads the transcript for the turn's other repositories (FAA-140): only once a worker
|
|
536
|
+
left the mark, and never on Windows, where a Bash command's paths are not this host's (FAA-175)."""
|
|
537
|
+
return sys.platform != "win32" and (state_root(env=env) / REPOSITORIES_MARK).exists()
|
|
538
|
+
|
|
539
|
+
|
|
530
540
|
def session_dir(session_id: str, env: Optional[Dict[str, str]] = None) -> Path:
|
|
531
541
|
return state_root(env=env) / "sessions" / session_id
|
|
532
542
|
|