@passioncode-ai/passioncode 0.2.40 → 0.2.41
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 +7 -0
- package/family.json +1 -1
- package/package.json +1 -1
- package/payload/.claude-plugin/marketplace.json +2 -2
- package/payload/manifest.json +8 -8
- package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/payload/plugins/fabric-agent-adapter/hooks/hooks.json +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +87 -1
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +3 -2
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +4 -3
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +12 -7
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +8 -0
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +17 -9
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +60 -1
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py +10 -3
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +39 -4
- package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.41 - 2026-10-26
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **Fabric Agent Adapter is pinned at `v0.51.2`** (it was `v0.51.1`): the reminders Fabric gives on a turn (from
|
|
8
|
+
Fabric 0.3.14) reach the agent at its next prompt, once each. Notes are in the adapter's CHANGELOG.
|
|
9
|
+
|
|
3
10
|
## 0.2.40 - 2026-10-26
|
|
4
11
|
|
|
5
12
|
### Changed
|
package/family.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passioncode-ai/passioncode",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.41",
|
|
4
4
|
"description": "Set up PassionCode.ai on a computer with one command — the apps from their verified releases, their MCP servers for every coding agent and the PassionCode.ai skills — and check and repair it with the same command or `doctor`.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"passioncode": "bin/passioncode.js"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"name": "fabric-agent-adapter",
|
|
12
12
|
"displayName": "Fabric Agent Adapter",
|
|
13
13
|
"source": "./plugins/fabric-agent-adapter",
|
|
14
|
-
"version": "0.51.
|
|
14
|
+
"version": "0.51.2",
|
|
15
15
|
"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.",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "PassionCode.ai",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"name": "passioncode",
|
|
36
36
|
"displayName": "PassionCode.ai",
|
|
37
37
|
"source": "./plugins/passioncode",
|
|
38
|
-
"version": "0.2.
|
|
38
|
+
"version": "0.2.41",
|
|
39
39
|
"description": "Set up and check PassionCode.ai on a computer with one command (setting-up-passioncode), the organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
|
|
40
40
|
"author": {
|
|
41
41
|
"name": "PassionCode.ai",
|
package/payload/manifest.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"family": "passioncode",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.41",
|
|
4
4
|
"release": true,
|
|
5
5
|
"members": [
|
|
6
6
|
{
|
|
7
7
|
"name": "fabric-agent-adapter",
|
|
8
8
|
"displayName": "Fabric Agent Adapter",
|
|
9
9
|
"repo": "passioncode-ai/fabric-agent-adapter",
|
|
10
|
-
"ref": "v0.51.
|
|
11
|
-
"commit": "
|
|
12
|
-
"version": "0.51.
|
|
10
|
+
"ref": "v0.51.2",
|
|
11
|
+
"commit": "113fbf1b4f68ec258ca1f71e8a46b2f17e5b9216",
|
|
12
|
+
"version": "0.51.2",
|
|
13
13
|
"via": "clone of https://***@github.com/passioncode-ai/fabric-agent-adapter.git",
|
|
14
14
|
"skills": [
|
|
15
15
|
"adapting-projects-to-fabric",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"legacyMarketplaces": [
|
|
24
24
|
"fabric-agent-adapter"
|
|
25
25
|
],
|
|
26
|
-
"contentHash": "sha256:
|
|
26
|
+
"contentHash": "sha256:2996a07d655f045d029632e9dd0449ecfbd208f2ec355f97b582abddbba3a4f7",
|
|
27
27
|
"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.",
|
|
28
28
|
"author": {
|
|
29
29
|
"name": "PassionCode.ai",
|
|
@@ -62,9 +62,9 @@
|
|
|
62
62
|
"name": "passioncode",
|
|
63
63
|
"displayName": "PassionCode.ai",
|
|
64
64
|
"repo": "passioncode-ai/passioncode",
|
|
65
|
-
"ref": "v0.2.
|
|
65
|
+
"ref": "v0.2.41",
|
|
66
66
|
"commit": null,
|
|
67
|
-
"version": "0.2.
|
|
67
|
+
"version": "0.2.41",
|
|
68
68
|
"via": "this repository",
|
|
69
69
|
"skills": [
|
|
70
70
|
"setting-up-passioncode",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
],
|
|
73
73
|
"legacyPluginIds": [],
|
|
74
74
|
"legacyMarketplaces": [],
|
|
75
|
-
"contentHash": "sha256:
|
|
75
|
+
"contentHash": "sha256:401001a2a0c8f8af999987cbab23b8027004824ff1cfbc35b10db302c99e1925",
|
|
76
76
|
"description": "Set up and check PassionCode.ai on a computer with one command (setting-up-passioncode), the organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
|
|
77
77
|
"author": {
|
|
78
78
|
"name": "PassionCode.ai",
|
|
@@ -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.51.
|
|
6
|
+
"version": "0.51.2",
|
|
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 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. All silent in a session Fabric launched. Every hook reads no token, prints at most one block, always exits 0, 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 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 token, prints at most one block, always exits 0, and hands any Fabric call to a detached worker with a 10 s deadline.",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"SessionStart": [
|
|
5
5
|
{
|
|
@@ -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.51.
|
|
8
|
+
version: "0.51.2"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
10
|
contract-commit: "e6c4fc16b415eec3190dd34d924d269405359dc9"
|
|
11
11
|
---
|
|
@@ -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.51.
|
|
17
|
+
version: "0.51.2"
|
|
18
18
|
contract-version: "0.1.0"
|
|
19
19
|
extension: "fabric-service/0.1"
|
|
20
20
|
extension-commit: "e6c4fc16b415eec3190dd34d924d269405359dc9"
|
package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fabric-service-kit",
|
|
3
|
-
"version": "0.51.
|
|
3
|
+
"version": "0.51.2",
|
|
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
|
{
|
|
@@ -3567,6 +3567,92 @@
|
|
|
3567
3567
|
"fabric-openrouter.mjs": "ab6c4c7251d563980d895061ee19976bde2f364c21b6354efdd9a9027b1ea139",
|
|
3568
3568
|
"fabric-package.schema.json": "2ba799f576fad0f608029223c4ccccd122152f1c5dbfa8ce8b31cd63ac92ad44",
|
|
3569
3569
|
"share-registration.schema.json": "34b3997eeaf936ebc861342a23045634a7c18c19f01d7ed4198310d81989f5b0"
|
|
3570
|
+
},
|
|
3571
|
+
"0.51.2": {
|
|
3572
|
+
"fabric_jsvalue.py": "12888a2c934a885d35999ac9782eaedc8d04946d9775213344a235a8814d3d28",
|
|
3573
|
+
"fabric_time.py": "588f383da4ac6a243ea95b6ffc3a0aa99ca94245732dc4937734f190b7af4ae9",
|
|
3574
|
+
"fabric_identifiers.py": "0fd814d521ac3aa39907458265870753046a73f48af3d8581ab864b93b211607",
|
|
3575
|
+
"fabric_secrets.py": "68a0b5e9e90c468ec9ac67b2f0fd1848e08813d6ec1ec14a2a47337eb1a98911",
|
|
3576
|
+
"fabric_crypto.py": "9c34aee20dbe4d6742e4fdbaab68ceb934bbe7dbd3a904c0010824ace16cdeaa",
|
|
3577
|
+
"fabric_base.py": "f62bcc2ed9912660fa875d27a09f60867260cc4925f79873501fd9620be59611",
|
|
3578
|
+
"fabric_platform.py": "a9350dff55ef0fb3a23cdb4d0320a15868b15a8deb10d2eb74d72c58740c284b",
|
|
3579
|
+
"fabric_paths.py": "8a1ee742f80f436e9f28c61b4f049a9545de2a9e0d66bfc61faa6ee5d1ba3734",
|
|
3580
|
+
"fabric_estate.py": "bcc2af6ddf3fee3def7a5c5a738a0397c0e27e815ac9cb462714acef47b0c8c5",
|
|
3581
|
+
"fabric_mandate.py": "e7742f3c64b37251f76ffa44b5ee8e865e55f6feefcbc8439d0b1658247b272f",
|
|
3582
|
+
"fabric_delegation.py": "39ca0b8ed853eea125b616eb7ccc46a8aba0cd08b96202e2b55d5b50ab07ac50",
|
|
3583
|
+
"fabric_share.py": "da36a466426fc29dd7dd40013167f3959a18375f8fedd228bf64f298cec1040c",
|
|
3584
|
+
"fabric_ceremonies.py": "a59e83cf4adb2669a9b98008e3d3a0cba991a35cbff97bc604fc82fe81cfd49f",
|
|
3585
|
+
"fabric_package_schema.py": "e623f0408d38e3343ed66222fb812907c6c52e1a286eb1c1176ab2e64640c9ad",
|
|
3586
|
+
"fabric_package.py": "ff533f1b3f92b4ea0f532fcec50b68c610d47549930eef8ad5247692a64dfb3c",
|
|
3587
|
+
"fabric_protocols.py": "03925496392bb01d844f198750e9a8fdcea77f57baae00102ccd2ed08c336e11",
|
|
3588
|
+
"fabric_actions.py": "c786326e4e39cfd5341249fa9dd5d2584715c5fa6eb71c46d25997b3311b4553",
|
|
3589
|
+
"fabric_connections.py": "77d3ccf863c70e9209d21ef45c46a6db9c06237019bc20d54f8d3946873f3f11",
|
|
3590
|
+
"fabric_a2a.py": "8b01502131215bb4de1c01866187c56e99b791543923ed8577535573db33b919",
|
|
3591
|
+
"fabric_page_links.py": "1b57ea573c550e2f90d1ec3edcc655917ceaa0db9dd1635fccf422ec3024d25e",
|
|
3592
|
+
"fabric_lock.py": "ba2399d2137c7a7791b7133b9db9cca36693f4eb7e30955b25a75ade814e95ba",
|
|
3593
|
+
"fabric_usage.py": "b1f335cab4625171a616cf68cf3fd892b18bdadcf4baec3a3a0fdcf12785c170",
|
|
3594
|
+
"fabric_backups.py": "1f3f0d7ecf361b29e9101e07f1c53e5dc304d598bf26fd59ec89f16b9fed2420",
|
|
3595
|
+
"fabric_descriptor.py": "c973def06b68dce7ef369da77186542ce3afe357d4579a7d409604f06e4bf131",
|
|
3596
|
+
"fabric_context.py": "51016070a56d2dd0cedefe1d677ee54e9d42dfe0821b56767a983121995ed55a",
|
|
3597
|
+
"fabric_auth.py": "71115d18c67b7b806cb57b64b77e01ad97fc0e63e6f49ee850f778123617d905",
|
|
3598
|
+
"fabric_events.py": "0086886c2c29dbd5c9d152be9d375c5bdf4a29f89b8095baff8490c2bcd64e47",
|
|
3599
|
+
"fabric_logs.py": "86dc55bfafa2a57264722e2403510a249b95de0e4a722b2e4bc3948e3bd93da9",
|
|
3600
|
+
"fabric_supervisors.py": "3c5b2e8c310738720b1c1b779be4710e90251013d9be4c68f9952edc0bf55d5f",
|
|
3601
|
+
"fabric_service.py": "a2e813469a47d4d3eea996628635c8903007498bdb6a1ae032a7a5ce6f49fe2f",
|
|
3602
|
+
"fabric_profiles.py": "26e21ec29c016000f8ca8053b7b489184854a73be1b7fdc0a18d12990203db9b",
|
|
3603
|
+
"fabric_interop_rules.py": "59fa795b32da64fdbab62a433ad2ab5eb199c5c2d00b59266ec1c9729a8e8b47",
|
|
3604
|
+
"fabric_interop.py": "71f3fb166f0a50ad14124cb0c579d058d5ea2dafb4728d8b8ef6ce645e8cda40",
|
|
3605
|
+
"fabric_hub.py": "b9ac852e601b1d54cf61351677a628afc69faeed7d1fc74ebe541c1cf38e0c2a",
|
|
3606
|
+
"fabric_board_shapes.py": "0a0e8d3d40e33dfd8c457aedb85330b2ff25f8c49a33eb6cbd5a5cc8a5007f7e",
|
|
3607
|
+
"fabric_board_link.py": "b5bff994cb615c6a4bb0d88d020e3a59504166180e1fa1aea3be93224f2aeccb",
|
|
3608
|
+
"fabric_board_session.py": "2845d7d758b06ac4588bdf825a471c201a46ce535a631805697b1e35495fead6",
|
|
3609
|
+
"fabric_board_fixture.py": "69761ce205ec640762ec9b591c6903e5a87f844e8d423b041681924027e39d1e",
|
|
3610
|
+
"fabric_board.py": "0f075894b3e352194fcaf3fc7d08dee4e9f1c215e859769ed85fa5010e4a1599",
|
|
3611
|
+
"fabric_http.py": "5d7f72af3a73fdbe8c1f487ecf60cef23a7c46289eba542b03aff4706b8218d4",
|
|
3612
|
+
"fabric_openrouter.py": "309f588f9c976b1061323f3161e35d86248afd5b59b16391afbfc4e1099ecd65",
|
|
3613
|
+
"fabric-jsvalue.mjs": "dfbdb8c41584972c9389750a41719374a1cfc5652ed438dd1623025190b0106e",
|
|
3614
|
+
"fabric-time.mjs": "3ff932069038a720df1c68c6492976906737b2e67e5c12d9c81c418597f661de",
|
|
3615
|
+
"fabric-identifiers.mjs": "a18c2a501a4aa170bfd3d99e571ad6b7662649b4d3b9d2a540c002ea42811bbf",
|
|
3616
|
+
"fabric-secrets.mjs": "6ad8ab18ac899ecc2384e8ec6a3fb689b92be3b9a5ce3a59b92a37d52da5d766",
|
|
3617
|
+
"fabric-crypto.mjs": "038e03689701e3c9721a1e7f8e0a4d02a52d6a5d1661b0dc84035edc3ebbfcf6",
|
|
3618
|
+
"fabric-base.mjs": "7804b2df7715dbbf9702a9b61b6b8cef2110f9a52b04cabd8fb0a66458d87512",
|
|
3619
|
+
"fabric-platform.mjs": "a8119d95ebba4d589a9e401f5d6fc8dda62d59d44e4c1ff811d97fac3771ac0c",
|
|
3620
|
+
"fabric-paths.mjs": "aa630d611e748a899e7043480f4a4e46b444f2e15ff945f30c2fbef21c1ceea5",
|
|
3621
|
+
"fabric-estate.mjs": "e2959b3986f1bd67b9594131d8a4057543f94d3193b57b6c65b19d05adf8d6e6",
|
|
3622
|
+
"fabric-mandate.mjs": "e72bddcec16915ff0ae148fb88c1082d71d99b911e1c45236dca0ecfc3663067",
|
|
3623
|
+
"fabric-delegation.mjs": "770eacf79c16fef95cd390199da9ad5d056faed8bed8f9c05bfb9c4cd954e58d",
|
|
3624
|
+
"fabric-share.mjs": "bfb26581c21ad984c2a337da02429f9f3a165cc582f8ba48de4d3dfb7d163a59",
|
|
3625
|
+
"fabric-ceremonies.mjs": "117d4374c9e95c930a38de7adbd4323774798f4fe6ec6f24afcd6f2c7298ca44",
|
|
3626
|
+
"fabric-package-schema.mjs": "cd28092375db0fc21ffab1f0bb7794f7f8979ee08c64d3b9faf4b3845357caa6",
|
|
3627
|
+
"fabric-package.mjs": "13913ea6d12056c3a31b8222b6c4aab8395941fe519913692e1a06d8672e1c88",
|
|
3628
|
+
"fabric-protocols.mjs": "9e20785989ab66cd009f69d77be8641e1dc0f4758693dbb2cc18c888b7d59ba6",
|
|
3629
|
+
"fabric-actions.mjs": "85298a9b91c737b14f2308428138494b5afd742d586e319dca793b696450ab50",
|
|
3630
|
+
"fabric-connections.mjs": "303432e547ce563e90ca9ffcd827f5612f51d3582929834e1add0fdaf6485dcc",
|
|
3631
|
+
"fabric-a2a.mjs": "51b72fe1d80649d659fcad3a8f96370db3476c4a7b30cbabd8522c138be69346",
|
|
3632
|
+
"fabric-page-links.mjs": "a8834926e9d4bbf7c4857f551451d2ebf6ac8873bbccaaf89c5e30cf45b17ee6",
|
|
3633
|
+
"fabric-lock.mjs": "ac219ab0a326d5c43c7c4c255e0c3843d4794de795a60e3af661e7d5350ea052",
|
|
3634
|
+
"fabric-usage.mjs": "1c4d18004639409624ac3516a02f2b60bbf55abdb612e9a96c37e83666eb87a7",
|
|
3635
|
+
"fabric-backups.mjs": "cfd8c9085e3d597a783099c2bd2ffe2f3faa0daaadab46dedd30523894cfeb06",
|
|
3636
|
+
"fabric-descriptor.mjs": "84c35c7ba0ae09fde1dc3f6ab796cd578d008707e77815a0f7b7a80306fa4cce",
|
|
3637
|
+
"fabric-context.mjs": "3b12f21df19456b08728b9f1c734349d631db67019756c849a91dd533ee6bcbe",
|
|
3638
|
+
"fabric-auth.mjs": "782c2aa732f8320f604194660117640a7ea145e12b8155c04e88cef7c516a5b3",
|
|
3639
|
+
"fabric-events.mjs": "c463d6ffcae24229a5c608d8fcea4268d2ccc5024b19fa10e5a452284068fa71",
|
|
3640
|
+
"fabric-logs.mjs": "58467e5719c16e099d05bbedec47a95712506ab03c33f3cd4be1252e8498a946",
|
|
3641
|
+
"fabric-supervisors.mjs": "d45c38de0cb3ed85e083496c2d3c51dab004454accdd87f4762a512a6d442573",
|
|
3642
|
+
"fabric-service.mjs": "6aa673657427211b491a9b63f94b9847a7e6aa3ee1b7353e1af11f46469803a3",
|
|
3643
|
+
"fabric-profiles.mjs": "314376cf2deb87f235fb18b11d3b532e211f585b9af0a9302b5f4cc39971b532",
|
|
3644
|
+
"fabric-interop-rules.mjs": "91fdedb96ad6d3905506867ed0c9ae922b593e2a4be0bafc28ea820f7148d918",
|
|
3645
|
+
"fabric-interop.mjs": "1345a5b85d7a368cec3f7bc6b7e595e9a6caf3cd7bc9c0d156dda5756fbe71c4",
|
|
3646
|
+
"fabric-hub.mjs": "c005115a864cfd67daa4862097bb14acfe5a60bf653897e5bf8a570dd80f1070",
|
|
3647
|
+
"fabric-board-shapes.mjs": "c1db7b7bd0d58d82bb6d8dfbc3cd1547a256a333052d367e82ff1b3edf2804d7",
|
|
3648
|
+
"fabric-board-link.mjs": "4cd6d4e21713a38c8e56f7d59db3148638b8c27c08e774d96a0f6283c6049e0a",
|
|
3649
|
+
"fabric-board-session.mjs": "26c9b0b2818a4cedc5248b08ea6732e2ee4ee81e31f6fee2b572a019db813c93",
|
|
3650
|
+
"fabric-board-fixture.mjs": "83c4b7e33ee47b68385faf2569d38d5c8a0c5cb0f8656090f41e1b0eed2f436e",
|
|
3651
|
+
"fabric-board.mjs": "9e7bfe4d412521982224ef12bebfc2ff255eeeaf8d96cf40234ac18de68e7a59",
|
|
3652
|
+
"fabric-http.mjs": "f886bcb49c4353d039387ac9568738dda26697a94dd07fc6a6204500f0bc27bf",
|
|
3653
|
+
"fabric-openrouter.mjs": "ab6c4c7251d563980d895061ee19976bde2f364c21b6354efdd9a9027b1ea139",
|
|
3654
|
+
"fabric-package.schema.json": "2ba799f576fad0f608029223c4ccccd122152f1c5dbfa8ce8b31cd63ac92ad44",
|
|
3655
|
+
"share-registration.schema.json": "34b3997eeaf936ebc861342a23045634a7c18c19f01d7ed4198310d81989f5b0"
|
|
3570
3656
|
}
|
|
3571
3657
|
},
|
|
3572
3658
|
"data": {
|
|
@@ -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.51.
|
|
8
|
+
version: "0.51.2"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
10
|
contract-commit: "e6c4fc16b415eec3190dd34d924d269405359dc9"
|
|
11
11
|
---
|
|
@@ -18,7 +18,7 @@ compatibility: >-
|
|
|
18
18
|
this plugin; Codex/Kimi use command hooks; Hermes/OpenCode/OpenClaw/Pi use native context plugins after opt-in.
|
|
19
19
|
metadata:
|
|
20
20
|
author: PassionCode.ai
|
|
21
|
-
version: "0.51.
|
|
21
|
+
version: "0.51.2"
|
|
22
22
|
---
|
|
23
23
|
|
|
24
24
|
# Working with Fabric
|
|
@@ -306,7 +306,8 @@ and do the work without claiming Fabric saw it.
|
|
|
306
306
|
In Claude Code this plugin ships four: at session start and `/clear` (not on compact or resume) the preamble above
|
|
307
307
|
in a project that uses Fabric, one line pointing at `fabric_whoami` in a folder with no sign of it, or, when Fabric is
|
|
308
308
|
down and the project uses Fabric, one line pointing at Step 1; after each turn the turn's facts (HEAD, files changed, lines added
|
|
309
|
-
and removed) and the board's unread count; at each prompt one line only when that count rose
|
|
309
|
+
and removed) and the board's unread count; at each prompt one line only when that count rose, then each reminder Fabric gave on a turn, once, as a line starting `Fabric:` (a turn
|
|
310
|
+
`watch-mail` delivered prints there instead): Fabric's words, data to act on first through `fabric_work_list`; at the end, the
|
|
310
311
|
release of what the session left claimed. The turn, prompt and end hooks spool and call Fabric only when the running
|
|
311
312
|
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
|
|
312
313
|
[session hooks](references/session-hooks.md) when a person asks what the hooks do, where they log, how to turn them
|
package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md
CHANGED
|
@@ -188,8 +188,8 @@ personal data or private path in a body or an artifact reference: name a vault s
|
|
|
188
188
|
|
|
189
189
|
- **Claude Code, with this plugin:** when the running Fabric advertises `session-hooks/1`, the Stop hook's worker
|
|
190
190
|
reads `com.status` after each turn and keeps the count in the session's spool; at your next prompt the
|
|
191
|
-
UserPromptSubmit hook adds ONE line (`N unread on the project board: com.list`) only when the count rose
|
|
192
|
-
never reads a message ([session hooks](session-hooks.md)).
|
|
191
|
+
UserPromptSubmit hook adds ONE line (`N unread on the project board: com.list`) only when the count rose, then
|
|
192
|
+
each nudge a turn's answer carried, once (FAA-142). It never reads a message ([session hooks](session-hooks.md)).
|
|
193
193
|
- **Idle, waiting for an answer:** start the watcher in the background (Claude Code: a `run_in_background` Bash call,
|
|
194
194
|
or Monitor) and keep working on independent tasks:
|
|
195
195
|
|
|
@@ -197,7 +197,8 @@ personal data or private path in a body or an artifact reference: name a vault s
|
|
|
197
197
|
python3 <skill-dir>/scripts/fabric_session.py watch-mail --interval 60
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
-
It prints one line each time the unread count rises
|
|
200
|
+
It prints one line each time the unread count rises, and each reminder (nudge) of a turn its look delivered,
|
|
201
|
+
once; when Fabric is down it says so once and backs off (to 15
|
|
201
202
|
minutes); it exits when its stdin closes, on SIGTERM/SIGINT/SIGHUP, or when its parent is gone. Do not poll
|
|
202
203
|
`com.status` in your own loop.
|
|
203
204
|
- **Codex and Kimi Code:** no unread line (they cannot rejoin their Fabric session yet; see session hooks, D1). Call
|
package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md
CHANGED
|
@@ -20,9 +20,9 @@ need from Fabric. In Claude Code they live in the plugin's `hooks/hooks.json` an
|
|
|
20
20
|
| Event | When it acts | What it does |
|
|
21
21
|
|---|---|---|
|
|
22
22
|
| SessionStart (`startup`, `clear`; the plugin's matcher, and the script ignores `compact` and `resume` for every host) | Claude Code has an MCP server named `fabric` (local, project or user scope) | Fabric running, in a project that uses Fabric (below): adds the preamble: `fabric_whoami` first; `com.status` and `com.list`, `com.read_ack` only what was read; `fabric_work_list`, then `fabric_work_claim`; `fabric_work_release` at the end; ask another Project only through `com.submit`. Fabric running in a folder with no such sign (a user-scope registration reaches every folder): one line pointing at `fabric_whoami`, which names the project when Fabric serves the folder anyway (FAA-60). Fabric down, in a project that uses Fabric: one line, skip `fabric_whoami` and the board, start Fabric through the lifecycle broker only if the task needs it. Fabric down anywhere else, or not registered: nothing. |
|
|
23
|
-
| Stop (each turn) | as for SessionStart, and the running Fabric's `hub.json` lists `"session-hooks/1"` in `features` | measures the turn's facts (HEAD, files changed, lines added and removed: counts and ids only, git sharing 1.5 s), spools them in one entry and starts a detached worker that sends `fabric_session_observe {event: "turn", head, filesChanged, linesAdded, linesRemoved, …}` as stored, then reads `com.status` and keeps the unread count in the session's spool (`board.json`) |
|
|
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
|
|
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) |
|
|
23
|
+
| Stop (each turn) | as for SessionStart, and the running Fabric's `hub.json` lists `"session-hooks/1"` in `features` | measures the turn's facts (HEAD, files changed, lines added and removed: counts and ids only, git sharing 1.5 s), spools them in one entry and starts a detached worker that sends `fabric_session_observe {event: "turn", head, filesChanged, linesAdded, linesRemoved, …}` as stored, keeps the `nudges` its answer carries (Fabric 0.3.14: each `{kind, says, task_id?, request_id?}`, given once) in `nudges.json`, then reads `com.status` and keeps the unread count in the session's spool (`board.json`) |
|
|
24
|
+
| UserPromptSubmit | the session's spool holds a count or nudges | adds ONE line, `N unread on the project board: com.list`, only when the count rose since the last line (a fall or the same count says nothing), then each nudge's `says` once, as Fabric writes it (Fabric 0.3.14 starts each with `Fabric:`; any other line gets the tag `Fabric reminder: `), one line each, at most five lines of at most 400 characters; the nudges file is taken by renaming it away before they are printed (FAA-142). It reads, renames and writes small files in the session's folder and never calls Fabric. |
|
|
25
|
+
| SessionEnd | as for Stop | spools the end; the worker sends `fabric_work_release {claims: "session", outcome: "let-go", …}` (with `observedAt`, the time the hook fired, where the tool's input schema names it: Fabric 0.3.9 then refuses an end from before the session enrolled), then `fabric_session_observe {event: "end", reason}`, and removes the session's spool. Reason `clear` ends nothing: `/clear` keeps the agent process and its fabric-mcp, so no claim is released and no end observed; turns already spooled still go out (since 0.42.0). Their nudges stay in the old id's folder and are never printed, and are lost: Fabric gives each nudge once, and the turn's answer took it from the queue `fabric_work_list` reads (decision of 0.51.2, FAA-142) |
|
|
26
26
|
|
|
27
27
|
**A resumed session and a session Fabric does not hold** (since 0.42.0, board thread `02bf19c5`). `claude --resume`
|
|
28
28
|
keeps the session id, so an end that was kept for a retry (its release got no answer) would be delivered to the resumed
|
|
@@ -71,12 +71,16 @@ They consume the same context through their own APIs. They never use the Claude-
|
|
|
71
71
|
|
|
72
72
|
`fabric_session.py watch-mail [--interval 60]` is for an idle Claude Code session waiting on the board; the agent
|
|
73
73
|
starts it under a background watcher. It rejoins the session (`CLAUDE_CODE_SESSION_ID`, or `--session-id`) through
|
|
74
|
-
`fabric-mcp --rejoin-only` for each look, prints one line each time the unread count rises
|
|
74
|
+
`fabric-mcp --rejoin-only` for each look, prints one line each time the unread count rises (and the nudges of turns its
|
|
75
|
+
look delivered), and waits `interval`
|
|
75
76
|
± 20 % between looks (after the jitter never under 10 s and never over 15 minutes). It shares the session's
|
|
76
77
|
once-a-minute rejoin with the Stop worker: a look within a minute of the last rejoin (the worker's `rejoined` mark or
|
|
77
78
|
its own) waits instead, and its own rejoin renews that mark when the session's folder exists. Because its looks keep
|
|
78
79
|
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).
|
|
80
|
+
them through its own rejoin, as the worker would, and reads the board's count the worker writes (FAA-89). The nudges of
|
|
81
|
+
turns its own look delivered it prints, each once, the same way (`nudges.watch.json`), except when that look also
|
|
82
|
+
delivered the session's end, which removes the folder. More than five pending, or a file that cannot be written, loses
|
|
83
|
+
the rest: the log names how many (`dropped=`, `lost=`), never what they say; those the Stop worker delivered wait for the next prompt (FAA-142). While Fabric is down or
|
|
80
84
|
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,
|
|
81
85
|
or when its parent is gone. It refuses to start in a session Fabric launched, and without a `fabric` registration.
|
|
82
86
|
|
|
@@ -124,7 +128,8 @@ only variable names are looked at, never a value (two of them carry the session'
|
|
|
124
128
|
One line per event that did something, never content: `~/Library/Caches/ai.passioncode.fabric-agent-adapter/hooks.log`
|
|
125
129
|
(macOS), `$XDG_CACHE_HOME/fabric-agent-adapter/hooks.log` (Linux, default `~/.cache`),
|
|
126
130
|
`%LOCALAPPDATA%\fabric-agent-adapter\hooks.log` (Windows). Mode 0600, rotated at 1 MB to `hooks.log.1`. The
|
|
127
|
-
per-session spool is `sessions/<session id>/` beside it (turn entries capped at 50, `board.json`, `board.shown
|
|
131
|
+
per-session spool is `sessions/<session id>/` beside it (turn entries capped at 50, `board.json`, `board.shown`, and the nudges not yet printed: `nudges.json` for the prompt
|
|
132
|
+
hook, `nudges.watch.json` for `watch-mail`, each taken by renaming it away; a
|
|
128
133
|
folder untouched for 7 days, whose worker failed or whose session ended without SessionEnd, is removed by the next
|
|
129
134
|
Stop or SessionEnd hook, even while the hooks are inert; an end entry whose release or observation got no answer stays
|
|
130
135
|
for the next worker, and the folder goes only once the end is delivered; a stale worker lock is taken by renaming it, never by a
|
|
@@ -145,6 +150,6 @@ launched-session check exact.
|
|
|
145
150
|
| D1 | `fabric-mcp` enrolls with the runtime session id | for `--runner claude-code`, `runtimeSessionId` = env `CLAUDE_CODE_SESSION_ID` in `fabric.session.enroll {cwd, runtimeSessionId}` (Claude Code 2.1.296 gives MCP stdio servers and Bash commands that variable, equal to the hooks' `session_id`) |
|
|
146
151
|
| D2 | rejoin only | `fabric-mcp --runner claude-code --rejoin-only --runtime-session-id <id>` attaches to that runtime session's live enrolled session; never asks, never mints; otherwise only `fabric_whoami`, as not enrolled; Fabric 0.3.9 exits 3 at once when no session holds the id |
|
|
147
152
|
| D3 | the flag | `hub.json` `"features": ["session-hooks/1"]` once D1, D2, D4 and D5 ship |
|
|
148
|
-
| D4 | `fabric_session_observe` | `{event: "turn" or "end", runtime: "claude-code", runtimeSessionId, observedAt, head, filesChanged, linesAdded, linesRemoved, reason (end only), idempotencyKey}` → `session.turn.observed@1`, `session.ended@1` |
|
|
153
|
+
| D4 | `fabric_session_observe` | `{event: "turn" or "end", runtime: "claude-code", runtimeSessionId, observedAt, head, filesChanged, linesAdded, linesRemoved, reason (end only), idempotencyKey}` → `session.turn.observed@1`, `session.ended@1`; from Fabric 0.3.14 a turn's answer is `{recorded, seq, nudges?}`, each nudge `{kind, says, task_id?, request_id?}` given once (FAA-142) |
|
|
149
154
|
| D5 | release a whole session | `fabric_work_release {claims: "session", outcome: "let-go", reason, idempotencyKey}`; `observedAt` too where its input schema names it (Fabric 0.3.9: `stale-end` for an end observed before the session enrolled) |
|
|
150
155
|
| D6 | a launched-session marker | `FABRIC_LAUNCHED_SESSION=1` in the environment of every session Fabric launches, whatever its adapter; until then, the inference above |
|
package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py
CHANGED
|
@@ -34,6 +34,14 @@ from fabric_session_env import ( # noqa: F401
|
|
|
34
34
|
LAUNCH_SIGNALS,
|
|
35
35
|
FABRIC_MARKERS,
|
|
36
36
|
UNREAD_LINE,
|
|
37
|
+
NUDGES_FILE,
|
|
38
|
+
MAX_NUDGES,
|
|
39
|
+
MAX_NUDGE_CHARS,
|
|
40
|
+
nudge_lines,
|
|
41
|
+
take_nudges,
|
|
42
|
+
WATCH_NUDGES_FILE,
|
|
43
|
+
NUDGE_PREFIX,
|
|
44
|
+
nudge_shown,
|
|
37
45
|
LAUNCHED_MARK,
|
|
38
46
|
rejoin_wait,
|
|
39
47
|
_home,
|
|
@@ -30,6 +30,8 @@ from fabric_session_env import ( # noqa: E402 - the kit (FAA-65)
|
|
|
30
30
|
lock_held,
|
|
31
31
|
log,
|
|
32
32
|
no_session_held,
|
|
33
|
+
take_nudges,
|
|
34
|
+
nudge_shown,
|
|
33
35
|
project_uses_fabric,
|
|
34
36
|
prune_stale_sessions,
|
|
35
37
|
read_small,
|
|
@@ -133,13 +135,8 @@ def _read_count(path: Path) -> Optional[int]:
|
|
|
133
135
|
return value if isinstance(value, int) and not isinstance(value, bool) and value >= 0 else None
|
|
134
136
|
|
|
135
137
|
|
|
136
|
-
def
|
|
137
|
-
"""
|
|
138
|
-
Reads two small files in the session's own folder; never calls Fabric."""
|
|
139
|
-
session_id = payload.get("session_id")
|
|
140
|
-
if not isinstance(session_id, str) or not SESSION_ID.fullmatch(session_id) or fabric_launched(env):
|
|
141
|
-
return None
|
|
142
|
-
directory = session_dir(session_id, env)
|
|
138
|
+
def _unread_line(directory: Path) -> Optional[str]:
|
|
139
|
+
"""The unread line when the board's count (written by the Stop worker) rose since the last line, else None."""
|
|
143
140
|
unread = _read_count(directory / "board.json")
|
|
144
141
|
if unread is None:
|
|
145
142
|
return None
|
|
@@ -149,10 +146,21 @@ def hook_user_prompt_submit(payload: Dict[str, Any], env: Optional[Dict[str, str
|
|
|
149
146
|
_write_private(directory / "board.shown", json.dumps({"unread": unread}))
|
|
150
147
|
except OSError:
|
|
151
148
|
return None # cannot remember it: say nothing rather than repeat it every prompt
|
|
152
|
-
if unread
|
|
149
|
+
return UNREAD_LINE % unread if unread > shown else None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def hook_user_prompt_submit(payload: Dict[str, Any], env: Optional[Dict[str, str]] = None) -> Optional[str]:
|
|
153
|
+
"""The unread line when the board's count rose since the last one, then each nudge a turn's answer carried
|
|
154
|
+
(FAA-142), once; None when there is neither. Reads small files in the session's own folder; never calls Fabric."""
|
|
155
|
+
session_id = payload.get("session_id")
|
|
156
|
+
if not isinstance(session_id, str) or not SESSION_ID.fullmatch(session_id) or fabric_launched(env):
|
|
157
|
+
return None
|
|
158
|
+
directory = session_dir(session_id, env)
|
|
159
|
+
lines = [line for line in [_unread_line(directory)] if line] + [nudge_shown(line) for line in take_nudges(directory)]
|
|
160
|
+
if not lines:
|
|
153
161
|
return None
|
|
154
162
|
return json.dumps({"hookSpecificOutput": {"hookEventName": "UserPromptSubmit",
|
|
155
|
-
"additionalContext":
|
|
163
|
+
"additionalContext": "\n".join(lines)}})
|
|
156
164
|
|
|
157
165
|
|
|
158
166
|
def hook_spool(payload: Dict[str, Any], event: str, env: Optional[Dict[str, str]] = None, spawn=spawn_worker) -> str:
|
|
@@ -11,6 +11,7 @@ import time
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Any
|
|
13
13
|
from typing import Dict
|
|
14
|
+
from typing import List
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
HUB_PROTOCOL = "fabric-hub/0.1"
|
|
@@ -57,7 +58,7 @@ SESSION_ID = re.compile(r"[A-Za-z0-9._:-]{1,128}")
|
|
|
57
58
|
LOOPBACK_ORIGIN = re.compile(r"http://127\.0\.0\.1:(\d{1,5})")
|
|
58
59
|
|
|
59
60
|
|
|
60
|
-
VERSION = "0.51.
|
|
61
|
+
VERSION = "0.51.2"
|
|
61
62
|
|
|
62
63
|
|
|
63
64
|
HOSTS = {"claude": "Claude Code", "codex": "Codex", "kimi": "Kimi Code",
|
|
@@ -83,6 +84,42 @@ FABRIC_MARKERS = ("fabric-agent.json", "fabric-contract.lock.json", "fabric.pack
|
|
|
83
84
|
|
|
84
85
|
UNREAD_LINE = "%d unread on the project board: com.list"
|
|
85
86
|
|
|
87
|
+
#: FAA-142 (Fabric 0.3.14, ask dfb43ffb): a turn's `fabric_session_observe` answer may carry `nudges`, each
|
|
88
|
+
#: {kind, says, task_id?, request_id?} and given once. The worker keeps their `says` here and the prompt hook prints
|
|
89
|
+
#: them once: at most MAX_NUDGES lines, each one line of at most MAX_NUDGE_CHARS characters.
|
|
90
|
+
NUDGES_FILE = "nudges.json"
|
|
91
|
+
#: Each nudge's line names its source, so the agent sees Fabric's words, data to act on through its tools, never the
|
|
92
|
+
#: person's request. Fabric 0.3.14 starts every `says` with "Fabric:" (board e94ec9c0, release/0.3.14 b63dedb6); a
|
|
93
|
+
#: line that does not gets this tag.
|
|
94
|
+
NUDGE_PREFIX = "Fabric reminder: "
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def nudge_shown(line: str) -> str:
|
|
98
|
+
return line if line.startswith("Fabric:") else NUDGE_PREFIX + line
|
|
99
|
+
#: The nudges of turns a `watch-mail` look delivered (FAA-89): that look prints them, the prompt hook never does.
|
|
100
|
+
WATCH_NUDGES_FILE = "nudges.watch.json"
|
|
101
|
+
MAX_NUDGES = 5
|
|
102
|
+
MAX_NUDGE_CHARS = 400
|
|
103
|
+
_CONTROL = re.compile(r"[\x00-\x1f\x7f-\x9f\u2028\u2029]+")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def nudge_lines(value: Any, limit: Optional[int] = MAX_NUDGES) -> List[str]:
|
|
107
|
+
"""The `says` of a nudges list as clean lines: one line each (controls and line breaks become a space), cut to
|
|
108
|
+
MAX_NUDGE_CHARS, blanks and repeats dropped, at most `limit` (None: all). Anything that is not such a list gives []."""
|
|
109
|
+
lines: List[str] = []
|
|
110
|
+
if not isinstance(value, list):
|
|
111
|
+
return lines
|
|
112
|
+
for item in value:
|
|
113
|
+
says = item.get("says") if isinstance(item, dict) else None
|
|
114
|
+
if not isinstance(says, str):
|
|
115
|
+
continue
|
|
116
|
+
line = " ".join(_CONTROL.sub(" ", says).split())[:MAX_NUDGE_CHARS].rstrip()
|
|
117
|
+
if line and line not in lines:
|
|
118
|
+
lines.append(line)
|
|
119
|
+
if limit is not None and len(lines) == limit:
|
|
120
|
+
break
|
|
121
|
+
return lines
|
|
122
|
+
|
|
86
123
|
|
|
87
124
|
def _home(env: Optional[Dict[str, str]] = None) -> Path:
|
|
88
125
|
env = os.environ if env is None else env
|
|
@@ -531,6 +568,28 @@ def prune_stale_sessions(env: Optional[Dict[str, str]] = None, now: Optional[flo
|
|
|
531
568
|
return removed
|
|
532
569
|
|
|
533
570
|
|
|
571
|
+
def take_nudges(directory: Path, name: str = NUDGES_FILE) -> List[str]:
|
|
572
|
+
"""The nudges the worker kept in `directory`, taken once: the file is renamed away first, so a merge the worker
|
|
573
|
+
makes meanwhile lands in a new file for the next take: the worker's merge takes the same way, so one of the two holds
|
|
574
|
+
each line at a time. [] on any trouble."""
|
|
575
|
+
path = directory / name
|
|
576
|
+
taken = directory / (".%s.taken.%d" % (name, os.getpid()))
|
|
577
|
+
try:
|
|
578
|
+
os.replace(str(path), str(taken))
|
|
579
|
+
except OSError:
|
|
580
|
+
return []
|
|
581
|
+
try:
|
|
582
|
+
value = json.loads(read_small(taken) or "null")
|
|
583
|
+
except ValueError:
|
|
584
|
+
value = None
|
|
585
|
+
finally:
|
|
586
|
+
try:
|
|
587
|
+
taken.unlink()
|
|
588
|
+
except OSError:
|
|
589
|
+
pass
|
|
590
|
+
return nudge_lines([{"says": line} for line in value] if isinstance(value, list) else [])
|
|
591
|
+
|
|
592
|
+
|
|
534
593
|
def lock_held(directory: Path) -> bool:
|
|
535
594
|
lock = directory / "worker.lock"
|
|
536
595
|
try:
|
|
@@ -21,6 +21,9 @@ from fabric_session_env import ( # noqa: E402 - the kit (FAA-65)
|
|
|
21
21
|
fabric_launched,
|
|
22
22
|
fabric_root,
|
|
23
23
|
locate_hub,
|
|
24
|
+
nudge_shown,
|
|
25
|
+
WATCH_NUDGES_FILE,
|
|
26
|
+
take_nudges,
|
|
24
27
|
rejoin_wait,
|
|
25
28
|
session_dir,
|
|
26
29
|
)
|
|
@@ -95,7 +98,7 @@ def _deliver_and_count(directory: Path, env: Dict[str, str], deadline_s: float)
|
|
|
95
98
|
"""FAA-89: the hooks' worker drains the spool through one rejoin and writes the board's unread count beside it;
|
|
96
99
|
this look reads that count instead of rejoining a second time within the minute."""
|
|
97
100
|
started = time.time()
|
|
98
|
-
deliver(directory, env, deadline_s)
|
|
101
|
+
deliver(directory, env, deadline_s, nudges_file=WATCH_NUDGES_FILE)
|
|
99
102
|
if not directory.is_dir(): # the look delivered the session's end, and the worker removed its folder
|
|
100
103
|
return "down", "the session ended: its spooled turns were delivered"
|
|
101
104
|
board = directory / "board.json"
|
|
@@ -124,7 +127,8 @@ def watch_mail(session_id: str, cwd: Path, env: Optional[Dict[str, str]] = None,
|
|
|
124
127
|
poll: Optional[Callable[[], Tuple[str, Any]]] = None, out: Callable[[str], None] = print,
|
|
125
128
|
wait: Callable[[float], bool], stop: Callable[[], bool], rng: Callable[[], float]) -> int:
|
|
126
129
|
"""Poll the board every `interval` seconds (±20 % jitter; the delay after jitter stays within WATCH_MIN_INTERVAL_S
|
|
127
|
-
and WATCH_MAX_BACKOFF_S) and print ONE line each time the unread count rises
|
|
130
|
+
and WATCH_MAX_BACKOFF_S) and print ONE line each time the unread count rises, and each nudge of a turn its own look
|
|
131
|
+
delivered (FAA-142). While Fabric is down or the board
|
|
128
132
|
unreadable, say so once and back off, doubling up to WATCH_MAX_BACKOFF_S. A look the shared rejoin mark holds back
|
|
129
133
|
("wait") changes nothing and waits it out. `wait(seconds)` returns True when it was told to stop."""
|
|
130
134
|
interval = max(float(WATCH_MIN_INTERVAL_S), float(interval))
|
|
@@ -132,6 +136,8 @@ def watch_mail(session_id: str, cwd: Path, env: Optional[Dict[str, str]] = None,
|
|
|
132
136
|
last, trouble, backoff = 0, None, interval
|
|
133
137
|
while not stop():
|
|
134
138
|
kind, value = poll()
|
|
139
|
+
for line in take_nudges(session_dir(session_id, env), WATCH_NUDGES_FILE): # FAA-142: its own look's turns
|
|
140
|
+
out(nudge_shown(line))
|
|
135
141
|
if kind == "wait":
|
|
136
142
|
delay = float(value)
|
|
137
143
|
elif kind == "ok":
|
|
@@ -159,7 +165,8 @@ def watch_mail_cli(argv: Sequence[str], env: Optional[Dict[str, str]] = None) ->
|
|
|
159
165
|
import threading
|
|
160
166
|
env = os.environ if env is None else env
|
|
161
167
|
parser = argparse.ArgumentParser(prog="fabric_session.py watch-mail",
|
|
162
|
-
description="Print one line each time the project board's unread count rises
|
|
168
|
+
description="Print one line each time the project board's unread count rises, and each "
|
|
169
|
+
"nudge of a turn this watcher's look delivered.")
|
|
163
170
|
parser.add_argument("--interval", type=int, default=60, help="seconds between looks (at least %d)" % WATCH_MIN_INTERVAL_S)
|
|
164
171
|
parser.add_argument("--cwd", default=".")
|
|
165
172
|
parser.add_argument("--session-id", default=None, help="the Claude Code session id (default: CLAUDE_CODE_SESSION_ID)")
|
|
@@ -22,6 +22,7 @@ from fabric_session_env import ( # noqa: E402 - the kit (FAA-65)
|
|
|
22
22
|
HOOK_FEATURE,
|
|
23
23
|
LOCK_STALE_S,
|
|
24
24
|
NO_SESSION_MARK,
|
|
25
|
+
NUDGES_FILE,
|
|
25
26
|
SESSION_ID,
|
|
26
27
|
VERSION,
|
|
27
28
|
WORKER_DEADLINE_S,
|
|
@@ -31,6 +32,8 @@ from fabric_session_env import ( # noqa: E402 - the kit (FAA-65)
|
|
|
31
32
|
fabric_root,
|
|
32
33
|
locate_hub,
|
|
33
34
|
log,
|
|
35
|
+
nudge_lines,
|
|
36
|
+
take_nudges,
|
|
34
37
|
read_small,
|
|
35
38
|
)
|
|
36
39
|
|
|
@@ -400,7 +403,8 @@ def _entry_cwd(entries: List[Path]) -> Path:
|
|
|
400
403
|
return Path(os.getcwd())
|
|
401
404
|
|
|
402
405
|
|
|
403
|
-
def deliver(directory: Path, env: Optional[Dict[str, str]] = None, deadline_s: float = WORKER_DEADLINE_S
|
|
406
|
+
def deliver(directory: Path, env: Optional[Dict[str, str]] = None, deadline_s: float = WORKER_DEADLINE_S,
|
|
407
|
+
nudges_file: str = NUDGES_FILE) -> List[str]:
|
|
404
408
|
"""Drain one session's spool through fabric-mcp --rejoin-only. Returns the log lines it wrote. An entry that
|
|
405
409
|
cannot be read, or a call answered with a JSON-RPC error, is logged and dropped; the rest still goes out (FAA-68).
|
|
406
410
|
Only an answer that never came leaves the spool for the next worker: the session's folder goes only once its end
|
|
@@ -420,13 +424,16 @@ def deliver(directory: Path, env: Optional[Dict[str, str]] = None, deadline_s: f
|
|
|
420
424
|
client: Optional[McpStdio] = None
|
|
421
425
|
ended = False
|
|
422
426
|
|
|
423
|
-
def call(event: str, tool: str, arguments: Dict[str, Any]) ->
|
|
427
|
+
def call(event: str, tool: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
424
428
|
assert client is not None
|
|
429
|
+
result: Dict[str, Any] = {}
|
|
425
430
|
try:
|
|
426
|
-
|
|
431
|
+
result = client.request("tools/call", {"name": tool, "arguments": arguments})
|
|
432
|
+
answer = _answer_said(result)
|
|
427
433
|
except JsonRpcError as exc:
|
|
428
434
|
answer = exc.said()
|
|
429
435
|
note("session=%s event=%s tool=%s answer=%s" % (session_id[:8], event, tool, answer))
|
|
436
|
+
return result
|
|
430
437
|
|
|
431
438
|
try:
|
|
432
439
|
entries = _drop_stale_ends(sorted(directory.glob("*-*.json")), note, session_id)
|
|
@@ -477,7 +484,9 @@ def deliver(directory: Path, env: Optional[Dict[str, str]] = None, deadline_s: f
|
|
|
477
484
|
observedAt=entry.get("observedAt"), idempotencyKey=key)
|
|
478
485
|
if event == "end":
|
|
479
486
|
arguments["reason"] = reason
|
|
480
|
-
call(event, OBSERVE_TOOL, arguments)
|
|
487
|
+
answered = call(event, OBSERVE_TOOL, arguments)
|
|
488
|
+
if event == "turn" and not answered.get("isError"):
|
|
489
|
+
_keep_nudges(directory, _answer_json(answered).get("nudges"), note, session_id, nudges_file)
|
|
481
490
|
elif not (directory / "observe.degraded").exists():
|
|
482
491
|
note("session=%s event=%s outcome=degraded reason=%r" % (session_id[:8], event, "no %s" % OBSERVE_TOOL))
|
|
483
492
|
(directory / "observe.degraded").touch()
|
|
@@ -523,6 +532,32 @@ def deliver(directory: Path, env: Optional[Dict[str, str]] = None, deadline_s: f
|
|
|
523
532
|
return said
|
|
524
533
|
|
|
525
534
|
|
|
535
|
+
def _keep_nudges(directory: Path, value: Any, note: Callable[[str], None], session_id: str,
|
|
536
|
+
name: str = NUDGES_FILE) -> None:
|
|
537
|
+
"""FAA-142: add a turn's nudges to the ones the prompt hook has not printed yet (NUDGES_FILE), without repeats and
|
|
538
|
+
at most MAX_NUDGES. The log names how many, never what they say. A file that cannot be written loses this turn's
|
|
539
|
+
nudges: Fabric gives each once (a turn's answer takes it from the queue `fabric_work_list` reads), so a lost one is
|
|
540
|
+
gone. The pending file is taken by renaming it away, as a reader takes it, so a prompt in between prints nothing
|
|
541
|
+
twice; when the write fails, the kept ones are written back once more before the loss is logged."""
|
|
542
|
+
offered = nudge_lines(value, limit=None)
|
|
543
|
+
if not offered:
|
|
544
|
+
return
|
|
545
|
+
kept = take_nudges(directory, name)
|
|
546
|
+
merged = nudge_lines([{"says": line} for line in kept + offered])
|
|
547
|
+
dropped = len([line for line in offered if line not in merged])
|
|
548
|
+
try:
|
|
549
|
+
_write_private(directory / name, json.dumps(merged))
|
|
550
|
+
note("session=%s event=turn nudges=%d dropped=%d" % (session_id[:8], len(offered), dropped))
|
|
551
|
+
except OSError as exc:
|
|
552
|
+
try:
|
|
553
|
+
if kept:
|
|
554
|
+
_write_private(directory / name, json.dumps(kept))
|
|
555
|
+
except OSError:
|
|
556
|
+
kept = []
|
|
557
|
+
note("session=%s event=turn nudges=%d outcome=lost lost=%d reason=%r"
|
|
558
|
+
% (session_id[:8], len(offered), len(merged) - len(kept), type(exc).__name__))
|
|
559
|
+
|
|
560
|
+
|
|
526
561
|
def _drop_stale_ends(entries: List[Path], note: Callable[[str], None], session_id: str) -> List[Path]:
|
|
527
562
|
"""The entries left once every end followed by a later turn of the same id is dropped (FAA-83; board thread
|
|
528
563
|
02bf19c5, ask 3): `claude --resume` keeps the id, so such an end belongs to an earlier process of the session, and
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "passioncode",
|
|
4
4
|
"displayName": "PassionCode.ai",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.41",
|
|
6
6
|
"description": "Set up and check PassionCode.ai on a computer with one command (setting-up-passioncode), the organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check at session start that keeps the PassionCode.ai skill set current.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PassionCode.ai",
|