@passioncode-ai/fabric-agent-adapter 0.35.2 → 0.35.4

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 CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.35.4 - 2026-10-19
4
+
5
+ ### Changed
6
+
7
+ - **The session hooks rejoin a session at most once a minute** (FAA-53). Every `fabric-mcp --rejoin-only` spends the
8
+ agent's Fabric request budget, which every session on the machine shares; with about forty Claude Code sessions,
9
+ agents waited minutes on "Fabric is rate limited". The worker marks a successful rejoin in the session's folder; a
10
+ turn within 60 s of it is spooled and delivered by the next worker, and the end of a session always starts one.
11
+ Fabric fixed its side in CO-334 (per-session charging, jitter after `Retry-After`) and advised this skip.
12
+ - **`creating-fabric-agents` → resilience reference follows contract DEC-0087** (FAA-56, guidance part): the codes
13
+ are `quota-exhausted` and `outcome-unknown`; a job a limit only paused stays `working` with the pause grammar in its
14
+ `statusMessage`; a job its provider will not resume ends `failed` with `data: {retryable, resetsAt?}`; a caller acts
15
+ as `jobCallerAction` does. The kits' rules and vectors come with the repin (FAA-54).
16
+
17
+ ## 0.35.3 - 2026-10-19
18
+
19
+ ### Added
20
+
21
+ - **`creating-fabric-agents`, resilience and autonomy, third pass**: a rerun calls read-only tools until its open
22
+ outcomes are reconciled; idempotency keys cover content as well as the run; every tool's input schema is pinned to
23
+ the domain schema by a test.
24
+ - **`creating-fabric-agents`, text for people** (FAA-52): the intake grill asks whether the agent writes text a person
25
+ reads; if it does, its prompt forbids the em dash (U+2014) and any substitute for it, and a test fails on U+2014 in
26
+ its output from the first commit. The resilience reference gains *Text for people* (the rule, its exact exceptions,
27
+ a portable check, `accept` rewriting or refusing a model's slip) and a checklist line; a new eval scenario covers it.
28
+ `test/validate.py` fails a tree where the grill, the reference or the scenario loses it.
29
+
30
+ ### Fixed
31
+
32
+ - **The resilience reference numbered two rules 9** under *Secrets and confinement*; the second is 10.
33
+
3
34
  ## 0.35.2 - 2026-10-18
4
35
 
5
36
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@passioncode-ai/fabric-agent-adapter",
3
- "version": "0.35.2",
3
+ "version": "0.35.4",
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.35.2",
6
+ "version": "0.35.4",
7
7
  "author": {
8
8
  "name": "PassionCode.ai",
9
9
  "url": "https://passioncode.ai/"
@@ -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.35.2"
8
+ version: "0.35.4"
9
9
  contract-version: "0.1.0"
10
10
  contract-commit: "d8146cfb0a1c74fc6432284c4356bb0c864b37b5"
11
11
  ---
@@ -18,7 +18,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
18
18
  compatibility: Python 3.9+ or Node.js 20+ for the kits; 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.
19
19
  metadata:
20
20
  author: PassionCode.ai
21
- version: "0.35.2"
21
+ version: "0.35.4"
22
22
  contract-version: "0.1.0"
23
23
  extension: "fabric-service/0.1"
24
24
  extension-commit: "d8146cfb0a1c74fc6432284c4356bb0c864b37b5"
@@ -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.35.2"
8
+ version: "0.35.4"
9
9
  contract-version: "0.1.0"
10
10
  contract-commit: "d8146cfb0a1c74fc6432284c4356bb0c864b37b5"
11
11
  ---
@@ -53,6 +53,7 @@ No file is created until every row has an answer. An answer of "later" fails the
53
53
  | **Which profile?** | `mcp` when Fabric owns planning and retries and the capability has a schema; `a2a` when the provider owns its task lifecycle end to end; `local-runner` when it is a terminal agent Fabric drives. When unsure, the sibling skill's profile-selection reference is the decision record |
54
54
  | **What effects?** | declare anything money-, deletion-, or publication-adjacent now — the host's floor and grants are designed against these declarations, and one discovered late invalidates the admission |
55
55
  | **Which tenancy?** | record whether the agent assumes one operator; the assumption is cheap to write down and expensive to excavate |
56
+ | **Text for people?** | if it writes anything a person reads (a reply, a message, a post, a report, a UI string), its prompt forbids the em dash (U+2014) and any substitute for it, and a test fails on U+2014 in new output, from the first commit (the resilience reference, *Text for people*) |
56
57
 
57
58
  ## Step 1 — the knowledge intake
58
59
 
@@ -207,7 +208,8 @@ Before the first stage that calls a model or spends money, read
207
208
  [resilience and autonomy](references/resilience-and-autonomy.md) and design its answers in:
208
209
  a limit pauses and resumes, every model answer passes the call site's own acceptance, fallbacks
209
210
  and checkers cross vendors, spend is a persisted ledger with idempotency keys, open questions
210
- become recorded decisions, and every fix has a test seen failing. Report its checklist with a
211
+ become recorded decisions, every fix has a test seen failing, and text a person reads carries no
212
+ em dash (U+2014), held by its prompt and a test. Report its checklist with a
211
213
  file and a test per line, or `NOT_RUN` where the agent has no such stage.
212
214
 
213
215
  ## Completion format
@@ -14,13 +14,19 @@ mechanism that held after the fix. Every rule is cheaper to build in than to ret
14
14
  - Verification you can believe
15
15
  - Operations that survive restarts
16
16
  - Secrets and confinement
17
+ - Text for people
17
18
  - Checklist
18
19
 
19
20
  ## Limits are pauses
20
21
 
21
- 1. **A provider or subscription limit pauses the job; it never fails it.** Store a typed pause
22
- (`quota_exhausted`, `retryable: true`, the reset time when known) and resume from the same
23
- stage. Failing it cost callers whole jobs and resubmits.
22
+ 1. **A provider or subscription limit pauses the job; it never fails it.** Keep the job `working` and
23
+ write its `statusMessage` in the contract's pause grammar, `quota-exhausted` or
24
+ `quota-exhausted until <UTC instant>` (optionally `: <text>`, at most 500 characters), with
25
+ `pollIntervalMs` reaching the reset; resume from the same stage. Only a job its provider will not
26
+ resume ends `failed` with `error: {code: "quota-exhausted", message, data: {retryable, resetsAt?}}`:
27
+ `retryable: false` when a person must act, and `resetsAt` (UTC, `T` and `Z`, at most three
28
+ fraction digits) only when `retryable: true` (contract DEC-0087, `FAC-SEM-119`). Failing a job a
29
+ limit only paused cost callers whole jobs and resubmits.
24
30
  2. **Resume first.** Every worker reads its own state — checkpoint, worktree, branches, open
25
31
  pull requests, notes — before it acts, and continues from there. Relaunching from scratch
26
32
  after a limit recreated work over committed work and re-issued credentials already held.
@@ -87,7 +93,11 @@ mechanism that held after the fix. Every rule is cheaper to build in than to ret
87
93
  4. **A resend of an unknown outcome reuses its idempotency key — or does not happen.** Derive
88
94
  keys from the intent (action and entity, or run and step), never from a clock or a random
89
95
  value, and make them unique in storage: time-and-random keys let redelivered requests charge
90
- again. A write whose effect is uncertain stops as `outcome_unknown`; a new key there pays twice.
96
+ again. A write whose effect is uncertain ends `failed` with the code `outcome-unknown`; only an
97
+ observed result settles it, and a new key there pays twice. A caller acts on each answer as the
98
+ contract's `jobCallerAction` does: wait out a pause, resend a retryable `quota-exhausted` under the
99
+ same key after `resetsAt`, hand a non-retryable one or `limit-reached` to a person, reconcile an
100
+ `outcome-unknown`, and never resend it.
91
101
  5. **A client error at submit is a rejection, not a lost task.** Settle it with known usage (zero
92
102
  if none), release the reserved money in the same transaction, and never resend the identical
93
103
  request; a 4xx while polling is still a lost task.
@@ -96,7 +106,10 @@ mechanism that held after the fix. Every rule is cheaper to build in than to ret
96
106
  7. **Reuse what was paid for.** A restarted stage looks up the job's successful call with the
97
107
  same route, prompt version and inputs before calling again (one restart made 117 paid calls
98
108
  where 49 sufficed). Cache keys are content plus definition, never an identity alone.
99
- 8. **Deny spending tools by enumerated name, and test the list** — a provider's newer tools were
109
+ 8. **A rerun after an interruption calls read-only tools until its open outcomes are reconciled**;
110
+ an idempotency key covers the content as well as the run, so a retried run that asks a new
111
+ question is not refused as a conflict.
112
+ 9. **Deny spending tools by enumerated name, and test the list**: a provider's newer tools were
100
113
  missing from a deny list while older ones were denied.
101
114
 
102
115
  ## Autonomy: decide, record, continue
@@ -209,22 +222,42 @@ mechanism that held after the fix. Every rule is cheaper to build in than to ret
209
222
  before merge: a `;` once let a private path reach a public branch.
210
223
  5. **Freeze every input at acceptance**, nested references included: copy it by hash and work on
211
224
  the copy, so a file swapped between acceptance and use is never read.
212
- 6. **Treat what a run wrote as untrusted** before the service copies, reads or renders it: a
225
+ 6. **Pin every tool's input schema to the domain schema with a test.** A save tool that silently
226
+ dropped unknown fields once stored an agent set to send without the guards it was given.
227
+ 7. **Treat what a run wrote as untrusted** before the service copies, reads or renders it: a
213
228
  regular file whose real path is inside the run's own tree.
214
- 7. **An artefact missing a secret refuses to start**, never runs unauthenticated: validation rejects
229
+ 8. **An artefact missing a secret refuses to start**, never runs unauthenticated: validation rejects
215
230
  placeholder-shaped values, and a test renders the configuration without the value and expects
216
231
  an error.
217
- 8. **Widen an access configuration first, code second**, so nothing is over-scoped even briefly.
232
+ 9. **Widen an access configuration first, code second**, so nothing is over-scoped even briefly.
218
233
 
219
- 9. **Bound every recursive parse of untrusted input** (a depth limit, tested at the limit and one
220
- above). Revoke or retire by a content-addressed reference, never a reusable name: a re-keyed
221
- machine keeps its key id. Never refresh a person's own credentials as a side effect; renew only
222
- what you own.
234
+ 10. **Bound every recursive parse of untrusted input** (a depth limit, tested at the limit and one
235
+ above). Revoke or retire by a content-addressed reference, never a reusable name: a re-keyed
236
+ machine keeps its key id. Never refresh a person's own credentials as a side effect; renew only
237
+ what you own.
238
+
239
+ ## Text for people
240
+
241
+ 1. **Text a person reads carries no em dash** (U+2014) and no substitute for it: not `--`, not a
242
+ spaced hyphen used as a dash. The operator reads it as the mark of machine-written text. Use a
243
+ full stop, a comma, a colon or parentheses, or rewrite the sentence. This covers replies,
244
+ messages, posts, mail, reports, captions and UI strings, in every language.
245
+ 2. **Exceptions are exact, never stylistic:** exact quotations, code, commands, paths and
246
+ historical records keep the character as they are.
247
+ 3. **The rule lives in the prompt and in a test.** The prompt of every lane that writes for people
248
+ states it; a test feeds that lane's output (and every fixed template) through a check that
249
+ fails on U+2014, for example `assert "\u2014" not in text`, and is seen failing on a planted
250
+ dash. A spaced hyphen cannot be told from a range or a minus by a pattern: the prompt and a
251
+ reviewer carry that half, the check does not guess.
252
+ 4. **A model's slip is rewritten or refused, never shipped.** When an accepted answer still holds U+2014, the
253
+ call site's `accept` rewrites it or retries once, then refuses; it never sends the dash on.
223
254
 
224
255
  ## Checklist
225
256
 
226
- Before the first release, answer each with a file and a test: where a limit pauses and resumes;
227
- what `accept` each model call has; which vendors its fallback and checker use; where the attempt
228
- ledger and idempotency keys live; what happens to an unknown outcome; which waits are durable;
229
- who owns the run lock; what the next start sweeps; how a cancel reaches paid inner work; which
230
- outputs are capped; how a secret reaches a child; which test was seen failing for each fix.
257
+ Before the first release, answer each with a file and a test: where a limit pauses and resumes; what
258
+ `accept` each model call has; which vendors its fallback and checker use; where the attempt ledger
259
+ and idempotency keys live; what happens to an unknown outcome; which waits are durable; who owns the
260
+ run lock; what the next start sweeps; how a cancel reaches paid inner work; which outputs are
261
+ capped; how a secret reaches a child; which test was seen failing for each fix; and, for an agent
262
+ that writes text for people, where its prompt says no em dash and which test fails on U+2014 in its
263
+ output.
@@ -19,7 +19,7 @@ compatibility: >-
19
19
  this plugin; Codex/Kimi use command hooks; Hermes/OpenCode/OpenClaw/Pi use native context plugins after opt-in.
20
20
  metadata:
21
21
  author: PassionCode.ai
22
- version: "0.35.2"
22
+ version: "0.35.4"
23
23
  ---
24
24
 
25
25
  # Working with Fabric
@@ -105,7 +105,7 @@ and its answer is an error that names what Fabric waits for — never a fake suc
105
105
  |---|---|---|
106
106
  | Fabric is not running, or its hub is closed | no live hub | Step 1; Settings → Agent access in Fabric says why a hub is closed |
107
107
  | Fabric's hub could not be verified | foreign protocol, dead pid, token outside Fabric's folder | send nothing; ask the person to open Fabric |
108
- | the folder is not a project | no project holds this folder | the person opens it in Fabric (+ Project → Open a project) |
108
+ | the folder is not a project | no project holds this folder yet | Fabric with automatic project admission creates or attaches the Project during enrollment; an older Fabric needs the person to open it (+ Project → Open a project) |
109
109
  | waiting for the operator to allow this coding agent | the consent prompt is on their screen | tell the person; call again in a moment; never loop fast |
110
110
  | denied | the operator said no | stop; the denial stands until they clear it in Agent access |
111
111
  | connecting | enrollment in progress | call again in a moment |
@@ -134,8 +134,9 @@ personal data or private path in a body or an artifact reference: name a vault s
134
134
  - Never enroll a second session from a hook or a script: a `fabric-mcp` started outside the agent's own connection
135
135
  enrolls a new session, and Fabric ends a binding's oldest live session past eight — the agent's own. The plugin's
136
136
  hooks and `watch-mail` only rejoin (`--rejoin-only`).
137
- - Never attach a folder to a Project, or create a Project, to make enrollment succeed. "The folder is not a project"
138
- is the answer: tell the person; which Projects exist is their decision.
137
+ - Never attach a folder or create a Project through a side channel to make enrollment succeed. Fabric's own
138
+ enrollment admits an eligible Git folder and records the Project it creates or attaches. If the answer is still
139
+ "the folder is not a project", report it; never write another product's registry or improvise a second admission path.
139
140
 
140
141
  ## Not yet available
141
142
 
@@ -87,6 +87,9 @@ only variable names are looked at, never a value (two of them carry the session'
87
87
  - **Start a second Fabric session.** A `fabric-mcp` started on its own would enroll a new session, and Fabric ends a
88
88
  binding's oldest live session past eight (ADR-0134 amendment 2) — the agent's own. The worker and `watch-mail` run
89
89
  `fabric-mcp --rejoin-only --runtime-session-id <id>`, and only when Fabric advertises `session-hooks/1`.
90
+ - **Rejoin more than once a minute.** Every rejoin spends the agent's Fabric request budget, which every session of
91
+ the machine shares. A turn within 60 s of the session's last successful rejoin (the worker marks it as `rejoined`
92
+ in the session's folder) is spooled and waits for the next worker; the end of a session always starts one (FAA-53).
90
93
  - **Retry in a loop.** A refused call is logged by its error code and dropped; an answer that never came is logged
91
94
  as `outcome=unknown`. The calls carry an `idempotencyKey`, so a repeat is the same event.
92
95
 
@@ -56,6 +56,9 @@ LOCK_STALE_S = 30.0
56
56
  #: A session folder untouched this long belongs to a session that will not spool again: its worker failed, or the
57
57
  #: session ended without SessionEnd. Nothing retries it, so the next spooling hook removes it (FAA-50).
58
58
  SESSION_STALE_S = 7 * 86400
59
+ #: A session rejoins at most once in this span (FAA-53): every rejoin spends Fabric's per-agent request budget, which
60
+ #: every session of the machine shares; a turn inside it waits in the spool for the next worker, an end never waits.
61
+ REJOIN_MIN_INTERVAL_S = 60.0
59
62
  READ_CAP = 64 * 1024
60
63
  CONFIG_CAP = 32 * 1024 * 1024
61
64
  WATCH_MIN_INTERVAL_S = 10
@@ -63,7 +66,7 @@ WATCH_MAX_BACKOFF_S = 900
63
66
  SESSION_ID = re.compile(r"[A-Za-z0-9._:-]{1,128}")
64
67
  LOOPBACK_ORIGIN = re.compile(r"http://127\.0\.0\.1:(\d{1,5})")
65
68
  LOCAL_ORIGIN = re.compile(r'"origin": "http://(?:127\.0\.0\.1|localhost)')
66
- VERSION = "0.35.2"
69
+ VERSION = "0.35.4"
67
70
  HOSTS = {"claude": "Claude Code", "codex": "Codex", "kimi": "Kimi Code",
68
71
  "hermes": "Hermes", "opencode": "OpenCode", "openclaw": "OpenClaw", "pi": "Pi"}
69
72
  NATIVE_PLUGIN_HOSTS = frozenset(("hermes", "opencode", "openclaw", "pi"))
@@ -559,10 +562,21 @@ def hook_spool(payload: Dict[str, Any], event: str, env: Optional[Dict[str, str]
559
562
  if event == "end" and isinstance(payload.get("reason"), str):
560
563
  entry["reason"] = payload["reason"][:64]
561
564
  spool(directory, entry)
562
- if event == "end" or not lock_held(directory):
563
- spawn(directory)
564
- return "spooled: worker started"
565
- return "spooled: a worker is running"
565
+ if event != "end" and lock_held(directory):
566
+ return "spooled: a worker is running"
567
+ if event != "end" and rejoined_recently(directory):
568
+ return "spooled: rejoined recently"
569
+ spawn(directory)
570
+ return "spooled: worker started"
571
+
572
+
573
+ def rejoined_recently(directory: Path, now: Optional[float] = None) -> bool:
574
+ """Whether this session's last rejoin succeeded within REJOIN_MIN_INTERVAL_S (the worker marks it)."""
575
+ try:
576
+ mark = (directory / "rejoined").stat().st_mtime
577
+ except OSError:
578
+ return False
579
+ return 0 <= (time.time() if now is None else now) - mark < REJOIN_MIN_INTERVAL_S
566
580
 
567
581
 
568
582
  def _remove_tree(directory: Path) -> None:
@@ -801,6 +815,7 @@ def deliver(directory: Path, env: Optional[Dict[str, str]] = None, deadline_s: f
801
815
  "clientInfo": {"name": "fabric-agent-adapter-hooks", "version": VERSION}})
802
816
  client.notify("notifications/initialized")
803
817
  names = _wait_for_session_tools(client, deadline)
818
+ _write_private(directory / "rejoined", "")
804
819
  while entries:
805
820
  for path in entries:
806
821
  entry = json.loads(read_small(path) or "{}")