@passioncode-ai/passioncode 0.1.29 → 0.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/family.json +2 -2
  3. package/package.json +1 -1
  4. package/payload/.claude-plugin/marketplace.json +3 -3
  5. package/payload/manifest.json +12 -12
  6. package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  7. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +9 -3
  8. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +39 -10
  9. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +23 -10
  10. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +77 -14
  11. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +5 -3
  12. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +6 -0
  13. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/usage.md +63 -0
  14. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +106 -3
  15. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +77 -0
  16. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +317 -8
  17. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +85 -1
  18. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +472 -15
  19. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +23 -10
  20. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +7 -3
  21. package/payload/plugins/observatory-log/.claude-plugin/plugin.json +1 -1
  22. package/payload/plugins/observatory-log/hooks/session-start.sh +25 -11
  23. package/payload/plugins/observatory-log/skills/explaining-changes/SKILL.md +1 -1
  24. package/payload/plugins/observatory-log/skills/handling-secrets/SKILL.md +2 -2
  25. package/payload/plugins/observatory-log/skills/tracking-resources/SKILL.md +1 -1
  26. package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
@@ -20,9 +20,7 @@ import json
20
20
  import os
21
21
  from http.server import BaseHTTPRequestHandler
22
22
  from pathlib import Path
23
- import signal
24
23
  import sys
25
- import threading
26
24
  from typing import Any, Dict, Optional
27
25
  from urllib.parse import parse_qs, urlparse
28
26
 
@@ -66,12 +64,15 @@ class Service:
66
64
  self.degraded = [{"source": "demo", "reason": args.degraded}] if args.degraded else []
67
65
  self.started_at = fs.now_iso()
68
66
  self.build = {"commit": os.environ.get("FABRIC_BUILD_COMMIT", "0000000"), "builtAt": self.started_at}
67
+ self.drain = fs.Drain() # LC-01: SIGTERM drains in-flight calls, then the server stops
69
68
 
70
69
  def start(self) -> None:
71
70
  # Rule: the lock comes before ANY side effect, including creating the token.
72
71
  self.lock = fs.hold_single_instance(self.data)
73
72
  self.token = fs.ensure_token(self.token_file)
74
73
  self.log = fs.JsonlEventLog(self.data / "events.jsonl")
74
+ self.usage = fs.JsonlUsageLedger(self.data / "usage.jsonl") # DEC-0021
75
+ self.usage.prune()
75
76
  self.codes = fs.LoginCodes(self.data / "auth")
76
77
  self.jobs = fi.JobStore(self.data / "jobs")
77
78
  self.mcp = fi.McpToolServer(self.id, VERSION, jobs=self.jobs, on_job_started=self.draft_started, on_input=self.draft_answered)
@@ -117,6 +118,8 @@ class Service:
117
118
  "binding": {"id": "urn:fabric:binding:sample.draft", "revision": 1, "contentHash": "sha256:" + "0" * 64}, "writeScopes": []},
118
119
  not_verified=[{"claim": "NOTE", "reason": "no checker has read the note"}],
119
120
  output={"title": title, "body": body}, usage={"inputTokens": 0, "outputTokens": 0, "wallMs": 1}))
121
+ # The sample writes from a template: one call that costs a known $0 (DEC-0021 usage report).
122
+ self.usage.record(fs.make_usage_receipt("local", "sample-template", input_tokens=0, output_tokens=0, cost_usd=0.0, cost_basis="price-list"))
120
123
  self.log.append("job.completed", "info", "The note %s is drafted." % title, **fi.trace_ids(ctx.traceparent))
121
124
 
122
125
  def well_known(self) -> Dict[str, Any]:
@@ -125,7 +128,8 @@ class Service:
125
128
  started_at=self.started_at, status="ready", degraded=self.degraded,
126
129
  summary=[{"label": "Events", "value": len(self.log.fetch(None, fs.EVENTS_MAX_LIMIT))}],
127
130
  surfaces={"dashboard": {"path": "/", "login": True}, "events": {"path": "/fabric/v1/events"},
128
- "mcp": {"path": "/mcp", "transport": "streamable-http", "capabilities": [c["name"] for c in CAPABILITIES]}},
131
+ "mcp": {"path": "/mcp", "transport": "streamable-http", "capabilities": [c["name"] for c in CAPABILITIES]},
132
+ "usage": {"path": fs.USAGE_PATH}},
129
133
  )
130
134
 
131
135
 
@@ -187,6 +191,10 @@ def make_handler(svc: Service):
187
191
  except fs.ServiceError as exc:
188
192
  return self._send(400, {"error": str(exc)})
189
193
  return self._send(200, page)
