@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.
Files changed (27) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/family.json +1 -1
  3. package/package.json +1 -1
  4. package/payload/.claude-plugin/marketplace.json +2 -2
  5. package/payload/manifest.json +8 -8
  6. package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  7. package/payload/plugins/fabric-agent-adapter/hooks/hooks.json +1 -1
  8. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
  9. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +7 -3
  10. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +82 -11
  11. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +2 -1
  12. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +1 -1
  13. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +33 -3
  14. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +146 -7
  15. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +34 -3
  16. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +149 -45
  17. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +181 -5
  18. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +13 -1
  19. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
  20. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +15 -10
  21. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +4 -1
  22. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +38 -9
  23. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +11 -0
  24. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +12 -3
  25. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +11 -1
  26. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +358 -14
  27. 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).start()
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.52.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.52.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 the Fabric release that shows agents' goals, it is shown as yours. File each piece you split off with
145
- `fabric_task_create` while that task is claimed. Unless the answer says `chained: true` with `chained_to` your
146
- claimed task (Fabric to 0.3.14 links only a session it launched for a task, never one opened from its own
147
- terminal), link it yourself with `fabric_task_link {taskId: <the piece>, rel: "spawned", targetTaskId: <the
148
- claimed task>}` (board `c42071bc`). An answer with `chained: false` names its `chain_reason`: link it yourself once,
149
- and if that is refused too, tell the person its reason; never file the piece again. `proposed: true` means nothing
150
- was filed, so there is nothing to link: report it and stop, the piece waits for the operator.
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; 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
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
 
@@ -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. Never resend it under
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.
@@ -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 (at most 1.5 s, shared by every
108
- git call). Every call to Fabric happens in a detached worker with a 10 s deadline; at its end the worker closes
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
- - **Read a secret.** The hooks never open Fabric's door token or an enrollment credential; `fabric-mcp` holds
112
- those. Nothing secret is in argv (the worker's argv is the script and a spool folder) or in the log.
113
- - **Send or read content.** A turn is the HEAD commit, the number of files that differ from it (untracked included)
114
- and the lines added and removed against it; no path, branch name, commit message or diff text. The board is read
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, `board.json`, `board.shown`, and the nudges not yet printed: `nudges.json` for the prompt
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 |
@@ -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, and start the worker.
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, budget_s=FACTS_BUDGET_S)}
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.52.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