@passioncode-ai/fabric-agent-adapter 0.48.3 → 0.49.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.
- package/CHANGELOG.md +52 -0
- package/README.md +2 -1
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +10 -10
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/profiles.md +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +8 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/usage.md +9 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +21 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +13 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +12 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +15 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_kit.py +15 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +14 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +81 -5
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +2 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +13 -12
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +4 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +1 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +13 -3
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py +36 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.49.0 (2026-10-24)
|
|
4
|
+
|
|
5
|
+
The operator's sweep (FAA-111): blind seam and product readings of 0.47.0 to 0.48.3, their findings fixed, documented
|
|
6
|
+
or filed ([dispositions](docs/evidence/reports/2026-10-24-faa-111-tiers/README.md)), and three open rows closed.
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- **The session start says less in a folder that does not use Fabric** (FAA-60). The plugin's `SessionStart` hook
|
|
11
|
+
printed the whole Fabric preamble (about 900 characters) in every session wherever `fabric` was registered, a project
|
|
12
|
+
Fabric never served included. It now prints the preamble only where `project_uses_fabric` finds a sign (a contract
|
|
13
|
+
file up to the checkout root, a project-scope registration, a service descriptor naming the checkout's origin), and
|
|
14
|
+
elsewhere one line that points at `fabric_whoami`, so a project Fabric serves without a contract file still finds it.
|
|
15
|
+
- **`watch-mail` delivers the turns the hooks spooled** (FAA-89). A look that finds spooled turns runs the worker's own
|
|
16
|
+
`deliver` (one rejoin that reports the turns and reads the unread count) instead of a second rejoin beside them, so
|
|
17
|
+
the once-a-minute budget no longer holds a turn back while an agent waits on the board. The count it reads must be a
|
|
18
|
+
whole number of zero or more, and a look that delivered the session's end says the session ended.
|
|
19
|
+
- **The trigger descriptions of three skills name their boundaries** (FAA-88): `building-fabric-services`,
|
|
20
|
+
`creating-fabric-agents` and `working-with-fabric`, after their first trigger evals against a real client
|
|
21
|
+
([report](docs/evidence/evals/2026-10-24-trigger-run.md): 26/26, 44/45, 38/39 with Claude Code 2.1.299 and haiku).
|
|
22
|
+
- **`fabric_kit.py build` refuses to rewrite a released version's digests**: when `kit.json`'s `"version"` already has
|
|
23
|
+
a release tag and the sources differ, it stops and says to move the version first (the mistake happened three times).
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **A resumed board handler ran twice** (a regression of 0.48.2). FAA-86 kept a resumed request first in the queue
|
|
28
|
+
until its resume returned; a handler that finished inside the resume (a synchronous `spawn`, the kit's fixture among
|
|
29
|
+
them) settled, the settle resumed the queue again and found the same request still first, so it ran the handler and
|
|
30
|
+
sent `com.effect_begin` a second time. Both twins now take the request off before resuming it and put it back first
|
|
31
|
+
only when the resume raises and no effect has begun (a request whose effect began waits for reconcile), which keeps
|
|
32
|
+
FAA-86's behaviour.
|
|
33
|
+
- **The OpenRouter ceiling counts money only** (DEC-0110). `Client.spent_usd` / `spentUsd` and `totals()` counted a
|
|
34
|
+
`subscription` receipt's cost in a shared ledger as spent, while the usage report has counted money only since 0.48.0.
|
|
35
|
+
A `subscription` receipt is now never money; `totals()` answers `spentUsd: 0` when every call was on a subscription
|
|
36
|
+
or there was no call.
|
|
37
|
+
- **The network guards no longer pass a 403 that carries the well-known document.** `guard_verdict` passed any 403;
|
|
38
|
+
a 403 whose JSON body holds the document or a part of it now fails, judged by the same shape as the no-token check.
|
|
39
|
+
- **The no-token check reads the document by its shape** (D4 of FAA-111). A proxy's JSON error with a `service` string
|
|
40
|
+
of its own failed as "the well-known document"; only a `protocol` of the extension, a `service` object with an `id` or
|
|
41
|
+
a `process` object with a `pid` counts. `references/remote-placement.md` lists the two false FAILs that remain.
|
|
42
|
+
|
|
43
|
+
### Documentation
|
|
44
|
+
|
|
45
|
+
- `references/usage.md`: a service built with a kit older than 0.48.0 counts a subscription's estimate until it
|
|
46
|
+
re-vendors `fabric_usage`; a self-stated `spentUsd` on the twin of `budget` must match the receipts or the report is
|
|
47
|
+
refused; a counted `null` means unknown. `references/profiles.md` and `references/protocol.md`: a `subscription`
|
|
48
|
+
row's cost is an estimate, and `platform-metered` is a route.
|
|
49
|
+
- Probe order on a remote placement: `auth.token-file` now comes before `well-known.requires-token` (since 0.48.0); a
|
|
50
|
+
reader that indexes the results by position should read them by rule id.
|
|
51
|
+
- `test/validate.py` scans the recipes' README for em dashes. The FAA-107 and FAA-84 backlog rows are corrected.
|
|
52
|
+
- Filed from the seam reading: FAA-112 (FAC-SEM-137 has no verify-path vectors) and FAA-113 (the remaining Python
|
|
53
|
+
estate lookups that mirror a Node `Set`).
|
|
54
|
+
|
|
3
55
|
## 0.48.3 (2026-10-24)
|
|
4
56
|
|
|
5
57
|
### Fixed
|
package/README.md
CHANGED
|
@@ -34,7 +34,8 @@ python3 plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_s
|
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
In Claude Code the plugin adds four session hooks: SessionStart (at startup and `/clear`, never on compact or
|
|
37
|
-
resume) puts that preamble into the session when the MCP server `fabric` is registered
|
|
37
|
+
resume) puts that preamble into the session when the MCP server `fabric` is registered, Fabric runs and the project
|
|
38
|
+
uses Fabric (one line pointing at `fabric_whoami` in a folder with no sign of it); when Fabric
|
|
38
39
|
is down it adds one line (skip `fabric_whoami`, start Fabric through the broker if the task needs it), and only in a
|
|
39
40
|
project that uses Fabric; Stop records each turn's facts (HEAD, files changed, lines added and removed, never content)
|
|
40
41
|
and the board's unread count; UserPromptSubmit adds one line only when that count rose; SessionEnd releases what the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passioncode-ai/fabric-agent-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.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.
|
|
6
|
+
"version": "0.49.0",
|
|
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.
|
|
8
|
+
version: "0.49.0"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
10
|
contract-commit: "2f020a696453ae299b123517f14957f6a1354379"
|
|
11
11
|
---
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: building-fabric-services
|
|
3
3
|
description: >-
|
|
4
|
-
Use when running an agent as a Fabric service (fabric-service/0.1, its kits and probe): a local
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
(
|
|
12
|
-
Fabric Dashboards.
|
|
4
|
+
Use when running an agent as a Fabric service (fabric-service/0.1, its kits and probe): a local service (launchd,
|
|
5
|
+
systemd, Windows), an ONLINE agent or dashboard on https, its dashboard link and kit (narrow panel, themes), data
|
|
6
|
+
and cache paths, how callers reach and authenticate it, long jobs (fabric.job.*), spend report and limits, runner
|
|
7
|
+
routes and model profiles, the estate record and delegation tokens, answering a Project's board requests, or packing
|
|
8
|
+
and signing it. «сделай агенту дашборд», «локальный сервис агента», «подключи агента к Fabric Dashboards», «отвечать
|
|
9
|
+
на запросы с доски проекта», «расходы агента на модели», "make this agent a local service", "make an online
|
|
10
|
+
dashboard a Fabric service", "add the well-known endpoint". NOT for a one-off script or cron job, a hosted product
|
|
11
|
+
with no agent, the provider manifest (adapting-projects-to-fabric), a new agent's design (creating-fabric-agents), a
|
|
12
|
+
session using Fabric or its board (working-with-fabric), or building the Dashboards app (window, tray).
|
|
13
13
|
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.
|
|
17
|
+
version: "0.49.0"
|
|
18
18
|
contract-version: "0.1.0"
|
|
19
19
|
extension: "fabric-service/0.1"
|
|
20
20
|
extension-commit: "2f020a696453ae299b123517f14957f6a1354379"
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md
CHANGED
|
@@ -25,7 +25,7 @@ python3 $K/fabric_kit.py install <service-dir> --lang node # fabric-ser
|
|
|
25
25
|
python3 $K/fabric_kit.py install <service-dir> --module fabric_openrouter --module fabric_board
|
|
26
26
|
python3 $K/fabric_kit.py install <service-dir> --all --dry-run # every module, nothing written
|
|
27
27
|
python3 $K/fabric_kit.py status <service-dir> [--json] # current, older (the release), modified, missing
|
|
28
|
-
python3 $K/fabric_kit.py build [--check] # maintainers: refresh kit.json from the sources
|
|
28
|
+
python3 $K/fabric_kit.py build [--check] # maintainers: refresh kit.json from the sources (refuses a tagged version)
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
- `install` copies the closure of the named modules and nothing else. It refuses to overwrite a file it does not
|
|
@@ -110,4 +110,6 @@ A probe result in an event is one of `PROBE_RESULTS`; a host never appends a run
|
|
|
110
110
|
## Usage by route
|
|
111
111
|
|
|
112
112
|
`make_usage_receipt(..., route="subscription"|"key")` records how the call was paid: the operator's own login in its
|
|
113
|
-
vendor's runner, or an issued key. The usage report keeps the rows of one model on two routes apart.
|
|
113
|
+
vendor's runner, or an issued key. The usage report keeps the rows of one model on two routes apart. A subscription
|
|
114
|
+
row's `costUsd` is an estimate at list price, never money spent: a limit's `spentUsd` leaves it out (DEC-0110,
|
|
115
|
+
[usage](usage.md)).
|
|
@@ -84,7 +84,7 @@ No auth; the Host/Origin guard applies; answered from memory in under 100 ms.
|
|
|
84
84
|
(a key not entered, a model not chosen). It is not an outage, and a watcher does not alert on it. The kits pass
|
|
85
85
|
entries through unchanged.
|
|
86
86
|
- `configuration` (DEC-0046) MAY say which runner and model profile the service resolved and from where
|
|
87
|
-
(`request`, `agent`, `binding`, `estate`); a usage receipt MAY carry the `route` (`subscription` or `
|
|
87
|
+
(`request`, `agent`, `binding`, `estate`); a usage receipt MAY carry the `route` (`subscription`, `key` or `platform-metered`; a `subscription` row's cost is an estimate, never money, DEC-0110). The kits
|
|
88
88
|
build both: `fabric_profiles.configuration` and `make_usage_receipt(route=)` ([profiles](profiles.md)).
|
|
89
89
|
- `build` needs `commit` or `digest` (`sha256:<64 hex>`).
|
|
90
90
|
- `summary`: at most six tiles; `attention: true` counts toward the host's badge.
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md
CHANGED
|
@@ -62,17 +62,20 @@ watch the gate fail.
|
|
|
62
62
|
(Heroku, Fly, Render, a CDN) a request that names a foreign `Host` never reaches the process: the
|
|
63
63
|
router answers it, usually `404` or `421`. The probe counts that as the refusal it is, as long as
|
|
64
64
|
the body is not the well-known document. The Origin and cross-site checks name the right `Host`,
|
|
65
|
-
reach the service, and still need its own `403`.
|
|
65
|
+
reach the service, and still need its own `403`. No refusal carries the document: a `403` whose JSON body
|
|
66
|
+
holds the document or a part of it (the shapes below) fails (FAA-111).
|
|
66
67
|
|
|
67
68
|
Without the token the well-known route answers `401` with none of the service's data (DEC-0109). The kit's server
|
|
68
69
|
answers it with an empty body. An identity proxy in front of the service (Cloudflare Access, for one) may answer that
|
|
69
70
|
`401` with its own sign-in page: `well-known.requires-token` passes it when the page neither parses as the well-known
|
|
70
71
|
document nor names the service's identity values (its id, `id.instance`, build, or pid, as whole words). The probe
|
|
71
72
|
takes the service's own host name out first, because a sign-in page names the address it guards, and does not look for
|
|
72
|
-
a value shorter than four characters, because a platform's pid is often `1`. A copy of the document
|
|
73
|
-
fails, however short the id
|
|
74
|
-
|
|
75
|
-
value it found
|
|
73
|
+
a value shorter than four characters, because a platform's pid is often `1`. A copy of the document, or of its parts,
|
|
74
|
+
anywhere inside a JSON body fails, however short the id: an object whose `protocol` is the extension's, a `service`
|
|
75
|
+
object with an `id`, a `process` object with a `pid`. A proxy's own JSON error with a `service` string passes. Two false
|
|
76
|
+
FAILs remain possible, and the evidence names the value it found: a proxy page that names the service's id or
|
|
77
|
+
`id.instance` (an application the proxy titles after the service), and, when the document was read, a page that happens
|
|
78
|
+
to contain the service's pid as a whole number (an inline script's constant, say).
|
|
76
79
|
|
|
77
80
|
## What a host does and will not do
|
|
78
81
|
|
|
@@ -82,8 +82,15 @@ ledger.report(service_id=ID, budget={"period": "month", "limitUsd": 100}, budget
|
|
|
82
82
|
a `subscription` route's `costUsd` is an estimate at list price, never spent money, so a window
|
|
83
83
|
spent only on a subscription spends `0`, and money is `null` only while no call off a subscription
|
|
84
84
|
is priced. `usage_spend(rows)` / `usageSpend(rows)` is the contract's `usageSpend` over
|
|
85
|
-
`days[].byModel[]` rows: money and the subscription's estimate apart. A
|
|
86
|
-
|
|
85
|
+
`days[].byModel[]` rows: money and the subscription's estimate apart. A day's `costUsd` still sums every
|
|
86
|
+
route, so beside a subscription it is larger than the limit's `spentUsd`. A service built with a kit older than
|
|
87
|
+
0.48.0 keeps counting the estimate as spent until it re-vendors `fabric_usage`
|
|
88
|
+
(`fabric_kit.py install <dir> --module fabric_usage`, or `--lang node --module fabric-usage`). A per-order limit
|
|
89
|
+
(`per_job`, `approval`) gets `spentUsd: null`. Every other limit states its own spend. The machine twin of
|
|
90
|
+
`budget` may leave `spentUsd` out (the kit counts it) or state the same number the receipts give; a different
|
|
91
|
+
number makes the twins disagree, and the report is refused (`FAC-SEM-031`). A counted `spentUsd` of `null` means
|
|
92
|
+
unknown, never zero: no call off a subscription in the window is priced, or one receipt in it names a `route` the
|
|
93
|
+
contract does not know or a cost its schema refuses, and then the whole window is unknown rather than guessed.
|
|
87
94
|
- `scope` is `machine`, `project`, `pool` or `job`; a `project` or `pool` limit names its
|
|
88
95
|
`subject`. A limit has at most one window (`period`, `windowSeconds` or `since`), and it agrees
|
|
89
96
|
with the kind (`daily`: the day or 86400 s; `monthly`: the month or 28–31 days; `velocity`: a
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py
CHANGED
|
@@ -79,22 +79,39 @@ def priority_problems(plist: dict) -> list:
|
|
|
79
79
|
PLATFORM_HOST_REFUSALS = (400, 404, 421)
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def _body_carries_document(body: bytes) -> bool:
|
|
83
|
+
"""Whether a raw answer body parses as JSON that carries the well-known document or a part of it."""
|
|
84
|
+
try:
|
|
85
|
+
return _carries_document(json.loads((body or b"").decode("utf-8", "replace")))
|
|
86
|
+
except RecursionError:
|
|
87
|
+
return True
|
|
88
|
+
except ValueError:
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
|
|
82
92
|
def guard_verdict(rule: str, remote: bool, status: int, body: bytes) -> Tuple[str, str]:
|
|
93
|
+
"""A guard refuses with the service's own 403, and no refusal carries the well-known document (a 403 whose body is
|
|
94
|
+
the document refused nothing). Elsewhere the substring `fabric-service/` alone already counts as disclosure."""
|
|
83
95
|
if status == 403:
|
|
96
|
+
if _body_carries_document(body):
|
|
97
|
+
return "FAIL", "HTTP 403, and the body carries the well-known document"
|
|
84
98
|
return "PASS", "HTTP 403"
|
|
85
|
-
disclosed = b"fabric-service/" in (body or b"")
|
|
99
|
+
disclosed = b"fabric-service/" in (body or b"") or _body_carries_document(body)
|
|
86
100
|
if rule == "network.host-check" and remote and status in PLATFORM_HOST_REFUSALS and not disclosed:
|
|
87
101
|
return "PASS", "HTTP %d from the platform router: a foreign Host never reaches the service" % status
|
|
88
102
|
return "FAIL", "HTTP %d%s" % (status, ", and the body is the well-known document" if disclosed else "")
|
|
89
103
|
|
|
90
104
|
|
|
91
105
|
def _carries_document(value: Any, depth: int = 0) -> bool:
|
|
92
|
-
"""Whether a parsed body is, or holds anywhere inside it, an object that reads as the well-known document
|
|
93
|
-
`protocol` is the extension's,
|
|
106
|
+
"""Whether a parsed body is, or holds anywhere inside it, an object that reads as the well-known document or a part
|
|
107
|
+
of it: its `protocol` is the extension's, its `service` is an object with an `id`, or its `process` an object with
|
|
108
|
+
a `pid` (D3 of FAA-109). A proxy's own error with a `service` string is none of these."""
|
|
94
109
|
if depth > 64:
|
|
95
110
|
return True # nothing honest nests that deep; refuse rather than recurse without bound
|
|
96
111
|
if isinstance(value, dict):
|
|
97
|
-
|
|
112
|
+
service, process = value.get("service"), value.get("process")
|
|
113
|
+
if value.get("protocol") == fs.PROTOCOL or (isinstance(service, dict) and "id" in service) \
|
|
114
|
+
or (isinstance(process, dict) and "pid" in process):
|
|
98
115
|
return True
|
|
99
116
|
return any(_carries_document(v, depth + 1) for v in value.values())
|
|
100
117
|
if isinstance(value, list):
|
|
@@ -318,9 +318,10 @@ export class Responder {
|
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
/**
|
|
321
|
-
* Resume a transferred request while nothing runs; each idle tick takes the next (finding 39). A
|
|
322
|
-
*
|
|
323
|
-
*
|
|
321
|
+
* Resume a transferred request while nothing runs; each idle tick takes the next (finding 39). A lost answer, of its
|
|
322
|
+
* probe (finding 35) or of the `com.complete` that sends a known outcome (FAA-86), puts the request back first in
|
|
323
|
+
* line, and the next tick sends the same keys again from the checkpoint. It leaves the queue before it resumes, so a
|
|
324
|
+
* handler that settles inside the resume (and reaches `replace`, which resumes the queue again) never runs it twice.
|
|
324
325
|
*/
|
|
325
326
|
async resumeNext() {
|
|
326
327
|
while (!this.current && this.toResume.length && !this.stopping) {
|
|
@@ -331,8 +332,15 @@ export class Responder {
|
|
|
331
332
|
}
|
|
332
333
|
const entry = JSON.parse(JSON.stringify(stored));
|
|
333
334
|
if (entry.probe) await this.repliedBefore(entry);
|
|
334
|
-
|
|
335
|
-
|
|
335
|
+
this.toResume.shift();
|
|
336
|
+
try {
|
|
337
|
+
await this.resume(entry);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
// Back first only while it can still be resumed: once its effect began it waits for reconcile.
|
|
340
|
+
const held = this.checkpoint.entries[entry.request];
|
|
341
|
+
if (held && !held.effect && held.step !== 'unknown' && !this.toResume.includes(entry.request)) this.toResume.unshift(entry.request);
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
336
344
|
}
|
|
337
345
|
}
|
|
338
346
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs
CHANGED
|
@@ -142,6 +142,7 @@ export class Client {
|
|
|
142
142
|
let total = 0;
|
|
143
143
|
for (const r of this.ledger.receipts()) {
|
|
144
144
|
if (!isObject(r) || !this.inWindow(r.at, today)) continue;
|
|
145
|
+
if (r.route === 'subscription') continue; // an estimate at list price, never money spent (DEC-0110)
|
|
145
146
|
if (typeof r.costUsd === 'number' && price(r.costUsd) !== null) total += r.costUsd;
|
|
146
147
|
else total += this.unmeasuredWorst.get(`${r.at} ${r.model}`) ?? unmeasuredUsd; // known here, else unmeasuredUsd
|
|
147
148
|
}
|
|
@@ -322,14 +323,22 @@ export class Client {
|
|
|
322
323
|
}
|
|
323
324
|
}
|
|
324
325
|
|
|
325
|
-
// Calls,
|
|
326
|
+
// Calls, money spent and unpriced calls over receipts. A subscription receipt's cost is an estimate, never money
|
|
327
|
+
// (DEC-0110): spentUsd is null only when calls off a subscription exist and none is priced, 0 when all were on one.
|
|
326
328
|
export function totals(receipts) {
|
|
327
329
|
let calls = 0;
|
|
328
330
|
let priced = 0;
|
|
331
|
+
let paid = 0;
|
|
332
|
+
let paidPriced = 0;
|
|
329
333
|
let spent = 0;
|
|
330
334
|
for (const r of receipts) {
|
|
331
335
|
calls += 1;
|
|
332
|
-
|
|
336
|
+
const onSubscription = r.route === 'subscription';
|
|
337
|
+
if (!onSubscription) paid += 1;
|
|
338
|
+
if (typeof r.costUsd === 'number' && price(r.costUsd) !== null) {
|
|
339
|
+
priced += 1;
|
|
340
|
+
if (!onSubscription) { paidPriced += 1; spent += r.costUsd; }
|
|
341
|
+
}
|
|
333
342
|
}
|
|
334
|
-
return { calls, spentUsd:
|
|
343
|
+
return { calls, spentUsd: paid && !paidPriced ? null : spent, unpricedCalls: calls - priced };
|
|
335
344
|
}
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py
CHANGED
|
@@ -377,19 +377,27 @@ class Responder:
|
|
|
377
377
|
self.checkpoint.put(entry)
|
|
378
378
|
|
|
379
379
|
def _resume_next(self) -> None:
|
|
380
|
-
"""Resume a transferred request while nothing runs; each idle tick takes the next (finding 39). A
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
380
|
+
"""Resume a transferred request while nothing runs; each idle tick takes the next (finding 39). A lost answer,
|
|
381
|
+
of its probe (finding 35) or of the `com.complete` that sends a known outcome (FAA-86), puts the request back
|
|
382
|
+
first in line, and the next tick sends the same keys again from the checkpoint. It leaves the queue before it
|
|
383
|
+
resumes, so a handler that settles inside the resume (and reaches `_replace`, which resumes the queue again)
|
|
384
|
+
never runs it twice."""
|
|
384
385
|
while self.current is None and self._to_resume and not self.stopping:
|
|
385
386
|
entry = self.checkpoint.entries.get(self._to_resume[0])
|
|
386
387
|
if entry is not None and self.slots.get(entry["capability"], {}).get("state") == "held":
|
|
387
388
|
entry = json.loads(json.dumps(entry))
|
|
388
389
|
if entry.get("probe"):
|
|
389
390
|
self._replied_before(entry)
|
|
390
|
-
self.
|
|
391
|
-
|
|
392
|
-
self.
|
|
391
|
+
self._to_resume.pop(0)
|
|
392
|
+
try:
|
|
393
|
+
self._resume(entry)
|
|
394
|
+
except BaseException:
|
|
395
|
+
# Back first only while it can still be resumed: once its effect began it waits for reconcile.
|
|
396
|
+
held = self.checkpoint.entries.get(entry["request"])
|
|
397
|
+
if held is not None and not held.get("effect") and held.get("step") != "unknown" \
|
|
398
|
+
and entry["request"] not in self._to_resume:
|
|
399
|
+
self._to_resume.insert(0, entry["request"])
|
|
400
|
+
raise
|
|
393
401
|
else:
|
|
394
402
|
self._to_resume.pop(0)
|
|
395
403
|
|
|
@@ -23,6 +23,7 @@ import json
|
|
|
23
23
|
import os
|
|
24
24
|
import re
|
|
25
25
|
import shutil
|
|
26
|
+
import subprocess
|
|
26
27
|
import sys
|
|
27
28
|
from pathlib import Path
|
|
28
29
|
from typing import Dict, List
|
|
@@ -61,6 +62,16 @@ def load_manifest() -> Dict:
|
|
|
61
62
|
return json.loads(MANIFEST.read_text(encoding="utf-8"))
|
|
62
63
|
|
|
63
64
|
|
|
65
|
+
def _released(version: str) -> bool:
|
|
66
|
+
"""Whether the checkout the kit sits in carries the release tag `v<version>` (False outside a Git checkout)."""
|
|
67
|
+
try:
|
|
68
|
+
listed = subprocess.run(["git", "-C", str(HERE), "tag", "--list", "v%s" % version],
|
|
69
|
+
capture_output=True, text=True, timeout=10)
|
|
70
|
+
except (OSError, subprocess.SubprocessError):
|
|
71
|
+
return False
|
|
72
|
+
return listed.returncode == 0 and bool(listed.stdout.strip())
|
|
73
|
+
|
|
74
|
+
|
|
64
75
|
def build(check: bool = False) -> List[str]:
|
|
65
76
|
"""Refresh every module's imports and sha256 in kit.json, and record this release's digests in its history.
|
|
66
77
|
Returns what changed (with check=True nothing is written)."""
|
|
@@ -83,6 +94,10 @@ def build(check: bool = False) -> List[str]:
|
|
|
83
94
|
current = {e["file"]: e["sha256"] for e in manifest["python"] + manifest["node"]}
|
|
84
95
|
current.update(digests)
|
|
85
96
|
if manifest.setdefault("history", {}).get(manifest["version"]) != current:
|
|
97
|
+
if not check and manifest["version"] in manifest["history"] and _released(manifest["version"]):
|
|
98
|
+
# A build before the version moves on would rewrite a released version's digests (FAA-111).
|
|
99
|
+
raise SystemExit("kit.json's version %s is released (tag v%s): move its \"version\" to the next release, "
|
|
100
|
+
"then build" % (manifest["version"], manifest["version"]))
|
|
86
101
|
manifest["history"][manifest["version"]] = current
|
|
87
102
|
changed.append("kit.json history %s" % manifest["version"])
|
|
88
103
|
if changed and not check:
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py
CHANGED
|
@@ -212,12 +212,15 @@ class Client:
|
|
|
212
212
|
return day == today if self.window == "day" else day[:7] == today[:7]
|
|
213
213
|
|
|
214
214
|
def spent_usd(self, *, unmeasured_usd: float = 0.0) -> float:
|
|
215
|
-
"""What the ledger records for the current window; a receipt without a cost counts as `unmeasured_usd`.
|
|
215
|
+
"""What the ledger records for the current window; a receipt without a cost counts as `unmeasured_usd`. A
|
|
216
|
+
`subscription` receipt in a shared ledger is an estimate at list price, never money spent (DEC-0110)."""
|
|
216
217
|
today = fabric_time.utc_day(fabric_time.now_ms(self._now))
|
|
217
218
|
total = 0.0
|
|
218
219
|
for receipt in self.ledger.receipts():
|
|
219
220
|
if not isinstance(receipt, dict) or not self._in_window(receipt.get("at"), today):
|
|
220
221
|
continue
|
|
222
|
+
if receipt.get("route") == "subscription":
|
|
223
|
+
continue
|
|
221
224
|
cost = receipt.get("costUsd")
|
|
222
225
|
measured = isinstance(cost, (int, float)) and _price(cost) is not None
|
|
223
226
|
if measured:
|
|
@@ -419,13 +422,18 @@ class Client:
|
|
|
419
422
|
|
|
420
423
|
|
|
421
424
|
def totals(receipts: Iterable[Mapping[str, Any]]) -> Dict[str, Any]:
|
|
422
|
-
"""Calls,
|
|
423
|
-
|
|
424
|
-
|
|
425
|
+
"""Calls, money spent and unpriced calls over receipts (what a dashboard line shows). A `subscription` receipt's
|
|
426
|
+
cost is an estimate, never money (DEC-0110): `spentUsd` is None only when calls off a subscription exist and none of
|
|
427
|
+
them is priced, and 0.0 when every call was on a subscription."""
|
|
428
|
+
calls = priced = paid = paid_priced = 0
|
|
425
429
|
spent = 0.0
|
|
426
430
|
for r in receipts:
|
|
427
431
|
calls += 1
|
|
432
|
+
on_subscription = r.get("route") == "subscription"
|
|
433
|
+
paid += 0 if on_subscription else 1
|
|
428
434
|
if isinstance(r.get("costUsd"), (int, float)) and _price(r.get("costUsd")) is not None:
|
|
429
435
|
priced += 1
|
|
430
|
-
|
|
431
|
-
|
|
436
|
+
if not on_subscription:
|
|
437
|
+
paid_priced += 1
|
|
438
|
+
spent += float(r["costUsd"])
|
|
439
|
+
return {"calls": calls, "spentUsd": None if paid and not paid_priced else spent, "unpricedCalls": calls - priced}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fabric-service-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"about": "The service kit's modules in dependency order: a module imports only modules listed before it. Written by fabric_kit.py build; edit the order and layers by hand, never the digests.",
|
|
5
5
|
"python": [
|
|
6
6
|
{
|
|
@@ -355,7 +355,7 @@
|
|
|
355
355
|
"fabric_hub",
|
|
356
356
|
"fabric_service"
|
|
357
357
|
],
|
|
358
|
-
"sha256": "
|
|
358
|
+
"sha256": "2845d7d758b06ac4588bdf825a471c201a46ce535a631805697b1e35495fead6"
|
|
359
359
|
},
|
|
360
360
|
{
|
|
361
361
|
"module": "fabric_board_fixture",
|
|
@@ -397,7 +397,7 @@
|
|
|
397
397
|
"fabric_time",
|
|
398
398
|
"fabric_usage"
|
|
399
399
|
],
|
|
400
|
-
"sha256": "
|
|
400
|
+
"sha256": "309f588f9c976b1061323f3161e35d86248afd5b59b16391afbfc4e1099ecd65"
|
|
401
401
|
}
|
|
402
402
|
],
|
|
403
403
|
"node": [
|
|
@@ -747,7 +747,7 @@
|
|
|
747
747
|
"fabric-hub",
|
|
748
748
|
"fabric-service"
|
|
749
749
|
],
|
|
750
|
-
"sha256": "
|
|
750
|
+
"sha256": "26c9b0b2818a4cedc5248b08ea6732e2ee4ee81e31f6fee2b572a019db813c93"
|
|
751
751
|
},
|
|
752
752
|
{
|
|
753
753
|
"module": "fabric-board-fixture",
|
|
@@ -788,7 +788,7 @@
|
|
|
788
788
|
"fabric-time",
|
|
789
789
|
"fabric-usage"
|
|
790
790
|
],
|
|
791
|
-
"sha256": "
|
|
791
|
+
"sha256": "ab6c4c7251d563980d895061ee19976bde2f364c21b6354efdd9a9027b1ea139"
|
|
792
792
|
}
|
|
793
793
|
],
|
|
794
794
|
"history": {
|
|
@@ -2875,6 +2875,82 @@
|
|
|
2875
2875
|
"fabric-openrouter.mjs": "f8f0168691f968b7cd85f36aa98e2167e88cc44f1104ff514fd0c7699b339253",
|
|
2876
2876
|
"fabric-package.schema.json": "2ba799f576fad0f608029223c4ccccd122152f1c5dbfa8ce8b31cd63ac92ad44",
|
|
2877
2877
|
"share-registration.schema.json": "34b3997eeaf936ebc861342a23045634a7c18c19f01d7ed4198310d81989f5b0"
|
|
2878
|
+
},
|
|
2879
|
+
"0.49.0": {
|
|
2880
|
+
"fabric_jsvalue.py": "ff1bbfae999fb5683e59e31189143f1c85fa9d7780040b5a6952e060a143fc18",
|
|
2881
|
+
"fabric_time.py": "588f383da4ac6a243ea95b6ffc3a0aa99ca94245732dc4937734f190b7af4ae9",
|
|
2882
|
+
"fabric_identifiers.py": "0fd814d521ac3aa39907458265870753046a73f48af3d8581ab864b93b211607",
|
|
2883
|
+
"fabric_secrets.py": "68a0b5e9e90c468ec9ac67b2f0fd1848e08813d6ec1ec14a2a47337eb1a98911",
|
|
2884
|
+
"fabric_crypto.py": "9c34aee20dbe4d6742e4fdbaab68ceb934bbe7dbd3a904c0010824ace16cdeaa",
|
|
2885
|
+
"fabric_base.py": "f62bcc2ed9912660fa875d27a09f60867260cc4925f79873501fd9620be59611",
|
|
2886
|
+
"fabric_platform.py": "a9350dff55ef0fb3a23cdb4d0320a15868b15a8deb10d2eb74d72c58740c284b",
|
|
2887
|
+
"fabric_paths.py": "6c92af34292baddf7e10d3134d0a5b0bb23387741812a9f79c409cf1628bad23",
|
|
2888
|
+
"fabric_estate.py": "f493cd424dae6380d1caad91ffdca2764e51a49a6b1051a77731e3d8755277cd",
|
|
2889
|
+
"fabric_delegation.py": "6638a6039befe3b43f59018bab292e050931af21b544df1044cd5059572ded36",
|
|
2890
|
+
"fabric_share.py": "78506fe1a60829c525dbf6266a0aa0749a2164290ed94da8249a20b4e9be5605",
|
|
2891
|
+
"fabric_ceremonies.py": "a59e83cf4adb2669a9b98008e3d3a0cba991a35cbff97bc604fc82fe81cfd49f",
|
|
2892
|
+
"fabric_package_schema.py": "e623f0408d38e3343ed66222fb812907c6c52e1a286eb1c1176ab2e64640c9ad",
|
|
2893
|
+
"fabric_package.py": "ff533f1b3f92b4ea0f532fcec50b68c610d47549930eef8ad5247692a64dfb3c",
|
|
2894
|
+
"fabric_protocols.py": "28e693282cddff8e4396ae39ce6fa935e875e8ddac7375bea2c189dd6fb04970",
|
|
2895
|
+
"fabric_connections.py": "77d3ccf863c70e9209d21ef45c46a6db9c06237019bc20d54f8d3946873f3f11",
|
|
2896
|
+
"fabric_lock.py": "ba2399d2137c7a7791b7133b9db9cca36693f4eb7e30955b25a75ade814e95ba",
|
|
2897
|
+
"fabric_usage.py": "b1f335cab4625171a616cf68cf3fd892b18bdadcf4baec3a3a0fdcf12785c170",
|
|
2898
|
+
"fabric_backups.py": "02dc5eb66ec956723d3ddba07b6dd15f6a9726d901f4d60edfd02eee57880ceb",
|
|
2899
|
+
"fabric_descriptor.py": "daf16f95b1810283b1386488e006e940a0cdfa5c3c8d09386ac33d29d58df78d",
|
|
2900
|
+
"fabric_auth.py": "55794587750f6c9f40935b74295d83d7096cb4b26a0b36ac292334cd87d8f853",
|
|
2901
|
+
"fabric_events.py": "0086886c2c29dbd5c9d152be9d375c5bdf4a29f89b8095baff8490c2bcd64e47",
|
|
2902
|
+
"fabric_logs.py": "86dc55bfafa2a57264722e2403510a249b95de0e4a722b2e4bc3948e3bd93da9",
|
|
2903
|
+
"fabric_supervisors.py": "a2dbf15aa1c4eb9de42996b413103fa4814afd2e4e66d1ab7315f30e06ab6841",
|
|
2904
|
+
"fabric_service.py": "103ab4026f96e127c01cd91e4ad44f6b0ce2424ff33f078ed106b39e773a10e3",
|
|
2905
|
+
"fabric_profiles.py": "26e21ec29c016000f8ca8053b7b489184854a73be1b7fdc0a18d12990203db9b",
|
|
2906
|
+
"fabric_interop_rules.py": "b5f0ecb1791b4c50c5ac151f987f692ed7001bc74841a8a161125ec775145e09",
|
|
2907
|
+
"fabric_interop.py": "f92fa111328fe3811438f733d8f0eb5295e6941c774123a78e26318bdcb35fc3",
|
|
2908
|
+
"fabric_hub.py": "b9ac852e601b1d54cf61351677a628afc69faeed7d1fc74ebe541c1cf38e0c2a",
|
|
2909
|
+
"fabric_board_shapes.py": "0a0e8d3d40e33dfd8c457aedb85330b2ff25f8c49a33eb6cbd5a5cc8a5007f7e",
|
|
2910
|
+
"fabric_board_link.py": "b5bff994cb615c6a4bb0d88d020e3a59504166180e1fa1aea3be93224f2aeccb",
|
|
2911
|
+
"fabric_board_session.py": "2845d7d758b06ac4588bdf825a471c201a46ce535a631805697b1e35495fead6",
|
|
2912
|
+
"fabric_board_fixture.py": "69761ce205ec640762ec9b591c6903e5a87f844e8d423b041681924027e39d1e",
|
|
2913
|
+
"fabric_board.py": "0f075894b3e352194fcaf3fc7d08dee4e9f1c215e859769ed85fa5010e4a1599",
|
|
2914
|
+
"fabric_http.py": "5d7f72af3a73fdbe8c1f487ecf60cef23a7c46289eba542b03aff4706b8218d4",
|
|
2915
|
+
"fabric_openrouter.py": "309f588f9c976b1061323f3161e35d86248afd5b59b16391afbfc4e1099ecd65",
|
|
2916
|
+
"fabric-jsvalue.mjs": "dfbdb8c41584972c9389750a41719374a1cfc5652ed438dd1623025190b0106e",
|
|
2917
|
+
"fabric-time.mjs": "3ff932069038a720df1c68c6492976906737b2e67e5c12d9c81c418597f661de",
|
|
2918
|
+
"fabric-identifiers.mjs": "a18c2a501a4aa170bfd3d99e571ad6b7662649b4d3b9d2a540c002ea42811bbf",
|
|
2919
|
+
"fabric-secrets.mjs": "6ad8ab18ac899ecc2384e8ec6a3fb689b92be3b9a5ce3a59b92a37d52da5d766",
|
|
2920
|
+
"fabric-crypto.mjs": "038e03689701e3c9721a1e7f8e0a4d02a52d6a5d1661b0dc84035edc3ebbfcf6",
|
|
2921
|
+
"fabric-base.mjs": "7804b2df7715dbbf9702a9b61b6b8cef2110f9a52b04cabd8fb0a66458d87512",
|
|
2922
|
+
"fabric-platform.mjs": "a8119d95ebba4d589a9e401f5d6fc8dda62d59d44e4c1ff811d97fac3771ac0c",
|
|
2923
|
+
"fabric-paths.mjs": "2201b4ed7126f260931b63e8386377b4317d8dcad419d109bf22c683e44b7f29",
|
|
2924
|
+
"fabric-estate.mjs": "b6610aaad9d00d8a7f42955627fb81725950841455bcedfc050abb0e079f0f93",
|
|
2925
|
+
"fabric-delegation.mjs": "dafde9ea327c88d92dc73d1400282b43dc6f84dd345a2d2b9b154304f65fc9d7",
|
|
2926
|
+
"fabric-share.mjs": "d40a7405d7bd4c3213ed691330b7549afe8e023db0753b582e249714e672010d",
|
|
2927
|
+
"fabric-ceremonies.mjs": "117d4374c9e95c930a38de7adbd4323774798f4fe6ec6f24afcd6f2c7298ca44",
|
|
2928
|
+
"fabric-package-schema.mjs": "cd28092375db0fc21ffab1f0bb7794f7f8979ee08c64d3b9faf4b3845357caa6",
|
|
2929
|
+
"fabric-package.mjs": "13913ea6d12056c3a31b8222b6c4aab8395941fe519913692e1a06d8672e1c88",
|
|
2930
|
+
"fabric-protocols.mjs": "dc039ec3612d0090c9ba6a3a949b0b7df54712bead300dcb6cc8b9bc305cd183",
|
|
2931
|
+
"fabric-connections.mjs": "303432e547ce563e90ca9ffcd827f5612f51d3582929834e1add0fdaf6485dcc",
|
|
2932
|
+
"fabric-lock.mjs": "ac219ab0a326d5c43c7c4c255e0c3843d4794de795a60e3af661e7d5350ea052",
|
|
2933
|
+
"fabric-usage.mjs": "1c4d18004639409624ac3516a02f2b60bbf55abdb612e9a96c37e83666eb87a7",
|
|
2934
|
+
"fabric-backups.mjs": "849acdc6eba0fff648c8158e61f6b68cccd7bedf0880f7d5ec03a6a20f1e7005",
|
|
2935
|
+
"fabric-descriptor.mjs": "95c0b7ff36c29d7c748f1fa6fb1767d6fe8c97fca7f1ee404559930abbd3cc84",
|
|
2936
|
+
"fabric-auth.mjs": "3ea6796470fcd829bc72a772fb7e4c37532a09d1e37ff41592dcf1a1a3074ca2",
|
|
2937
|
+
"fabric-events.mjs": "c463d6ffcae24229a5c608d8fcea4268d2ccc5024b19fa10e5a452284068fa71",
|
|
2938
|
+
"fabric-logs.mjs": "58467e5719c16e099d05bbedec47a95712506ab03c33f3cd4be1252e8498a946",
|
|
2939
|
+
"fabric-supervisors.mjs": "9d61c1a17538a5e2453e33e5ae347337336748041bc054478a96dd51f3d4fc51",
|
|
2940
|
+
"fabric-service.mjs": "1b4639a10c530c1241d317c5e9cf3a10cd96e5d9af8b29fc1b1844de5c63bfec",
|
|
2941
|
+
"fabric-profiles.mjs": "314376cf2deb87f235fb18b11d3b532e211f585b9af0a9302b5f4cc39971b532",
|
|
2942
|
+
"fabric-interop-rules.mjs": "57a8081307956e8c381119b361fa5232a7bf791815be8e3541eb5492820e2ea5",
|
|
2943
|
+
"fabric-interop.mjs": "581983ec3e197e5c2340ad25e8468320c69a2459c90097c7fcf2699766b4fb26",
|
|
2944
|
+
"fabric-hub.mjs": "c005115a864cfd67daa4862097bb14acfe5a60bf653897e5bf8a570dd80f1070",
|
|
2945
|
+
"fabric-board-shapes.mjs": "c1db7b7bd0d58d82bb6d8dfbc3cd1547a256a333052d367e82ff1b3edf2804d7",
|
|
2946
|
+
"fabric-board-link.mjs": "4cd6d4e21713a38c8e56f7d59db3148638b8c27c08e774d96a0f6283c6049e0a",
|
|
2947
|
+
"fabric-board-session.mjs": "26c9b0b2818a4cedc5248b08ea6732e2ee4ee81e31f6fee2b572a019db813c93",
|
|
2948
|
+
"fabric-board-fixture.mjs": "83c4b7e33ee47b68385faf2569d38d5c8a0c5cb0f8656090f41e1b0eed2f436e",
|
|
2949
|
+
"fabric-board.mjs": "9e7bfe4d412521982224ef12bebfc2ff255eeeaf8d96cf40234ac18de68e7a59",
|
|
2950
|
+
"fabric-http.mjs": "f886bcb49c4353d039387ac9568738dda26697a94dd07fc6a6204500f0bc27bf",
|
|
2951
|
+
"fabric-openrouter.mjs": "ab6c4c7251d563980d895061ee19976bde2f364c21b6354efdd9a9027b1ea139",
|
|
2952
|
+
"fabric-package.schema.json": "2ba799f576fad0f608029223c4ccccd122152f1c5dbfa8ce8b31cd63ac92ad44",
|
|
2953
|
+
"share-registration.schema.json": "34b3997eeaf936ebc861342a23045634a7c18c19f01d7ed4198310d81989f5b0"
|
|
2878
2954
|
}
|
|
2879
2955
|
},
|
|
2880
2956
|
"data": {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: creating-fabric-agents
|
|
3
|
-
description: Use when designing and building a NEW agent or provider that must be Fabric-compatible from its first commit, «создай агента, совместимого с фабрикой Passion Code», «новый агент под Fabric», "create a fabric-compatible agent", "build a new Fabric provider", "fabric-ready agent from scratch", «агент в облаке организации», "which models and OpenRouter key for a new Fabric agent". Runs the intake grill (capability, named consumer, workflow or agent, profile, effects, where it runs, models, who pays, how it learns), turns source projects' recorded failures into planted eval fixtures, scaffolds the pinned contract bundle and sets the canary expectation. NOT for adapting an existing project (adapting-projects-to-fabric), making an agent a local or online Fabric service with a dashboard (building-fabric-services), a coding session working through Fabric or its board (working-with-fabric), the Fabric host or orchestrator, or agent
|
|
3
|
+
description: Use when designing and building a NEW agent or provider that must be Fabric-compatible from its first commit, «создай агента, совместимого с фабрикой Passion Code», «новый агент под Fabric», "create a fabric-compatible agent", "build a new Fabric provider", "fabric-ready agent from scratch", «агент в облаке организации», "which models and OpenRouter key for a new Fabric agent". Runs the intake grill (capability, named consumer, workflow or agent, profile, effects, where it runs, models, who pays, how it learns), turns source projects' recorded failures into planted eval fixtures, scaffolds the pinned contract bundle and sets the canary expectation. NOT for adapting an existing project (adapting-projects-to-fabric), making an agent a local or online Fabric service with a dashboard (building-fabric-services), a coding session working through Fabric or its board (working-with-fabric), the Fabric host or orchestrator, or an agent, bot or A2A server that need not speak Fabric.
|
|
4
4
|
license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
5
5
|
compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode. Ships in one plugin with adapting-projects-to-fabric, whose scripts it reuses.
|
|
6
6
|
metadata:
|
|
7
7
|
author: PassionCode.ai
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.49.0"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
10
|
contract-commit: "2f020a696453ae299b123517f14957f6a1354379"
|
|
11
11
|
---
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
name: working-with-fabric
|
|
3
3
|
description: >-
|
|
4
4
|
Use when a coding agent (Claude Code, Codex, Kimi Code, Kilo, Hermes) works in a project through Fabric from a
|
|
5
|
-
terminal: finding Fabric, having the lifecycle broker start
|
|
6
|
-
fabric_whoami first, the project board (com.status, com.list, com.read_ack; asking another Project
|
|
7
|
-
com.submit; unread-mail lines, watch-mail), claiming and releasing work (fabric_work_claim,
|
|
8
|
-
reports, handoffs, refusals. Triggers: "connect to Fabric", "register fabric-mcp", "claim the
|
|
9
|
-
"check the project board", "ask the other project through Fabric", "Fabric is not running",
|
|
10
|
-
Fabric», «возьми задачу в Fabric», «проверь доску проекта»,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
terminal: finding Fabric, having the lifecycle broker start Fabric (not other apps), registering fabric-mcp once
|
|
6
|
+
with no secret, fabric_whoami first, the project board (com.status, com.list, com.read_ack; asking another Project
|
|
7
|
+
only through com.submit; unread-mail lines, watch-mail), claiming and releasing work (fabric_work_claim,
|
|
8
|
+
fabric_work_release), reports, handoffs, refusals. Triggers: "connect to Fabric", "register fabric-mcp", "claim the
|
|
9
|
+
task in Fabric", "check the project board", "ask the other project through Fabric", "Fabric is not running",
|
|
10
|
+
«подключись к Fabric», «возьми задачу в Fabric», «проверь доску проекта», «Fabric не запущен», "hit your limit,
|
|
11
|
+
resets at". Explains the plugin's session hooks. NOT for building or adapting an agent or a board responder
|
|
12
|
+
(creating-fabric-agents, adapting-projects-to-fabric, building-fabric-services), changing Fabric's own code, or
|
|
13
|
+
messaging people.
|
|
14
14
|
license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
15
15
|
compatibility: >-
|
|
16
16
|
Needs the Fabric desktop app (0.4.0 or later for fabric-mcp and the fabric_work tools) and an MCP client.
|
|
@@ -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.
|
|
22
|
+
version: "0.49.0"
|
|
23
23
|
---
|
|
24
24
|
|
|
25
25
|
# Working with Fabric
|
|
@@ -227,8 +227,9 @@ and do the work without claiming Fabric saw it.
|
|
|
227
227
|
|
|
228
228
|
## The session hooks
|
|
229
229
|
|
|
230
|
-
In Claude Code this plugin ships four: at session start and `/clear` (not on compact or resume) the preamble above
|
|
231
|
-
|
|
230
|
+
In Claude Code this plugin ships four: at session start and `/clear` (not on compact or resume) the preamble above
|
|
231
|
+
in a project that uses Fabric, one line pointing at `fabric_whoami` in a folder with no sign of it, or, when Fabric is
|
|
232
|
+
down and the project uses Fabric, one line pointing at Step 1; after each turn the turn's facts (HEAD, files changed, lines added
|
|
232
233
|
and removed) and the board's unread count; at each prompt one line only when that count rose; at the end, the
|
|
233
234
|
release of what the session left claimed. The turn, prompt and end hooks spool and call Fabric only when the running
|
|
234
235
|
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
|
|
@@ -19,7 +19,7 @@ need from Fabric. In Claude Code they live in the plugin's `hooks/hooks.json` an
|
|
|
19
19
|
|
|
20
20
|
| Event | When it acts | What it does |
|
|
21
21
|
|---|---|---|
|
|
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: 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 down, in a project that uses Fabric
|
|
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
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, 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 | 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. It reads two small files 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) |
|
|
@@ -74,7 +74,9 @@ starts it under a background watcher. It rejoins the session (`CLAUDE_CODE_SESSI
|
|
|
74
74
|
`fabric-mcp --rejoin-only` for each look, prints one line each time the unread count rises, and waits `interval`
|
|
75
75
|
± 20 % between looks (after the jitter never under 10 s and never over 15 minutes). It shares the session's
|
|
76
76
|
once-a-minute rejoin with the Stop worker: a look within a minute of the last rejoin (the worker's `rejoined` mark or
|
|
77
|
-
its own) waits instead, and its own rejoin renews that mark when the session's folder exists.
|
|
77
|
+
its own) waits instead, and its own rejoin renews that mark when the session's folder exists. Because its looks keep
|
|
78
|
+
that mark fresh, a Stop-hook turn usually waits in the spool while it runs: a look that finds spooled turns delivers
|
|
79
|
+
them through its own rejoin, as the worker would, and reads the board's count the worker writes (FAA-89). While Fabric is down or
|
|
78
80
|
the board unreadable it says so once and backs off, doubling to at most 15 minutes. It exits on its stdin's end (when started with a pipe), SIGTERM, SIGINT or SIGHUP,
|
|
79
81
|
or when its parent is gone. It refuses to start in a session Fabric launched, and without a `fabric` registration.
|
|
80
82
|
|
package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py
CHANGED
|
@@ -65,6 +65,16 @@ PREAMBLE = (
|
|
|
65
65
|
)
|
|
66
66
|
|
|
67
67
|
|
|
68
|
+
#: FAA-60: one line where Fabric runs and `fabric` is registered, but nothing says this folder works through it (a
|
|
69
|
+
#: user-scope registration reaches every folder of the machine). It hides nothing: `fabric_whoami` names the project
|
|
70
|
+
#: when Fabric serves the folder all the same.
|
|
71
|
+
POINTER = (
|
|
72
|
+
"[fabric-agent-adapter] Fabric is running and this {host} has `fabric` registered; this folder carries no sign that "
|
|
73
|
+
"it works through Fabric. If it does, call `fabric_whoami`: it names the project. The working-with-fabric skill has "
|
|
74
|
+
"the routine."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
68
78
|
#: One line, and only in a project that uses Fabric (project_uses_fabric): everywhere else a Fabric that is down is
|
|
69
79
|
#: nothing the session needs to hear.
|
|
70
80
|
DOWN_NOTICE = (
|
|
@@ -89,8 +99,8 @@ def read_hook_input() -> Dict[str, Any]:
|
|
|
89
99
|
def hook_session_start(payload: Dict[str, Any], env: Optional[Dict[str, str]] = None,
|
|
90
100
|
host: str = "claude") -> Optional[str]:
|
|
91
101
|
"""What to print, or None: Claude/Codex hook JSON, plain context for native plugins and Kimi. The preamble when the
|
|
92
|
-
host has `fabric` registered
|
|
93
|
-
(
|
|
102
|
+
host has `fabric` registered, Fabric runs and the project uses Fabric (project_uses_fabric); a one-line pointer
|
|
103
|
+
when the project shows no sign (FAA-60); when Fabric is down, one line, and only in a project that uses Fabric. Silent on a compacted or resumed session (`source`), which already holds what it was told,
|
|
94
104
|
and in a session Fabric launched. How to read the machine's Fabric services is the host instructions' to say."""
|
|
95
105
|
if host not in HOSTS or payload.get("source") in QUIET_SOURCES or fabric_launched(env):
|
|
96
106
|
return None
|
|
@@ -102,7 +112,7 @@ def hook_session_start(payload: Dict[str, Any], env: Optional[Dict[str, str]] =
|
|
|
102
112
|
if not registered:
|
|
103
113
|
return None
|
|
104
114
|
if locate_hub(fabric_root(env=env))["state"] == "running":
|
|
105
|
-
context = PREAMBLE.format(host=HOSTS[host])
|
|
115
|
+
context = (PREAMBLE if project_uses_fabric(cwd, env, registration) else POINTER).format(host=HOSTS[host])
|
|
106
116
|
elif project_uses_fabric(cwd, env, registration):
|
|
107
117
|
context = DOWN_NOTICE
|
|
108
118
|
else:
|
package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
agent waits on it, with backoff while Fabric is down. Standard library only."""
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
import os
|
|
6
7
|
import sys
|
|
7
8
|
import time
|
|
8
9
|
from pathlib import Path
|
|
9
|
-
from typing import Optional
|
|
10
|
+
from typing import Any, Dict, Optional, Tuple
|
|
10
11
|
|
|
11
12
|
from fabric_session_env import ( # noqa: E402 - the kit (FAA-65)
|
|
12
13
|
BOARD_STATUS_TOOL,
|
|
@@ -28,6 +29,7 @@ from fabric_session_mcp import ( # noqa: E402 - the kit (FAA-65)
|
|
|
28
29
|
NoSessionError,
|
|
29
30
|
_wait_for_session_tools,
|
|
30
31
|
board_unread,
|
|
32
|
+
deliver,
|
|
31
33
|
)
|
|
32
34
|
|
|
33
35
|
|
|
@@ -43,7 +45,8 @@ def poll_unread(session_id: str, cwd: Path, env: Optional[Dict[str, str]] = None
|
|
|
43
45
|
answers but the board cannot be read, ("down", why) when Fabric cannot be reached, ("wait", seconds) when the
|
|
44
46
|
session rejoined within the last minute (the Stop worker's mark, or this one's): the rejoin budget is shared, so
|
|
45
47
|
no fabric-mcp starts then (FAA-68). No fabric-mcp starts unless the running Fabric advertises session-hooks/1
|
|
46
|
-
(the rejoin). A successful rejoin renews the mark in the session's folder when the hooks made one.
|
|
48
|
+
(the rejoin). A successful rejoin renews the mark in the session's folder when the hooks made one. Turns the hooks
|
|
49
|
+
spooled while the mark held them back are delivered by this look, through the same rejoin (FAA-89)."""
|
|
47
50
|
env = dict(os.environ if env is None else env)
|
|
48
51
|
directory = session_dir(session_id, env)
|
|
49
52
|
left = rejoin_wait(directory)
|
|
@@ -57,6 +60,8 @@ def poll_unread(session_id: str, cwd: Path, env: Optional[Dict[str, str]] = None
|
|
|
57
60
|
registration = claude_registration(cwd, env)
|
|
58
61
|
if not registration:
|
|
59
62
|
return "down", "Claude Code has no MCP server named fabric"
|
|
63
|
+
if directory.is_dir() and any(directory.glob("*-*.json")):
|
|
64
|
+
return _deliver_and_count(directory, env, deadline_s)
|
|
60
65
|
client: Optional[McpStdio] = None
|
|
61
66
|
try:
|
|
62
67
|
deadline = time.monotonic() + deadline_s
|
|
@@ -86,6 +91,35 @@ def poll_unread(session_id: str, cwd: Path, env: Optional[Dict[str, str]] = None
|
|
|
86
91
|
client.close()
|
|
87
92
|
|
|
88
93
|
|
|
94
|
+
def _deliver_and_count(directory: Path, env: Dict[str, str], deadline_s: float) -> Tuple[str, Any]:
|
|
95
|
+
"""FAA-89: the hooks' worker drains the spool through one rejoin and writes the board's unread count beside it;
|
|
96
|
+
this look reads that count instead of rejoining a second time within the minute."""
|
|
97
|
+
started = time.time()
|
|
98
|
+
deliver(directory, env, deadline_s)
|
|
99
|
+
if not directory.is_dir(): # the look delivered the session's end, and the worker removed its folder
|
|
100
|
+
return "down", "the session ended: its spooled turns were delivered"
|
|
101
|
+
board = directory / "board.json"
|
|
102
|
+
try:
|
|
103
|
+
fresh = board.stat().st_mtime >= started - 1
|
|
104
|
+
except OSError:
|
|
105
|
+
fresh = False
|
|
106
|
+
unread = None
|
|
107
|
+
if fresh:
|
|
108
|
+
try:
|
|
109
|
+
data = json.loads(board.read_text(encoding="utf-8"))
|
|
110
|
+
unread = data.get("unread") if isinstance(data, dict) else None
|
|
111
|
+
except (OSError, ValueError):
|
|
112
|
+
unread = None
|
|
113
|
+
if isinstance(unread, int) and not isinstance(unread, bool) and unread >= 0:
|
|
114
|
+
return "ok", unread
|
|
115
|
+
if fresh:
|
|
116
|
+
return "unavailable", "the project board cannot be read"
|
|
117
|
+
left = rejoin_wait(directory)
|
|
118
|
+
if left > 0:
|
|
119
|
+
return "wait", left
|
|
120
|
+
return "down", "the spooled turns could not be delivered now"
|
|
121
|
+
|
|
122
|
+
|
|
89
123
|
def watch_mail(session_id: str, cwd: Path, env: Optional[Dict[str, str]] = None, interval: float = 60, *,
|
|
90
124
|
poll: Optional[Callable[[], Tuple[str, Any]]] = None, out: Callable[[str], None] = print,
|
|
91
125
|
wait: Callable[[float], bool], stop: Callable[[], bool], rng: Callable[[], float]) -> int:
|