194
+ if url.path == fs.USAGE_PATH:
195
+ if not self._token_ok():
196
+ return
197
+ return self._send(200, svc.usage.report(service_id=svc.id, instance=svc.instance))
190
198
  if url.path == "/mcp":
191
199
  # No server-to-client stream here: Streamable HTTP says 405 for a GET.
192
200
  return self._send(405, {"error": "POST JSON-RPC to /mcp."}, {"Allow": "POST"})
@@ -216,7 +224,14 @@ def make_handler(svc: Service):
216
224
  message = json.loads(self.rfile.read(length) or b"null")
217
225
  except ValueError:
218
226
  return self._send(400, {"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": "Parse error."}})
219
- response = svc.mcp.handle(message)
227
+ try:
228
+ with svc.drain.work():
229
+ response = svc.mcp.handle(message, self.headers)
230
+ except fs.Stopping as exc:
231
+ return self._send(503, {"jsonrpc": "2.0", "id": None, "error": {"code": -32000, "message": str(exc)}},
232
+ {"Retry-After": "5"})
233
+ if response is not None and (response.get("error") or {}).get("code") == fi.HEADER_MISMATCH:
234
+ return self._send(400, response)
220
235
  return self._send(202) if response is None else self._send(200, response)
221
236
  if url.path == "/fabric/v1/login-code":
222
237
  if not self._token_ok():
@@ -247,12 +262,10 @@ def serve(args: argparse.Namespace) -> int:
247
262
  svc.start()
248
263
  server = make_server(svc)
249
264
 
250
- def stop(signum: int, _frame: Any) -> None:
251
- svc.log.append("service.stopping", "info", "%s is stopping." % svc.name)
252
- threading.Thread(target=server.shutdown, daemon=True).start()
253
-
254
- signal.signal(signal.SIGTERM, stop)
255
- signal.signal(signal.SIGINT, stop)
265
+ # Stop taking calls, let in-flight ones finish (8 s at most), then end serve_forever; a
266
+ # hand-over that hangs is cut by the kit's hard exit, inside launchd's ExitTimeOut.
267
+ svc.drain.install(lambda drained: server.shutdown(),
268
+ on_stopping=lambda: svc.log.append("service.stopping", "info", "%s is stopping." % svc.name))
256
269
  try:
257
270
  server.serve_forever(poll_interval=0.2)
258
271
  finally:
@@ -5,9 +5,9 @@ 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.6.3"
8
+ version: "0.8.1"
9
9
  contract-version: "0.1.0"
10
- contract-commit: "2ce392291c6668598d12cd38327e24696b5ca15c"
10
+ contract-commit: "623bf61358c339cb10297807b3f024b5d9f1f327"
11
11
  ---
12
12
 
13
13
  # Creating Fabric-compatible agents
@@ -119,12 +119,16 @@ python3 <plugin-dir>/skills/adapting-projects-to-fabric/scripts/adapt_project.py
119
119
  --schema-base <immutable-base-uri>
