@passioncode-ai/fabric-agent-adapter 0.62.0 → 0.64.0

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 (31) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +9 -5
  3. package/SECURITY.md +6 -1
  4. package/package.json +1 -1
  5. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  6. package/plugins/fabric-agent-adapter/hooks/hooks.json +13 -1
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +6 -1
  8. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +1 -1
  9. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +48 -7
  10. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/channel-agent.example.json +103 -0
  11. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +1 -0
  12. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +141 -17
  13. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +2 -2
  14. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py +2 -0
  15. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +2 -2
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +1 -1
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +183 -3
  18. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +4 -3
  19. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-that-answer-people.md +5 -2
  20. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/practices.md +52 -0
  21. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +2 -1
  22. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +21 -20
  23. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/native-host-hooks.md +7 -1
  24. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +209 -0
  25. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +34 -6
  26. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +39 -6
  27. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +4 -0
  28. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +455 -9
  29. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +35 -6
  30. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_hosts.py +4 -1
  31. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py +8 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,141 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.64.0 (2026-11-05)
4
+
5
+ ### Added
6
+
7
+ - working-with-fabric: the retro after work (FAA-206 part 1; Fabric's obligation `retro-after-work`, its
8
+ `fabric_whoami` since `e1eecec0`, ADR-0188 §5). A Claude Code `PostToolUse` hook on Fabric's release tools adds one
9
+ line of context when a claim is released as done (`fabric_work_release` with `how` or `outcome` done, or the older
10
+ `fabric_task_release` with `outcome` succeeded): write the retro in this turn, `observatory_lesson` with one class
11
+ key per lesson for what went wrong, insight/1 lines for what was learned about the product, or one line that there
12
+ is nothing, no values in either (without `observatory_lesson`, in the turn's answer). The ending is read as Fabric
13
+ reads it (`how`, then `outcome`). Any other release, any other tool, a release Fabric refused (its answer says
14
+ `released: false`, which is how Fabric refuses one, `already-released`, or carries `isError`), a session release
15
+ that released nothing (`claims: []`) and a repeat (Fabric answers it with the first release's `at`) print nothing; an answer the hook cannot read keeps the line. Fabric's
16
+ `retro-after-work` is on its `main` (`e1eecec0`) and in no release yet, so neither the line nor the preamble sends
17
+ the agent to `fabric_whoami` for it. It says it in a session Fabric
18
+ launched too, the one hook that speaks there besides a dispatched run's exit contract and the prompt hook's verdict on it. No hook can speak to the
19
+ model when a session ends, so the session-start preamble names the retro itself (it does not rely on `fabric_whoami`, since no released Fabric carries `retro-after-work` yet) for the end of the work or the session.
20
+ Measured through the real CLI under a load average of about 68: median 155 ms a call against 50 ms for a bare
21
+ `python3 -S`, 1006 ms at most over 20 calls ([raw](docs/reports/2026-11-05-faa-206-retro/raw/hook-timing.log)).
22
+ - creating-fabric-agents and adapting-projects-to-fabric: an agent built or adapted with the kit closes each piece of
23
+ work with the same retro (practices, *Reliability*).
24
+
25
+ ### Fixed
26
+
27
+ - A dispatched run's exit contract (step 7) and `references/on-demand-session.md` no longer say `fabric_whoami` asks
28
+ the retro: no released Fabric carries `retro-after-work` (0.63.0 shipped that wording).
29
+ - The session hooks read their input as UTF-8 bytes, so a hook payload in Cyrillic (a release's note) is no longer
30
+ dropped on Windows before Python 3.15, where the text stream decodes with the locale's code page.
31
+
32
+ ### Upgrading
33
+
34
+ - The plugin's `hooks.json` gains a `PostToolUse` entry whose matcher names Fabric's two release tools on the `fabric`
35
+ server (or a plugin's `plugin_<name>_fabric`) only; nothing to configure. Codex and Kimi get no post-tool hook from `install-host-hooks` in this release.
36
+ - working-with-fabric SKILL.md, to stay within its word budget, no longer repeats five things its
37
+ `references/session-hooks.md` keeps (README keeps the first three too, SKILL-CARD and SECURITY.md the first two): that the
38
+ transcript's tail can hold a secret the session displayed, the off switch `FABRIC_SESSION_NO_REPOSITORIES=1`, that
39
+ session start ignores compact and resume, how a reminder Fabric gave is tagged, and where a turn `watch-mail`
40
+ delivered prints it.
41
+
42
+ ## 0.63.0 (2026-11-05)
43
+
44
+ ### Added
45
+
46
+ - working-with-fabric: a session Fabric started on a board request (FAA-201, Fabric's on-demand spec at `1329b60f`).
47
+ A session is the run only when its own prompt's first line `fabric-dispatch/1 workflow=wf_<16 hex> handoff=<id>
48
+ request=<uuid>` names the same three as `FABRIC_DISPATCH_WORKFLOW`, `FABRIC_DISPATCH_HANDOFF` and
49
+ `FABRIC_DISPATCH_REQUEST` (Switchboard's `dispatch` sets all three and writes the line, SB-247, released in
50
+ Switchboard 0.7.8 on 2026-11-04); the environment alone makes no run, since a reader or a subshell inherits it, and a
51
+ line that disagrees, or an environment without all three, makes none either: that session says so in the request's
52
+ thread and stops (Fabric's spec 3.5 on its main at `deaf51fe`, agreed on the project board, 2026-11-05). A line with no workflow in the
53
+ environment is quoted text. The session-start hook, which sees no prompt yet, prints the exit contract on that
54
+ condition in place of the preamble (naming the session's own `sessionId` when it is a UUID, and after a `/clear`
55
+ the session id the clear replaced, which a replayed accept needs for its `leaseId` and a pending handoff is accepted
56
+ under, or the new id when the replaced one is unknown), a retro before the run ends, also in a session
57
+ Fabric launched: `fabric_whoami`, the packet
58
+ (`observatory_checkpoint_latest`), the request and its thread (`com.get`, `com.list`), the lease
59
+ (`observatory_handoff_accept` with its owner, key and the executor the launcher names), `com.status` and the claim,
60
+ a request an earlier run left accepted or in progress (`com.responder_replace`, `com.reconcile`), in_progress
61
+ checkpoints, renewals about every 20 s (work that outlasts the run gives its state in a `com.progress` note and is left accepted for the next run,
62
+ which Fabric's dispatcher starts since its main at `fff4b6d9`, in no release yet, Fabric itself moving the open
63
+ Observatory workflow to that run, as its spec at `c560d97e` says (sections 3.4 and 3.6; its code does not yet); when the request's deadline is not after
64
+ run-ends-at plus 10 minutes, since no next run starts that close to it (`RUN_END_TO_DEADLINE_MS` on Fabric's main at
65
+ `afe28a60`, in no release yet), it fails known instead), the reply and `com.complete`, and the closing checkpoint with its
66
+ `workflowId` (`done`, or for `failed_known` `failed` once Observatory takes it, OBS-323, until then `blocked` with
67
+ the reason). The prompt hook, which sees the line, tells the session it is the run (or, for a line that disagrees,
68
+ that it is none), and prints no unread line or nudge in such a session. Only the
69
+ first session whose line names the dispatch holds it: another one gets one line
70
+ telling it not to accept, claim, complete or close (the dispatch is told by its `FABRIC_DISPATCH_ID` when the
71
+ launcher sets one in the form `[A-Za-z0-9._-]{1,64}`, as Switchboard 0.7.8 does), and a `/clear` in the holding session keeps the contract (the
72
+ hooks keep one record per run under `dispatch/`, pruned after 7 days by the Claude Code turn and end hooks and by the next session start on a board request). Every
73
+ checkpoint the session writes repeats the goal, constraints, artifacts and credentials of the packet's
74
+ `checkpoint.body`, since Observatory keeps only the latest body; the session takes the request from the environment
75
+ or the first line and its thread from `com.get`, since Observatory's write redacts the packet's board references. A
76
+ Codex run gets the hook only where its `CODEX_HOME` trusts it (Switchboard's isolated homes carry the person's trust since 0.7.8); a host whose hook gets no session id (the native bridges) is always told
77
+ the conditional contract and writes no record; on Windows, where each hook runs under a new Git Bash, a `/clear` holds
78
+ nothing (it cannot tell the holder from a helper), so the cleared session is told it runs inside a run that holds it,
79
+ stays a helper and Fabric takes the workflow over after the run. On any system, a `/clear` while another session
80
+ holds the dispatch (a nested session's clear) is told the same.
81
+ `watch-mail` refuses to start in a dispatched session, and the dashboard-kit notice says nothing there. `references/on-demand-session.md` holds the steps and every argument; the
82
+ hooks tell a dispatched session from this version on.
83
+ - working-with-fabric: whatever waits on the Project is answered in the same turn (FAA-205, the operator's rule of
84
+ 2026-11-04, board thread `11646774`): the session-start preamble, the unread line (`N unread on the project board:
85
+ com.list; answer what waits on you in this turn: ...`, its prefix unchanged for watchers) and the skill's board step
86
+ say to answer a request, an announcement or a question with its state now (done, where, when, what blocks) and send
87
+ the result later in a second message; `references/project-board.md` gains "Answering at once".
88
+ - The owner's word is done the first time (FAA-207, the operator's rule of 2026-11-04 relayed on the board, thread
89
+ `a7a9d05d`; the contract's CT-220): working-with-fabric's Step 5 and creating-fabric-agents' practices say the agent
90
+ does it itself, hands the person no command to run, asks one short question on a risk and then acts, and passes on
91
+ only a human gate with its reason; money limits and secrets stay.
92
+ - The channel-agent standard v1 at its round 4 (FAA-197): creating-fabric-agents' practices gain a section of the
93
+ standard's rules for agents on a channel people write to (the ticket as the unit, one outcome per ticket, answer
94
+ rather than route, repair before hand-off, declared sources with a maximum age, hand-off queues with their answered
95
+ signal, text from a channel as data (the contract's proposed CA-8), metadata only, the declaration's form, with a
96
+ holding reply owing a final text, FAC-SEM-164). The support-agent recipe:
97
+ - a hand-off its checks made carries the refused `draft` in the returned decision and to an `on_handoff` the agent
98
+ passes, never into a file (`decisions.jsonl` or the default outbox), so a repeat of the id returns it without; `answer(new_id, text, repair=decision)` rewrites it once with the refusal's reasons: a pass is
99
+ a reply with `repaired: true`, and a repair that ends otherwise (a second refusal, a model `HANDOFF`, no knowledge on
100
+ the retry; not a `model:*` outage) a hand-off with `fallback: true` (the standard's 4.4 and its ticket's
101
+ `fallback`), which `repair=` then refuses, as it refuses a repair under the refused decision's own message id;
102
+ - `KnowledgeSource` (or `knowledgeMaxAgeSeconds` in the config, which reads the file at once) reads the knowledge
103
+ file as a source: read again when it changes (its time, size, inode or status-change time, the last a creation time on Windows), the last good version kept when a read fails or the file has the
104
+ wrong shape (nested too deep included), `state()` with the age of that version, from its file's modification
105
+ time;
106
+ - `Knowledge.load` refuses a file that is not JSON, one nested deeper than 32 levels (on every Python), a file that is not an object with an `entries` list, an entry whose tags are not a list
107
+ of text or whose question is not text, each as a `RecipeError`;
108
+ - `channel-agent.example.json`, one project's declaration under
109
+ `https://fabric.passioncode.ai/agent-contract/extensions/channel-agent/0.1` in the round 4 form, held to it and to
110
+ the example config's maximum age by `test/test_channel_agent_example.py`.
111
+
112
+ ### Upgrading
113
+
114
+ - A recopied `support_agent.py` refuses at start (exit 1, a `RecipeError` naming the entry or the file's shape) a knowledge file 0.62.0
115
+ took in other ways, so an agent that ran on 0.62.0 can stop at start now. Per shape, 0.62.0: a file that is not an
116
+ object with an `entries` list (`[]`, `{}`, a string) loaded as an empty set and handed every message off as
117
+ `no-knowledge`; `{"entries": null}` or `{"entries": <number>}` stopped it at start with a `TypeError`; an entry with `tags: null`
118
+ failed each message with a `TypeError`; an entry whose `tags` is a string or holds something other than text, or
119
+ whose `question` is not text, was read and answered from. A file nested deeper than 32 levels, which 0.62.0
120
+ loaded, is refused too. Fix the file before upgrading.
121
+ - A custom `on_handoff` that stores the row it is given now receives the refused `draft`, which can echo what the
122
+ customer wrote: drop `draft` before storing it (the default outbox does).
123
+ - A session carrying `FABRIC_DISPATCH_WORKFLOW` now gets the exit contract at session start, on the condition that its
124
+ own prompt starts with the dispatch line naming the same three, even when Fabric launched it; with the workflow
125
+ set but the handoff or the request missing or malformed, it gets one line that no session there is a run. Its prompt hook prints no unread line or reminder (the nudges file
126
+ stays), and `watch-mail` refuses to start there. A launcher that sets the workflow alone starts no run any more.
127
+ - The live docs name a closed module neutrally, as the organization edition (`references/package.md` in
128
+ building-fabric-services and two backlog rows), at its owner's request; dated reports and handoffs keep their words.
129
+ - A session that does a board request with no person in its chat stops before a production step and names it for a
130
+ person, unless the operator's own words, relayed verbatim with their date and source from one of his own projects (provenance `trusted`) and with a scope naming the step, approve it (working-with-fabric `references/project-board.md`; board threads `1add3c03` and `24e0c88e`; Fabric's board rule at `108239c0`).
131
+
132
+ ### Fixed
133
+
134
+ - Every annotation in the shipped scripts (the kit, the skills' scripts, the recipe and the Hermes adapter) resolves
135
+ with `typing.get_type_hints` (FAA-203, a board finding, thread `9fd84d47`): `fabric_estate.py` and four working-with-fabric modules import the `typing` names they use, and
136
+ `LoginCodes`' `token` is written `Optional[Union[str, Callable[[], str]]]`, which Python 3.9 reads. A vendored copy
137
+ of `fabric_estate.py` or `fabric_auth.py` reads as older; it worked at run time before.
138
+
3
139
  ## 0.62.0 (2026-11-04)
4
140
 
5
141
  A new install names the repository of its code, and a service says what it uses, so a map of the installed agents can
package/README.md CHANGED
@@ -23,7 +23,8 @@ The skills are `adapting-projects-to-fabric`, `creating-fabric-agents`,
23
23
  `working-with-fabric` is for the other side: a coding agent (Claude Code, Codex, Kimi Code, Kilo, Hermes) opened in
24
24
  a terminal that works in a project through Fabric. It finds Fabric (`hub.json`, its `pid`), has the machine's
25
25
  lifecycle broker start it when it is down, registers `fabric-mcp` once with no secret, calls `fabric_whoami`
26
- first, reads the project board (`com.status`, `com.list`, `com.read_ack` only what was read) and asks other
26
+ first, reads the project board (`com.status`, `com.list`, `com.read_ack` only what was read), answers what waits on its
27
+ Project in the same turn with its state (the result later; a request a responder should take stays queued), and asks other
27
28
  Projects only through `com.submit`, asks who is working on what and claims its task before it starts
28
29
  (`fabric_work_list`, `fabric_work_claim`, Fabric v0.3.5 and later), links tasks to backlog rows instead of copying them, and
29
30
  releases at the end (`fabric_work_release`). `scripts/fabric_session.py status` prints where Fabric stands and one
@@ -33,16 +34,19 @@ next action:
33
34
  python3 plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py status
34
35
  ```
35
36
 
36
- In Claude Code the plugin adds four session hooks: SessionStart (at startup and `/clear`, never on compact or
37
+ In Claude Code the plugin adds five session hooks: SessionStart (at startup and `/clear`, never on compact or
37
38
  resume) puts that preamble into the session when the MCP server `fabric` is registered, Fabric runs and the project
38
39
  uses Fabric (one line pointing at `fabric_whoami` in a folder with no sign of it); when Fabric
39
40
  is down it adds one line (skip `fabric_whoami`, start Fabric through the broker if the task needs it), and only in a
40
- project that uses Fabric; Stop records each turn's facts (HEAD, files changed, lines added and removed, never content)
41
+ project that uses Fabric; in a session Fabric started on a board request (`FABRIC_DISPATCH_WORKFLOW` set) it prints the
42
+ exit contract instead, on the condition that the session's own prompt starts with the dispatch line naming the same
43
+ three, and UserPromptSubmit, which sees that line, confirms the run or says it is none (FAA-201); Stop records each turn's facts (HEAD, files changed, lines added and removed, never content)
41
44
  and the board's unread count, and, once the running Fabric takes them (unless `FABRIC_SESSION_NO_REPOSITORIES=1`), the other repositories the turn touched (it then
42
45
  reads the last 512 KB of the session transcript (message text and tool output included, which can hold a secret the session displayed; read in memory only), uses only tool names and path arguments, and sends host/owner/name, never a
43
46
  local path or text); UserPromptSubmit adds one line only when that count rose, and each reminder Fabric gave on a turn once (a turn `watch-mail` delivered prints there instead); SessionEnd releases what the
44
- session left claimed. Stop, UserPromptSubmit and SessionEnd spool and call Fabric only when the running Fabric
45
- advertises `session-hooks/1`; Stop and SessionEnd always prune their own stale spool folders. All are silent in a session Fabric launched, never block the session, read no Fabric token (an `origin` URL's embedded credential is dropped in memory, never
47
+ session left claimed. After a call to Fabric's release tools that releases a claim as done, PostToolUse adds one
48
+ line naming the retro (FAA-206; Fabric's `retro-after-work`, on its `main`, in no release yet). Stop, UserPromptSubmit and SessionEnd spool and call Fabric only when the running Fabric
49
+ advertises `session-hooks/1`; Stop and SessionEnd always prune their own stale spool folders. All are silent in a session Fabric launched (except the retro line after a release of finished work, and the exit contract in one started on a board request, where the prompt hook prints no unread line or reminder), never block the session, read no Fabric token (an `origin` URL's embedded credential is dropped in memory, never
46
50
  kept or sent) and always
47
51
  exit 0 once `python3` starts (on Windows, Claude Code runs hooks in Git Bash, or PowerShell without it, where `python3`
48
52
  must name Python 3.9 or later; FAA-183). `fabric_session.py watch-mail` waits for board mail under a background watcher, and `install-host-hooks
package/SECURITY.md CHANGED
@@ -32,6 +32,9 @@ bounded and non-publishing until explicit scoped grants exist in a project bindi
32
32
  external APIs the service's author configures, such as OpenRouter, with keys the service
33
33
  reads by name at run time; the kit itself calls no outside host on its own. A board
34
34
  responder talks only to Fabric's hub on loopback.
35
+ - The working-with-fabric PostToolUse hook (FAA-206) reads only the release call Claude Code gives it on stdin (the
36
+ tool's name, its input, which can carry the agent's note, and Fabric's answer), in memory: it writes, logs and sends
37
+ nothing and prints one fixed line or nothing.
35
38
  - The working-with-fabric session hooks run as the person: Stop and SessionEnd run `git`
36
39
  in the session's folder (counts only, at most 1.5 s; with the mark below, also that folder's own `origin`, only to leave its repository out), read the process table to tell a
37
40
  session Fabric launched. Stop also, only once the running Fabric's `fabric_session_observe` takes
@@ -47,7 +50,9 @@ bounded and non-publishing until explicit scoped grants exist in a project bindi
47
50
  remote, `owner` is the folder path on that server before the repository's name). The mark is
48
51
  removed only by a worker that sees a schema without the field, so after Fabric is downgraded
49
52
  the turns that end before a worker next reaches the running Fabric (about a minute; longer while
50
- rejoins fail) are still read; none of their names is sent. They write their spool and log under the user's cache folder (0600),
53
+ rejoins fail) are still read; none of their names is sent. They write their spool and log under the user's cache folder (0600), and, in a session
54
+ started on a board request (FAA-201), one record of which session holds that dispatch (the holding session's id and, after a `/clear`, the id it replaced, the agent's process id and a time, 0600,
55
+ pruned after 7 days),
51
56
  and the delivery worker starts `fabric-mcp` in its own process group and ends that group
52
57
  (`killpg`, or `taskkill /T /F` on Windows) at its 10 s deadline. They read no Fabric token. An
53
58
  `origin` URL that embeds a credential (`https://user:token@host/...`) is read by `git config`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@passioncode-ai/fabric-agent-adapter",
3
- "version": "0.62.0",
3
+ "version": "0.64.0",
4
4
  "description": "Create Fabric-compatible agents, run them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract. This package is the installer CLI.",
5
5
  "scripts": {
6
6
  "test": "python3 test/validate.py && python3 -m unittest discover -s test -q && node --test 'test/*.test.mjs'"
@@ -3,7 +3,7 @@
3
3
  "name": "fabric-agent-adapter",
4
4
  "displayName": "Fabric Agent Adapter",
5
5
  "description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
6
- "version": "0.62.0",
6
+ "version": "0.64.0",
7
7
  "author": {
8
8
  "name": "PassionCode.ai",
9
9
  "url": "https://passioncode.ai/"
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "Two silent-unless-relevant hook sets. The dashboard-kit notice: one line at session start or /clear in a repository that owns a Fabric service whose dashboard has no kit or an older one (fabric-dashboard/0.1, DEC-0036), phrased as an upgrade the agent may make itself. The Fabric session hooks (working-with-fabric, COM-04, FAA-12a): at session start or /clear, the preamble when Claude Code has the MCP server `fabric` and Fabric runs (fabric_whoami, then the project board through com.status and com.list, claim and release); when Fabric is down, one line, only in a project that uses Fabric (a Fabric contract file, a project-bound registration or a service descriptor naming the repository); nothing on compact or resume; each turn's facts (HEAD, files changed, lines added and removed, never content), the board's unread count, and, once the running Fabric's fabric_session_observe takes them (unless FABRIC_SESSION_NO_REPOSITORIES=1), the other repositories the turn touched (the last 512 KB of the session transcript, message text and tool output included (a secret the session displayed can be there; read in memory only), is then read and only the current turn's tool names and path arguments are used; host/owner/name leave the machine, never a local path or text) and the session's end through Fabric, only when the running Fabric advertises session-hooks/1; at each prompt one line only when the unread count rose, then each nudge a turn's answer carried, once (watch-mail prints those of turns it delivered). All silent in a session Fabric launched. Every hook reads no Fabric token (a credential embedded in an origin URL is read by git config and dropped in memory, never spooled or sent), prints at most one block, always exits 0 once python3 starts (on Windows it must name Python in Git Bash, FAA-183), and hands any Fabric call to a detached worker with a 10 s deadline.",
2
+ "description": "Two silent-unless-relevant hook sets. The dashboard-kit notice: one line at session start or /clear in a repository that owns a Fabric service whose dashboard has no kit or an older one (fabric-dashboard/0.1, DEC-0036), phrased as an upgrade the agent may make itself. The Fabric session hooks (working-with-fabric, COM-04, FAA-12a): at session start or /clear, the preamble when Claude Code has the MCP server `fabric` and Fabric runs (fabric_whoami, then the project board through com.status and com.list, claim and release); when Fabric is down, one line, only in a project that uses Fabric (a Fabric contract file, a project-bound registration or a service descriptor naming the repository); nothing on compact or resume; each turn's facts (HEAD, files changed, lines added and removed, never content), the board's unread count, and, once the running Fabric's fabric_session_observe takes them (unless FABRIC_SESSION_NO_REPOSITORIES=1), the other repositories the turn touched (the last 512 KB of the session transcript, message text and tool output included (a secret the session displayed can be there; read in memory only), is then read and only the current turn's tool names and path arguments are used; host/owner/name leave the machine, never a local path or text) and the session's end through Fabric, only when the running Fabric advertises session-hooks/1; at each prompt one line only when the unread count rose, then each nudge a turn's answer carried, once (watch-mail prints those of turns it delivered). All silent in a session Fabric launched but the retro line below. Every hook reads no Fabric token (a credential embedded in an origin URL is read by git config and dropped in memory, never spooled or sent), prints at most one block, always exits 0 once python3 starts (on Windows it must name Python in Git Bash, FAA-183), and hands any Fabric call to a detached worker with a 10 s deadline. In a session Fabric started on a board request (FABRIC_DISPATCH_WORKFLOW), session start prints the exit contract instead of the preamble on the condition that the session's own prompt starts with the dispatch line naming the same three, launched or not, and the prompt hook, which sees that line, confirms the run or says it is none, launched or not, with no unread line or nudge, and the dashboard-kit notice says nothing (FAA-201). After a call to Fabric's fabric_work_release or fabric_task_release that releases a claim as done, PostToolUse adds one line naming the retro (FAA-206); after any other call, or a release Fabric's answer refuses (released: false, already-released) or that released nothing, nothing.",
3
3
  "hooks": {
4
4
  "SessionStart": [
5
5
  {
@@ -55,6 +55,18 @@
55
55
  }
56
56
  ]
57
57
  }
58
+ ],
59
+ "PostToolUse": [
60
+ {
61
+ "matcher": "^mcp__(fabric|plugin_[A-Za-z0-9_-]+_fabric)__fabric_(work|task)_release$",
62
+ "hooks": [
63
+ {
64
+ "type": "command",
65
+ "command": "python3 -S \"${CLAUDE_PLUGIN_ROOT}/skills/working-with-fabric/scripts/fabric_session.py\" hook post-tool-use",
66
+ "timeout": 5
67
+ }
68
+ ]
69
+ }
58
70
  ]
59
71
  }
60
72
  }
@@ -5,7 +5,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
5
5
  compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode.
6
6
  metadata:
7
7
  author: PassionCode.ai
8
- version: "0.62.0"
8
+ version: "0.64.0"
9
9
  contract-version: "0.1.0"
10
10
  contract-commit: "3830a3353923e20f826d466610e1c479f120bec0"
11
11
  ---
@@ -248,6 +248,11 @@ shared reference above. An agent someone builds for themselves is not a PassionC
248
248
  its licence is its owner's choice and none of those files is required of it (an agent built new with
249
249
  creating-fabric-agents carries a `LICENSE` naming its holder; an existing one keeps what it has).
250
250
 
251
+ **An adapted agent closes each piece of work with a retro**, as Fabric's `retro-after-work` (on its `main`, in no release yet) asks every session:
252
+ `observatory_lesson` for what went wrong (one class key per lesson), insight/1 lines for what it learned about the
253
+ product, or one line that there is nothing; a resident service does it when a job ends, and without Project
254
+ Observatory it writes the lines to its own retrospective file (creating-fabric-agents practices, *Reliability*).
255
+
251
256
  ## Completion format
252
257
 
253
258
  Report:
@@ -14,7 +14,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
14
14
  compatibility: Python 3.9+ or Node.js 20+ for the kits (Node.js 22+ for a Linux service's instance lock); the probe needs Python 3.9+. launchd steps are macOS-only; Linux services install a systemd user unit and Windows ones a per-user Scheduled Task with the kits' writers. Dashboard handoff optionally uses Fabric Dashboards MCP link/host_status/open; without it, report unresolved host capability. The contract checkout is optional.
15
15
  metadata:
16
16
  author: PassionCode.ai
17
- version: "0.62.0"
17
+ version: "0.64.0"
18
18
  contract-version: "0.1.0"
19
19
  extension: "fabric-service/0.1"
20
20
  extension-commit: "3830a3353923e20f826d466610e1c479f120bec0"
@@ -22,9 +22,14 @@ service that calls it owns its channel (mail, a chat, a store review) and its re
22
22
  This recipe is the draft-and-check step of an agent that answers people, not that agent, and used alone it falls short
23
23
  of one. Every hand-off goes to `on_handoff`, by default the outbox a person reads, at the first refusal: the practices
24
24
  name that as the failing pattern (P2). An agent that repairs passes its own `on_handoff=` so a failed check reaches
25
- its repair step rather than a person; the recipe returns the failed checks by name in `reasons`, but takes no reason
26
- in and returns no refused draft yet, and it calls again under a new message id, since the stored decision answers any
27
- repeat of the same id. `on_handoff` runs under the agent's lock, before the decision is saved, so it queues the
25
+ its repair step rather than a person; a hand-off the checks made carries the failed checks in `reasons` and the
26
+ refused `draft`, and the repair step passes that decision back as `answer(new_id, text, repair=decision)`: the model
27
+ gets its draft and the reasons, and a pass comes back as a reply with `repaired: true`, a second refusal as a hand-off
28
+ with `fallback: true` (the channel-agent standard's 4.4 and its ticket's `fallback`, FAA-197). It calls under a new message id, since the stored decision answers any repeat of the same id (`repair=` under the
29
+ refused decision's own id is refused with a `RecipeError`); `repair=` takes only a hand-off the checks
30
+ made (not `no-knowledge`, `model-unsure` or `model:*`), and never a repeat: a repeat of the id returns the stored
31
+ decision, which has no draft, so the repair step keeps the decision it was handed and passes each first decision to
32
+ `repair=` once. A repair the model declines (`HANDOFF`) or that finds no knowledge is marked `fallback: true` too: it marks a spent repair; a `model:*` outage is not one. `on_handoff` runs under the agent's lock, before the decision is saved, so it queues the
28
33
  hand-off and returns: since 0.55.0 a call to `answer` from inside it, on the same thread, is refused with
29
34
  `ReentryError` (a `RecipeError`), and unless `on_handoff` catches it the first call raises it too, its draft paid and
30
35
  nothing saved or delivered. A call it hands to another thread and waits for waits for the lock forever, so it never
@@ -42,8 +47,8 @@ are not counted. The recipe hands off nothing for `policy`: the calling service
42
47
  handle (refunds, unbans, account recovery) before it calls the recipe, or a model's `HANDOFF` on them is counted as a
43
48
  false knowledge gap. Count its hand-offs by cause: a failed check is a refusal for the agent to repair, research and
44
49
  hold, and only a hold on the material counts as `inability` (a hold after an outage or at the research time cap is
45
- `infrastructure`); `unsupported:amount` is a money reason, never researched, and so is `unsupported:number` while the
46
- decision returns no draft to tell a price from another number; `no-knowledge` and `model-unsure` are `inability`;
50
+ `infrastructure`); `unsupported:amount` is a money reason, never researched, and so is `unsupported:number` unless the
51
+ refused `draft` shows the number is not a price; `no-knowledge` and `model-unsure` are `inability`;
47
52
  every `inability` opens a gap; `model:*` is `infrastructure`, and `model:ceiling` and `model:payment` are also the
48
53
  turn's `limit` outcome. The rest of the agent's parts with checks come from [agents that answer
49
54
  people](../../../creating-fabric-agents/references/agents-that-answer-people.md): the turn record, the daily
@@ -85,6 +90,34 @@ cp <adapter>/plugins/fabric-agent-adapter/skills/building-fabric-services/recipe
85
90
  python3 <adapter>/.../fabric_kit.py status <service>/support # current, older or modified, per file
86
91
  ```
87
92
 
93
+ ## The knowledge file as a source
94
+
95
+ The channel-agent standard declares every source with its owner, how it is read, its cadence and its maximum age, and a source that fails
96
+ keeps its last good version and says so (its 7.1 and 7.4). `KnowledgeSource(path, max_age_s=...)` is the knowledge
97
+ file read that way: it reads the file again when it changes, keeps the last good version when a read fails, and
98
+ `state()` gives `{id, age_s, max_age_s, stale, degraded}`. The age is the last good version's, from its file's
99
+ modification time: a file nobody rebuilt grows old however often it is read, and a restart does not make it new.
100
+ The standard ages a source from its last finished sync, so the knowledge build writes or touches the file at every
101
+ successful rebuild, unchanged or not, and never keeps the source's time (`cp -p`, `rsync -t`). Pass it as `knowledge=`, or set
102
+ `knowledgeMaxAgeSeconds` in the config, which reads the file at once (a file never read well fails there). The calling service shows `state()`
103
+ on its page and opens one alert when `stale` turns true: the recipe has no page and sends no alert itself.
104
+
105
+ ## Declaring the channel-agent standard
106
+
107
+ An agent that answers people on a channel declares what it follows under
108
+ `https://fabric.passioncode.ai/agent-contract/extensions/channel-agent/0.1` (the contract's proposed DEC-0150, not
109
+ merged yet, so the key and the form may change), in its `fabric-agent.json` or its
110
+ well-known `extensions` (the standard's 11 and 13.6; the doctrine: creating-fabric-agents
111
+ [practices](../../../creating-fabric-agents/references/practices.md#agents-on-a-channel-people-write-to)).
112
+ `channel-agent.example.json` is one project's declaration in the standard's round 4 form: a required member that is
113
+ not there yet reads `{"none": "not_yet", "until": "<date>"}`, one not used `{"none": "not_used"}`, never null except
114
+ where nothing is decided (a channel's `decision`); a final text never `not_used` beside a holding reply with replies
115
+ on (FAC-SEM-164); `recommendation` only with `answered_signal: "none"`, the id of the
116
+ board thread where the queue's owner was told; a channel's `items_seen_unit` one of `mails`, `updates`, `messages`,
117
+ `reviews`; durations in ISO 8601; the knowledge file as a source with its
118
+ maximum age. `test/test_channel_agent_example.py` holds the example to that form; `check_service.py` reads the
119
+ declaration once the contract publishes its schema.
120
+
88
121
  ## Configure
89
122
 
90
123
  `config.example.json`, paths relative to the config's folder:
@@ -92,6 +125,7 @@ python3 <adapter>/.../fabric_kit.py status <service>/support # current, old
92
125
  | Key | Meaning |
93
126
  |---|---|
94
127
  | `knowledge` | the knowledge file: `{"entries": [{"id", "question", "answer", "tags"}]}`, ids unique |
128
+ | `knowledgeMaxAgeSeconds` | optional: the knowledge file is then a source with an age and a last good version (above) |
95
129
  | `stateDir` | the decisions (`decisions.jsonl`), the handoff outbox (`handoffs.jsonl`) and the usage ledger (`usage.jsonl`) |
96
130
  | `model`, `fallback` | two models of different vendors; a lane that reads what customers write runs on an open-vendor model (DEC-0083) |
97
131
  | `keyEnv` | the environment variable that holds the agent's own OpenRouter key; never a value in the file |
@@ -111,7 +145,11 @@ the store's runner, which puts the named key into this one process's environment
111
145
  `python3 "$(project-observatory full-path)/tools/use_secret.py" run --vault-only <project> OPENROUTER_API_KEY --
112
146
  python3 support_agent.py answer ...`); a long-running
113
147
  service gets it the same way from its service manager. The decision
114
- prints as JSON: `{"messageId", "action": "reply"|"handoff", "reply"?, "citations", "reasons", "costUsd", "repeated"}`.
148
+ prints as JSON: `{"messageId", "action": "reply"|"handoff", "reply"?, "citations", "reasons", "costUsd", "repeated"}`,
149
+ with `"draft"` on a hand-off the checks made, `"repaired": true` on a reply after `repair=` and `"fallback": true` on
150
+ a repair that ended in a second refusal, a declined repair (`HANDOFF`) or no knowledge; a `model:*` outage during a
151
+ repair is a plain hand-off without a draft. Two meanings of the word: the config's `fallback` is the second model; a decision's `fallback: true`
152
+ is the channel-agent standard's fallback, a spent repair (a second refusal, a declined repair or no knowledge on the retry, never a `model:*` outage). `repair=` refuses a decision that is already `fallback: true`.
115
153
  Exit 1 on a broken config or knowledge file. A missing key is not a crash: the message is handed off with
116
154
  `model:config`.
117
155
 
@@ -126,7 +164,10 @@ Every handoff row in `handoffs.jsonl` holds the customer's own message, which ca
126
164
  number or a card number. It stays until it is pruned. Choose a retention the product's privacy policy states, at most
127
165
  30 days for an agent that answers people (the standard's A1). A1 also asks for the text redacted: the outbox keeps it
128
166
  as written, and `prune` takes any `--keep-days`, so such an agent passes an `on_handoff` that redacts what it stores,
129
- which replaces the outbox, and keeps 30 days or fewer. Prune on a schedule:
167
+ which replaces the outbox, and keeps 30 days or fewer. A refused `draft`, which can echo what the customer wrote,
168
+ reaches the returned decision and an `on_handoff` the agent passes, never a file: neither `decisions.jsonl` nor the
169
+ default outbox stores it. A capability handler returns the decision without it: a kit job's result is written to
170
+ the job's file (`fabric_interop`'s job store), so pass `draft` only to the repair step. Prune on a schedule:
130
171
 
131
172
  ```bash
132
173
  python3 support_agent.py prune --config config.json --keep-days 30
@@ -0,0 +1,103 @@
1
+ {
2
+ "https://fabric.passioncode.ai/agent-contract/extensions/channel-agent/0.1": {
3
+ "standard": "channel-agent-standard/1",
4
+ "projects": [
5
+ {
6
+ "project": "project:example-shop",
7
+ "side": "example-side",
8
+ "lead_agent": "example-agent",
9
+ "adopted_at": "2026-11-10",
10
+ "taxonomy": {
11
+ "version": "2026-11-04",
12
+ "codes": [
13
+ {
14
+ "code": "orders.status",
15
+ "label": "Where is my order"
16
+ },
17
+ {
18
+ "code": "payments.refund",
19
+ "label": "A refund"
20
+ }
21
+ ]
22
+ },
23
+ "tiers": [
24
+ {
25
+ "code": "premium",
26
+ "label": "Premium",
27
+ "rule": "open_orders > 10",
28
+ "escalate": "at_once"
29
+ }
30
+ ],
31
+ "question_route": {
32
+ "capability": "operator.ask",
33
+ "target": "00000000-0000-4000-8000-000000000001",
34
+ "daily_cap": 10,
35
+ "days": "mon-fri",
36
+ "tz": "Europe/Warsaw",
37
+ "send_at": "09:00",
38
+ "expires": "P7D"
39
+ },
40
+ "gap_registry": {
41
+ "none": "not_yet",
42
+ "until": "2026-11-30"
43
+ },
44
+ "channels": [
45
+ {
46
+ "id": "mail",
47
+ "kind": "mail",
48
+ "replies_enabled": true,
49
+ "decision": null,
50
+ "final_limit": "P7D",
51
+ "first_reply_limit": "PT24H",
52
+ "holding_reply": true,
53
+ "clarifying_question": true,
54
+ "quiet_window": null,
55
+ "late_answer_window": null,
56
+ "items_seen_unit": "mails",
57
+ "texts": {
58
+ "holding": {
59
+ "en": "Thank you, we are looking into it and will answer here."
60
+ },
61
+ "final": {
62
+ "en": "Here is the answer to your question. Write again if anything is still unclear."
63
+ }
64
+ }
65
+ }
66
+ ],
67
+ "sources": [
68
+ {
69
+ "id": "knowledge",
70
+ "owner": "example-agent",
71
+ "read": "file",
72
+ "cadence": "PT1H",
73
+ "max_age": "PT2H"
74
+ }
75
+ ],
76
+ "declined_sources": [],
77
+ "handoff_queues": [
78
+ {
79
+ "id": "support-mailbox",
80
+ "owner": "example-team",
81
+ "limit": "P2D",
82
+ "answered_signal": "none",
83
+ "recommendation": "5d1f0a3c-8b2e-4c7a-9e6f-1a2b3c4d5e6f"
84
+ }
85
+ ],
86
+ "evals": {
87
+ "suite": "test/evals/support",
88
+ "judge_budget_usd": 0,
89
+ "replay_budget_usd": 0,
90
+ "spend_record": null
91
+ },
92
+ "tickets": {
93
+ "outbox": "state/tickets-outbox.jsonl",
94
+ "token_slot": "example-shop/prod/SIGNAL_TICKETS_TOKEN",
95
+ "pd_check": {
96
+ "none": "not_used"
97
+ }
98
+ },
99
+ "recorded_decisions": []
100
+ }
101
+ ]
102
+ }
103
+ }
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "knowledge": "knowledge.example.json",
3
+ "knowledgeMaxAgeSeconds": 7200,
3
4
  "stateDir": "state",
4
5
  "model": "deepseek/deepseek-v4.1-flash",
5
6
  "fallback": "qwen/qwen3.8-flash",