120
120
  ```
121
121
 
122
- Pin exactly contract `0.1.0` at commit `2ce392291c6668598d12cd38327e24696b5ca15c` and
122
+ Pin exactly contract `0.1.0` at commit `623bf61358c339cb10297807b3f024b5d9f1f327` and
123
123
  read the pinned guide before implementing protocol details. If this skill is installed
124
124
  without its sibling, the scaffolder is absent: create the bundle by hand from the pinned
125
125
  contract's `docs/guides/connecting-compatible-agents.md` and mark the structural check
126
126
  `NOT_RUN` — do not reconstruct normative rules from memory.
127
127
 
128
+ This default accepts underscore capability names; a declared communications name does
129
+ not grant COM access or prove runtime admission. Existing bundles retain their explicit
130
+ supported revision and require its exact checkout; never silently migrate their lock.
131
+
128
132
  The generated placeholders are deliberate blockers; a bundle still containing one is not
129
133
  ready for admission.
130
134
 
@@ -3,7 +3,7 @@
3
3
  "name": "observatory-log",
4
4
  "displayName": "Observatory Log",
5
5
  "description": "Records what changed in a watched project at the end of every turn, and asks for the one thing a diff cannot contain: why.",
6
- "version": "0.17.3",
6
+ "version": "0.18.0",
7
7
  "license": "AGPL-3.0-only OR LicenseRef-PassionCode-Commercial",
8
8
  "author": {
9
9
  "name": "PassionCode.ai",
@@ -47,17 +47,28 @@ fi
47
47
  # rate-limited, so a session never waits for it and never fails because of it.
48
48
  stamps="${XDG_STATE_HOME:-$HOME/.local/state}/project-observatory"
49
49
  mkdir -p "$stamps" 2>/dev/null && chmod 700 "$stamps" 2>/dev/null
50
- # claim NAME SECONDS: true for exactly one caller per window. `mkdir` is atomic, so twenty
51
- # sessions starting at once start one job, not twenty (review F6).
50
+ # claim NAME SECONDS: true for exactly one caller per window. The check and the stamp happen
51
+ # under one exclusive lock, so twenty sessions starting at once start one job, not twenty —
52
+ # also when the previous window has just expired (audit A20).
52
53
  claim() {
53
- local lock="$stamps/$1.claim" then now
54
- if ! mkdir "$lock" 2>/dev/null; then
55
- then="$(stat -c %Y "$lock" 2>/dev/null || stat -f %m "$lock" 2>/dev/null || echo 0)"
56
- now="$(date +%s)"
57
- [ $((now - then)) -ge "$2" ] || return 1
58
- rmdir "$lock" 2>/dev/null; mkdir "$lock" 2>/dev/null || return 1
59
- fi
60
- return 0
54
+ "$py" - "$stamps/$1.at" "$2" <<'PYCLAIM' 2>/dev/null
55
+ import fcntl, os, sys, time
56
+ path, window = sys.argv[1], int(sys.argv[2])
57
+ fd = os.open(path + ".lock", os.O_CREAT | os.O_RDWR, 0o600)
58
+ try:
59
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
60
+ except BlockingIOError:
61
+ sys.exit(1)
62
+ try:
63
+ last = os.stat(path).st_mtime
64
+ except OSError:
65
+ last = 0
66
+ if time.time() - last < window:
67
+ sys.exit(1)
68
+ with open(path, "a"):
69
+ pass
70
+ os.utime(path, None)
71
+ PYCLAIM
61
72
  }
62
73
  # detach LOG CMD...: a new session of its own, so ending this session or its process group
63
74
  # cannot stop an install half-way (review F8).
@@ -93,7 +104,10 @@ print(1 if (d.get(\"updates\") or {}).get(\"auto\") is False else 0)" "$home/con
93
104
  "$po" full update --check >/dev/null 2>&1; rc=$?
94
105
  echo "$(date -u +%FT%TZ) check exit $rc"
95
106
  [ "$rc" = "10" ] || exit 0
96
- "$po" full update --apply; echo "$(date -u +%FT%TZ) apply exit $?"
107
+ # No launchd (Linux): nothing for the update to stop, and saying so lets it run (A02).
108
+ if command -v launchctl >/dev/null 2>&1; then "$po" full update --apply
109
+ else "$po" full update --apply --writers-stopped; fi
110
+ echo "$(date -u +%FT%TZ) apply exit $?"
97
111
  ' _ "$po" "$py"
98
112
  fi
99
113
  fi
@@ -15,7 +15,7 @@ description: >-
15
15
  decision with different evidence.
16
16
  license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
17
17
  metadata:
18
- version: "0.17.3"
18
+ version: "0.18.0"
19
19
  compatibility: >-
20
20
  Requires an initialized full Project Observatory installation to persist
21
21
  records. Uses its MCP server when available in the current host; the bundled
@@ -10,7 +10,7 @@ description: >-
10
10
  provider permissions or choosing a project's authentication architecture.
11
11
  license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
12
12
  metadata:
13
- version: "0.17.3"
13
+ version: "0.18.0"
14
14
  compatibility: >-
15
15
  Requires an initialized full Project Observatory installation, Python 3.11+
16
16
  and local shell access on macOS or Linux. Provider operations additionally
@@ -30,7 +30,7 @@ names. Never ask the user to paste a credential into the conversation.
30
30
  2. Select the user's initialized `OBSERVATORY_HOME`. Run
31
31
  `project-observatory full doctor`. A missing workspace
32
32
  needs the documented onboarding before secret operations.
33
- 3. Run `"$OBSERVATORY_PYTHON" "$OBSERVATORY_ROOT/tools/skill_check.py" handling-secrets 0.17.3`
33
+ 3. Run `"$OBSERVATORY_PYTHON" "$OBSERVATORY_ROOT/tools/skill_check.py" handling-secrets 0.18.0`
34
34
  (`full agent install` records that interpreter; the stock macOS `python3` is too old).
35
35
  If stale, read the installed skill once and follow its compatible commands.
36
36
  Do not turn an unavailable version check into a retry loop.
@@ -15,7 +15,7 @@ description: >-
15
15
  the repository.
16
16
  license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
17
17
  metadata:
18
- version: "0.17.3"
18
+ version: "0.18.0"
19
19
  compatibility: >-
20
20
  Requires a full Project Observatory installation with organizations.json
21
21
  configured; the observatory MCP server or its local CLI. Creating the resource
@@ -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.1.29",
5
+ "version": "0.1.31",
6
6
  "description": "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",