@passioncode-ai/passioncode 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/LICENSE +22 -0
- package/README.md +69 -0
- package/SECURITY.md +42 -0
- package/bin/passioncode.js +65 -0
- package/family.json +45 -0
- package/lib/launcher.js +361 -0
- package/package.json +48 -0
- package/payload/.claude-plugin/marketplace.json +47 -0
- package/payload/manifest.json +77 -0
- package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +24 -0
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +175 -0
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/profile-selection.md +71 -0
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-bundle.md +62 -0
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/verification.md +55 -0
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +537 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +240 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard.md +32 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/events-and-notifications.md +44 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +63 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +32 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +83 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +58 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +373 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +380 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +663 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +226 -0
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +126 -0
- package/payload/plugins/observatory-log/.claude-plugin/plugin.json +19 -0
- package/payload/plugins/observatory-log/hooks/ask-why.py +98 -0
- package/payload/plugins/observatory-log/hooks/hooks.json +31 -0
- package/payload/plugins/observatory-log/hooks/record-turn.sh +81 -0
- package/payload/plugins/observatory-log/hooks/session-start.sh +27 -0
- package/payload/plugins/observatory-log/skills/explaining-changes/SKILL.md +111 -0
- package/payload/plugins/observatory-log/skills/handling-secrets/SKILL.md +115 -0
- package/payload/plugins/observatory-log/skills/tracking-resources/SKILL.md +87 -0
- package/payload/plugins/passioncode/.claude-plugin/plugin.json +13 -0
- package/payload/plugins/passioncode/hooks/hooks.json +10 -0
- package/payload/plugins/passioncode/hooks/probe.js +23 -0
- package/payload/plugins/passioncode/hooks/session-start.js +45 -0
- package/payload/plugins/passioncode/hooks/update-check.js +126 -0
- package/payload/plugins/passioncode/trust.json +6 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""A complete, minimal fabric-service/0.1 service built on fabric_service.py.
|
|
3
|
+
|
|
4
|
+
It is the worked example the skill points at, the target check_service.py tests
|
|
5
|
+
itself against, and the fixture Fabric Dashboards runs its end-to-end tests on.
|
|
6
|
+
|
|
7
|
+
sample_service.py serve --port 47190 --data-dir DIR [--id sample] [--instance default]
|
|
8
|
+
sample_service.py register --port 47190 --data-dir DIR [--services-dir DIR]
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import html
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
from http.server import BaseHTTPRequestHandler
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
import signal
|
|
20
|
+
import sys
|
|
21
|
+
import threading
|
|
22
|
+
from typing import Any, Dict, Optional
|
|
23
|
+
from urllib.parse import parse_qs, urlparse
|
|
24
|
+
|
|
25
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
26
|
+
import fabric_service as fs # noqa: E402
|
|
27
|
+
|
|
28
|
+
VERSION = "0.1.0"
|
|
29
|
+
PAGE = """<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
30
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"><title>{name}</title>
|
|
31
|
+
<style>body{{font:15px system-ui;margin:2rem;color:#e9e4ec;background:#0a070d}}li{{margin:.3rem 0}}</style>
|
|
32
|
+
</head><body><h1>{name}</h1><p>Status: {status}</p><ul>{rows}</ul></body></html>"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Service:
|
|
36
|
+
def __init__(self, args: argparse.Namespace):
|
|
37
|
+
self.id = args.id
|
|
38
|
+
self.instance = args.instance
|
|
39
|
+
self.name = args.name
|
|
40
|
+
self.port = args.port
|
|
41
|
+
self.data = Path(args.data_dir)
|
|
42
|
+
self.token_file = Path(args.token_file) if args.token_file else self.data / "service.token"
|
|
43
|
+
self.degraded = [{"source": "demo", "reason": args.degraded}] if args.degraded else []
|
|
44
|
+
self.started_at = fs.now_iso()
|
|
45
|
+
self.build = {"commit": os.environ.get("FABRIC_BUILD_COMMIT", "0000000"), "builtAt": self.started_at}
|
|
46
|
+
|
|
47
|
+
def start(self) -> None:
|
|
48
|
+
# Rule: the lock comes before ANY side effect, including creating the token.
|
|
49
|
+
self.lock = fs.hold_single_instance(self.data)
|
|
50
|
+
self.token = fs.ensure_token(self.token_file)
|
|
51
|
+
self.log = fs.JsonlEventLog(self.data / "events.jsonl")
|
|
52
|
+
self.codes = fs.LoginCodes(self.data / "auth")
|
|
53
|
+
self.log.append("service.started", "info", "%s started on build %s." % (self.name, self.build["commit"]))
|
|
54
|
+
|
|
55
|
+
def well_known(self) -> Dict[str, Any]:
|
|
56
|
+
return fs.build_well_known(
|
|
57
|
+
service_id=self.id, instance=self.instance, name=self.name, version=VERSION, build=self.build,
|
|
58
|
+
started_at=self.started_at, status="ready", degraded=self.degraded,
|
|
59
|
+
summary=[{"label": "Events", "value": len(self.log.fetch(None, fs.EVENTS_MAX_LIMIT))}],
|
|
60
|
+
surfaces={"dashboard": {"path": "/", "login": True}, "events": {"path": "/fabric/v1/events"}},
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def make_handler(svc: Service):
|
|
65
|
+
class Handler(BaseHTTPRequestHandler):
|
|
66
|
+
server_version = "fabric-sample/" + VERSION
|
|
67
|
+
|
|
68
|
+
def log_message(self, fmt: str, *args: Any) -> None: # quiet by default
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
def _send(self, status: int, body: Any = None, headers: Optional[Dict[str, str]] = None,
|
|
72
|
+
content_type: str = "application/json") -> None:
|
|
73
|
+
payload = b""
|
|
74
|
+
if body is not None:
|
|
75
|
+
payload = body.encode() if isinstance(body, str) else json.dumps(body).encode()
|
|
76
|
+
self.send_response(status)
|
|
77
|
+
self.send_header("Cache-Control", "no-store")
|
|
78
|
+
self.send_header("X-Content-Type-Options", "nosniff")
|
|
79
|
+
self.send_header("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'self'")
|
|
80
|
+
if body is not None:
|
|
81
|
+
self.send_header("Content-Type", content_type)
|
|
82
|
+
self.send_header("Content-Length", str(len(payload)))
|
|
83
|
+
for key, value in (headers or {}).items():
|
|
84
|
+
self.send_header(key, value)
|
|
85
|
+
self.end_headers()
|
|
86
|
+
if payload:
|
|
87
|
+
self.wfile.write(payload)
|
|
88
|
+
|
|
89
|
+
def _guard(self) -> bool:
|
|
90
|
+
reason = fs.check_request(svc.port, self.headers.get("Host"), self.headers.get("Origin"),
|
|
91
|
+
self.headers.get("Sec-Fetch-Site"))
|
|
92
|
+
if reason:
|
|
93
|
+
self._send(403, {"error": reason})
|
|
94
|
+
return False
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
def _token_ok(self) -> bool:
|
|
98
|
+
if fs.token_matches(self.headers.get("Authorization"), svc.token):
|
|
99
|
+
return True
|
|
100
|
+
self._send(401, {"error": "The service token is required."}, {"WWW-Authenticate": "Bearer"})
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
def _session_ok(self) -> bool:
|
|
104
|
+
return svc.codes.session_valid(fs.cookie_value(self.headers.get("Cookie")))
|
|
105
|
+
|
|
106
|
+
def do_GET(self) -> None: # noqa: N802
|
|
107
|
+
if not self._guard():
|
|
108
|
+
return
|
|
109
|
+
url = urlparse(self.path)
|
|
110
|
+
query = parse_qs(url.query)
|
|
111
|
+
if url.path == "/.well-known/fabric-service":
|
|
112
|
+
return self._send(200, svc.well_known())
|
|
113
|
+
if url.path == "/fabric/v1/events":
|
|
114
|
+
if not self._token_ok():
|
|
115
|
+
return
|
|
116
|
+
try:
|
|
117
|
+
limit = fs.parse_limit((query.get("limit") or [None])[0])
|
|
118
|
+
page = fs.events_page(svc.log.fetch, (query.get("after") or [None])[0], limit)
|
|
119
|
+
except fs.ServiceError as exc:
|
|
120
|
+
return self._send(400, {"error": str(exc)})
|
|
121
|
+
return self._send(200, page)
|
|
122
|
+
if url.path == "/fabric/v1/login":
|
|
123
|
+
cookie = svc.codes.redeem((query.get("code") or [None])[0])
|
|
124
|
+
if not cookie:
|
|
125
|
+
return self._send(403, "This sign-in link was already used or has expired.", content_type="text/plain")
|
|
126
|
+
return self._send(302, None, {"Location": "/", "Set-Cookie": fs.session_cookie_header(cookie)})
|
|
127
|
+
if url.path == "/":
|
|
128
|
+
if not self._session_ok():
|
|
129
|
+
return self._send(401, "Open this dashboard from Fabric Dashboards.", content_type="text/plain")
|
|
130
|
+
rows = "".join("<li>%s — %s</li>" % (html.escape(e["at"]), html.escape(e["text"]))
|
|
131
|
+
for e in reversed(svc.log.fetch(None, 20)))
|
|
132
|
+
return self._send(200, PAGE.format(name=html.escape(svc.name), status="ready", rows=rows),
|
|
133
|
+
content_type="text/html; charset=utf-8")
|
|
134
|
+
self._send(404, {"error": "Not found."})
|
|
135
|
+
|
|
136
|
+
def do_POST(self) -> None: # noqa: N802
|
|
137
|
+
if not self._guard():
|
|
138
|
+
return
|
|
139
|
+
url = urlparse(self.path)
|
|
140
|
+
if url.path == "/fabric/v1/login-code":
|
|
141
|
+
if not self._token_ok():
|
|
142
|
+
return
|
|
143
|
+
return self._send(200, svc.codes.issue())
|
|
144
|
+
if url.path == "/api/emit":
|
|
145
|
+
if not (self._session_ok() or fs.token_matches(self.headers.get("Authorization"), svc.token)):
|
|
146
|
+
return self._send(401, {"error": "Sign in first."})
|
|
147
|
+
if self.headers.get("X-Fabric-Request") != "1":
|
|
148
|
+
return self._send(403, {"error": "Missing request header."})
|
|
149
|
+
length = min(int(self.headers.get("Content-Length") or 0), 65536)
|
|
150
|
+
body = json.loads(self.rfile.read(length) or b"{}")
|
|
151
|
+
event = svc.log.append(body.get("kind", "demo.note"), body.get("level", "info"),
|
|
152
|
+
body.get("text", "A note from the sample service."),
|
|
153
|
+
notify=bool(body.get("notify")), link=body.get("link"))
|
|
154
|
+
return self._send(200, event)
|
|
155
|
+
self._send(404, {"error": "Not found."})
|
|
156
|
+
|
|
157
|
+
return Handler
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def make_server(svc: Service) -> fs.LoopbackHTTPServer:
|
|
161
|
+
return fs.LoopbackHTTPServer(("127.0.0.1", svc.port), make_handler(svc))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def serve(args: argparse.Namespace) -> int:
|
|
165
|
+
svc = Service(args)
|
|
166
|
+
svc.start()
|
|
167
|
+
server = make_server(svc)
|
|
168
|
+
|
|
169
|
+
def stop(signum: int, _frame: Any) -> None:
|
|
170
|
+
svc.log.append("service.stopping", "info", "%s is stopping." % svc.name)
|
|
171
|
+
threading.Thread(target=server.shutdown, daemon=True).start()
|
|
172
|
+
|
|
173
|
+
signal.signal(signal.SIGTERM, stop)
|
|
174
|
+
signal.signal(signal.SIGINT, stop)
|
|
175
|
+
try:
|
|
176
|
+
server.serve_forever(poll_interval=0.2)
|
|
177
|
+
finally:
|
|
178
|
+
server.server_close()
|
|
179
|
+
svc.lock.release()
|
|
180
|
+
return 0
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def register(args: argparse.Namespace) -> int:
|
|
184
|
+
data = Path(args.data_dir)
|
|
185
|
+
descriptor = {
|
|
186
|
+
"protocol": fs.PROTOCOL, "id": args.id, "instance": args.instance, "name": args.name,
|
|
187
|
+
"summary": "Sample fabric-service/0.1 service.",
|
|
188
|
+
"origin": "http://127.0.0.1:%d" % args.port,
|
|
189
|
+
"auth": {"tokenFile": str(Path(args.token_file) if args.token_file else data / "service.token")},
|
|
190
|
+
"lifecycle": {"manager": "launchd", "label": args.label, "plist": args.plist} if args.label
|
|
191
|
+
else {"manager": "none"},
|
|
192
|
+
"paths": {"data": str(data), "logs": []},
|
|
193
|
+
"installedAt": fs.now_iso(), "installedBy": "sample_service.py register",
|
|
194
|
+
}
|
|
195
|
+
try:
|
|
196
|
+
path = fs.write_descriptor(descriptor, Path(args.services_dir) if args.services_dir else None)
|
|
197
|
+
except fs.ServiceError as exc:
|
|
198
|
+
print(str(exc), file=sys.stderr)
|
|
199
|
+
return 1
|
|
200
|
+
print(path)
|
|
201
|
+
return 0
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def main(argv: Optional[list] = None) -> int:
|
|
205
|
+
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
206
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
207
|
+
for name in ("serve", "register"):
|
|
208
|
+
p = sub.add_parser(name)
|
|
209
|
+
p.add_argument("--port", type=int, required=True)
|
|
210
|
+
p.add_argument("--data-dir", required=True)
|
|
211
|
+
p.add_argument("--token-file")
|
|
212
|
+
p.add_argument("--id", default="sample")
|
|
213
|
+
p.add_argument("--instance", default="default")
|
|
214
|
+
p.add_argument("--name", default="Sample Service")
|
|
215
|
+
if name == "serve":
|
|
216
|
+
p.add_argument("--degraded", help="report one degraded source with this reason")
|
|
217
|
+
else:
|
|
218
|
+
p.add_argument("--services-dir")
|
|
219
|
+
p.add_argument("--label")
|
|
220
|
+
p.add_argument("--plist")
|
|
221
|
+
args = parser.parse_args(argv)
|
|
222
|
+
return serve(args) if args.command == "serve" else register(args)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
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". Runs the intake grill (capability, named consumer, workflow-or-agent, MCP/A2A/local-runner profile, effect declarations), distils source projects into a knowledge pack whose recorded failures become planted eval fixtures, scaffolds the pinned contract bundle, and sets the two-clock eval expectation with the conformance report. NOT for adapting an existing project (use adapting-projects-to-fabric), building the Fabric host or orchestrator, or generic agent design where Fabric compatibility is not part of the request.
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned private fabric-agent-contract checkout. 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
|
+
metadata:
|
|
7
|
+
author: passioncode-ai
|
|
8
|
+
version: "0.4.2"
|
|
9
|
+
contract-version: "0.1.0"
|
|
10
|
+
contract-commit: "20a818e648a4c09a60df0126d11626922e8b9094"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Creating Fabric-compatible agents
|
|
14
|
+
|
|
15
|
+
Design a new agent so that Fabric compatibility is a property of its first commit, not a
|
|
16
|
+
retrofit. The sibling skill `adapting-projects-to-fabric` wraps an interface that already
|
|
17
|
+
exists; this one runs the stages that come *before* an interface exists — the intake
|
|
18
|
+
grill and the knowledge intake — then hands the scaffolding and verification to the same
|
|
19
|
+
pinned machinery, so both doors lead into one pipeline.
|
|
20
|
+
|
|
21
|
+
## Boundary
|
|
22
|
+
|
|
23
|
+
Use this skill to design and build a new provider. Do not use it to:
|
|
24
|
+
|
|
25
|
+
- adapt a project that already has a stable API, MCP/A2A surface, or CLI — that is
|
|
26
|
+
`adapting-projects-to-fabric`, and entering there skips nothing;
|
|
27
|
+
- design the Fabric host, registry, scheduler, admission service, or binding runtime;
|
|
28
|
+
- build a generic agent with no Fabric requirement — nothing here helps a project that
|
|
29
|
+
will never be a provider;
|
|
30
|
+
- claim admission: the deliverable is an admission-ready provider bundle, never a
|
|
31
|
+
connected provider.
|
|
32
|
+
|
|
33
|
+
## Step 0 — the intake grill
|
|
34
|
+
|
|
35
|
+
No file is created until every row has an answer. An answer of "later" fails the grill.
|
|
36
|
+
|
|
37
|
+
| Question | The gate |
|
|
38
|
+
|---|---|
|
|
39
|
+
| **What capability?** | one capability, named as `domain.action` (optionally `@surface`), with input and output shapes statable now |
|
|
40
|
+
| **Who calls it?** | **a named consumer** — the project, schedule, or workflow that will actually invoke this capability. No consumer, no agent: a role that "would be useful" is a catalogue entry, not a build order |
|
|
41
|
+
| **Workflow or agent?** | if every step can be named now, build a deterministic workflow behind the capability and skip the autonomy — an agent buys flexibility with latency, cost, and a new failure class, and that price needs a reason |
|
|
42
|
+
| **Which profile?** | `mcp` when Fabric owns planning and retries and the capability has a schema; `a2a` when the provider owns its task lifecycle end to end; `local-runner` when it is a terminal agent Fabric drives. When unsure, the sibling skill's profile-selection reference is the decision record |
|
|
43
|
+
| **What effects?** | declare anything money-, deletion-, or publication-adjacent now — the host's floor and grants are designed against these declarations, and one discovered late invalidates the admission |
|
|
44
|
+
| **Which tenancy?** | record whether the agent assumes one operator; the assumption is cheap to write down and expensive to excavate |
|
|
45
|
+
|
|
46
|
+
## Step 1 — the knowledge intake
|
|
47
|
+
|
|
48
|
+
If the estate holds prior art — a project that did this job, an audit that names its
|
|
49
|
+
failures, a retro — distil it before writing code:
|
|
50
|
+
|
|
51
|
+
1. Name the sources: repositories, `docs/`, audits, retrospectives, with references.
|
|
52
|
+
2. Extract **patterns** (what worked, each citing its origin) and **traps** (recorded
|
|
53
|
+
failures and dead ends).
|
|
54
|
+
3. **Convert every trap into a planted eval fixture.** The new agent is not done until
|
|
55
|
+
it has been watched rejecting the exact defects its predecessors were burned by —
|
|
56
|
+
knowledge transfers as a check, not as prose.
|
|
57
|
+
4. Record the pack in the new project as `docs/knowledge-pack.md` with its sources, so
|
|
58
|
+
the provenance of every borrowed decision survives the person who borrowed it.
|
|
59
|
+
|
|
60
|
+
Skipping this step because there is no prior art is legal; skipping it because reading
|
|
61
|
+
is slower than generating is how the same collector truncates at the same row limit
|
|
62
|
+
twice.
|
|
63
|
+
|
|
64
|
+
## Step 2 — the project skeleton
|
|
65
|
+
|
|
66
|
+
Create the minimal project: repository, `README.md` naming the capability and its
|
|
67
|
+
consumer, and the eval **observables** — for each requirement, the pass/fail criterion
|
|
68
|
+
that would show it met, written now, before the implementation. The input corpus is NOT
|
|
69
|
+
authored now: it grows from real traces once the provider runs, and the step-1 fixtures
|
|
70
|
+
are its seed because a source project's recorded failures count as production.
|
|
71
|
+
|
|
72
|
+
## Step 3 — scaffold the provider bundle
|
|
73
|
+
|
|
74
|
+
Reuse the sibling skill's scaffolder against the new skeleton with the profile chosen at
|
|
75
|
+
step 0:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python3 <plugin-dir>/skills/adapting-projects-to-fabric/scripts/adapt_project.py \
|
|
79
|
+
scaffold <project-root> --profile <mcp|a2a|local-runner> \
|
|
80
|
+
--provider-id <stable-uri> --provider-name "<name>" \
|
|
81
|
+
--capability-id <stable-uri> --capability-name <domain.action> \
|
|
82
|
+
--schema-base <immutable-base-uri>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Pin exactly contract `0.1.0` at commit `20a818e648a4c09a60df0126d11626922e8b9094` and
|
|
86
|
+
read the pinned guide before implementing protocol details. If this skill is installed
|
|
87
|
+
without its sibling, the scaffolder is absent: create the bundle by hand from the pinned
|
|
88
|
+
contract's `docs/guides/connecting-compatible-agents.md` and mark the structural check
|
|
89
|
+
`NOT_RUN` — do not reconstruct normative rules from memory.
|
|
90
|
+
|
|
91
|
+
The generated placeholders are deliberate blockers; a bundle still containing one is not
|
|
92
|
+
ready for admission.
|
|
93
|
+
|
|
94
|
+
## Step 4 — implement against the observables
|
|
95
|
+
|
|
96
|
+
If the agent keeps running on the operator's computer — it answers other agents, runs
|
|
97
|
+
long jobs or shows a dashboard — build it as a service with the sibling skill
|
|
98
|
+
`building-fabric-services`: its instance lock, launchd plist, descriptor, well-known
|
|
99
|
+
document and events feed are part of this step, not a later retrofit. If that skill is
|
|
100
|
+
absent, apply the `fabric-service/0.1` rules from the pinned contract's
|
|
101
|
+
`docs/specification/service.md` by hand and mark its probe `NOT_RUN`.
|
|
102
|
+
|
|
103
|
+
Build the capability behind the chosen surface. Keep model choice and internal reasoning
|
|
104
|
+
outside the contract; expose typed outcomes, evidence, and protocol-visible state. Every
|
|
105
|
+
provider output is untrusted until schemas and semantic assertions pass. The step-2
|
|
106
|
+
observables are the definition of done; an observable attached after the code exists
|
|
107
|
+
lets the output decide what counts as success.
|
|
108
|
+
|
|
109
|
+
## Step 5 — check, report, and set the canary expectation
|
|
110
|
+
|
|
111
|
+
Run the sibling skill's `check` (structural, then exact when the contract checkout is
|
|
112
|
+
available) and complete `fabric/FABRIC-CONFORMANCE.md` with dated receipts, exactly as
|
|
113
|
+
its workflow steps 5–7 prescribe — this skill adds no second verification path on
|
|
114
|
+
purpose. Then record one expectation the report must carry:
|
|
115
|
+
|
|
116
|
+
> When a Fabric host admits this provider, it enters under a **canary binding** — a
|
|
117
|
+
> checker on its output and a budget cap — regardless of who wrote it. Unsupervised
|
|
118
|
+
> operation is a later, recorded promotion citing eval results and run history.
|
|
119
|
+
|
|
120
|
+
## Completion format
|
|
121
|
+
|
|
122
|
+
Report: capability, consumer, profile with the step-0 grill answers; the knowledge pack
|
|
123
|
+
and which traps became fixtures; created files; contract version and commit; the gate
|
|
124
|
+
table with `PASS`, `FAIL`, `NOT_RUN`, or `NOT_VERIFIED`; remaining placeholders; and the
|
|
125
|
+
exact next command. Never summarize the outcome as "Fabric-compatible" unless every
|
|
126
|
+
required admission gate passed against the real provider.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
|
+
"name": "observatory-log",
|
|
4
|
+
"displayName": "Observatory Log",
|
|
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.12.2",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": {
|
|
9
|
+
"name": "Sergey Sheleg",
|
|
10
|
+
"url": "https://sshlg.me/"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/passioncode-ai/project-observatory-dashboard",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"observability",
|
|
15
|
+
"memory",
|
|
16
|
+
"git",
|
|
17
|
+
"ledger"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Turn a record_turn.py result into the Stop hook's structured output.
|
|
3
|
+
|
|
4
|
+
Standard library only, and silent on anything unexpected: a hook that reports
|
|
5
|
+
its own confusion as a failure teaches the operator to disable hooks.
|
|
6
|
+
|
|
7
|
+
It never blocks. The facts are already on the record without the agent's
|
|
8
|
+
cooperation, so the only thing left to ask for is the `why` — and asking is
|
|
9
|
+
worth more than a veto that costs a turn. An absent `why` stays visible in the
|
|
10
|
+
ledger as a proposed record with a null field, which is accountability without
|
|
11
|
+
obstruction.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
OWNER = "agent:claude-code"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main() -> int:
|
|
21
|
+
try:
|
|
22
|
+
r = json.load(sys.stdin)
|
|
23
|
+
except Exception:
|
|
24
|
+
return 0
|
|
25
|
+
if not r.get("recorded"):
|
|
26
|
+
# A FAULT IS NOT A QUIET TURN. This returned 0 for every `recorded:
|
|
27
|
+
# false`, and the recorder's reasons are two different kinds: "nothing
|
|
28
|
+
# changed" is an answer about the work, while `IllegalTransition:
|
|
29
|
+
# observed -> proposed` is the recorder failing. Measured 2026-09-07:
|
|
30
|
+
# once `tools/corroborate.py` promoted a session's record, every later
|
|
31
|
+
# turn of that session raised that error and said nothing — roughly
|
|
32
|
+
# seventy-two turns in one session, and the only visible sign was a
|
|
33
|
+
# ledger row that had stopped moving.
|
|
34
|
+
#
|
|
35
|
+
# The recorder marks which kind it is; this only has to stop hiding it.
|
|
36
|
+
reason = r.get("reason") or ""
|
|
37
|
+
if not r.get("fault"):
|
|
38
|
+
return 0 # nothing changed, not watched, or explained
|
|
39
|
+
print(json.dumps({"systemMessage":
|
|
40
|
+
"Observatory could NOT record this turn: " + reason +
|
|
41
|
+
"\n\nThe facts of this turn are not in the ledger. "
|
|
42
|
+
"`store/raw/record-turn.json` holds the same reason, and "
|
|
43
|
+
"`./observatory.py findings` raises it.",
|
|
44
|
+
"suppressOutput": True}))
|
|
45
|
+
return 0
|
|
46
|
+
if r.get("hasWhy"):
|
|
47
|
+
return 0 # already explained
|
|
48
|
+
|
|
49
|
+
mid = r.get("memoryId")
|
|
50
|
+
rev = r.get("revision")
|
|
51
|
+
project = r.get("project", "this project")
|
|
52
|
+
files = r.get("files", 0)
|
|
53
|
+
plural = "" if files == 1 else "s"
|
|
54
|
+
unpushed = r.get("unpushed") or 0
|
|
55
|
+
extra = f", {unpushed} unpushed commit(s)" if unpushed else ""
|
|
56
|
+
|
|
57
|
+
message = (
|
|
58
|
+
f"Observatory recorded the facts of this turn in {project}: "
|
|
59
|
+
f"{files} file{plural} changed, +{r.get('insertions', 0)}/-{r.get('deletions', 0)}"
|
|
60
|
+
f"{extra}. Stored as {mid}@{rev}, state proposed, with no `why`.\n\n"
|
|
61
|
+
"The diff already says WHAT changed. Add WHY — one or two sentences a "
|
|
62
|
+
"reader in six months could not reconstruct from the diff: the constraint "
|
|
63
|
+
"that forced the shape, the alternative rejected, the trap avoided.\n\n"
|
|
64
|
+
f" observatory_record(owner=\"{OWNER}\", memory_id=\"{mid}\", "
|
|
65
|
+
f"expected_revision={rev}, statement=<the same claim>, why=<the reason>)\n\n"
|
|
66
|
+
"If the change genuinely needs no explanation — a typo, a rename — say so "
|
|
67
|
+
"and skip it. An empty `why` on a trivial edit is honest; an invented one "
|
|
68
|
+
"is worse than nothing, because it will be read as true."
|
|
69
|
+
)
|
|
70
|
+
# THE NUDGE AT THE MOMENT OF WORK. The agent is already inside this project;
|
|
71
|
+
# rebuilding its graph or touching its note costs least right now. Thresholds
|
|
72
|
+
# match the board's grace (7 days), and None means the artefact does not
|
|
73
|
+
# exist — not adopting graphify is a choice this hook does not argue with.
|
|
74
|
+
stale_bits = []
|
|
75
|
+
g = r.get("graphAgeDays")
|
|
76
|
+
if isinstance(g, int) and g > 7:
|
|
77
|
+
stale_bits.append(f"its code graph is {g} days old — refresh it: "
|
|
78
|
+
f"/graphify . --update")
|
|
79
|
+
w = r.get("wikiAgeDays")
|
|
80
|
+
if isinstance(w, int) and w > 7:
|
|
81
|
+
stale_bits.append(f"its wiki notes were last touched {w} days ago — if this "
|
|
82
|
+
f"work changed what the project IS (modules, scope, "
|
|
83
|
+
f"stack), update the overview note too")
|
|
84
|
+
if stale_bits:
|
|
85
|
+
message += ("\n\nWhile you are here — this project's recorded knowledge "
|
|
86
|
+
"is behind its code:\n - " + "\n - ".join(stale_bits))
|
|
87
|
+
print(json.dumps({"systemMessage": message, "suppressOutput": True},
|
|
88
|
+
ensure_ascii=False))
|
|
89
|
+
return 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
try:
|
|
94
|
+
raise SystemExit(main())
|
|
95
|
+
except SystemExit:
|
|
96
|
+
raise
|
|
97
|
+
except Exception:
|
|
98
|
+
raise SystemExit(0)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Optional Claude Code hooks: record changes in watched Git projects and request missing reasoning. SessionStart reports brief actionable context. Missing workspace, interpreter or watched project degrades quietly; hooks do not block a turn. Project names and paths stay in the private workspace.",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"SessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"shell": "bash",
|
|
10
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh\"",
|
|
11
|
+
"timeout": 15,
|
|
12
|
+
"statusMessage": "What the observatory knows about this project"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"Stop": [
|
|
18
|
+
{
|
|
19
|
+
"hooks": [
|
|
20
|
+
{
|
|
21
|
+
"type": "command",
|
|
22
|
+
"shell": "bash",
|
|
23
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/record-turn.sh\"",
|
|
24
|
+
"timeout": 20,
|
|
25
|
+
"statusMessage": "Recording what changed"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
set -uo pipefail
|
|
10
|
+
|
|
11
|
+
emit() { printf '%s\n' "$1"; exit 0; }
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if [ -t 0 ]; then payload=""; else payload=$(cat 2>/dev/null || true); fi
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
root="${OBSERVATORY_ROOT:-}"
|
|
39
|
+
if [ -z "$root" ] && [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
|
|
40
|
+
c="${CLAUDE_PLUGIN_ROOT}"
|
|
41
|
+
for _ in 1 2 3 4 5 6; do
|
|
42
|
+
[ -f "$c/observatory.py" ] && { root="$c"; break; }
|
|
43
|
+
n="$(cd "$c/.." 2>/dev/null && pwd)" || break
|
|
44
|
+
[ "$n" = "$c" ] && break # reached the filesystem root
|
|
45
|
+
c="$n"
|
|
46
|
+
done
|
|
47
|
+
fi
|
|
48
|
+
[ -n "$root" ] || exit 0 # no checkout: silent, not an error
|
|
49
|
+
[ -f "$root/tools/record_turn.py" ] || exit 0 # older checkout: silent
|
|
50
|
+
|
|
51
|
+
py="${OBSERVATORY_PYTHON:-$root/.venv/bin/python}" # set by `full agent install`
|
|
52
|
+
[ -x "$py" ] || py="$(command -v python3 2>/dev/null)"
|
|
53
|
+
[ -n "$py" ] || exit 0 # no interpreter: silent
|
|
54
|
+
|
|
55
|
+
read_field() { # read_field <key>
|
|
56
|
+
[ -n "$payload" ] || return 0
|
|
57
|
+
printf '%s' "$payload" | "$py" -c '
|
|
58
|
+
import json, sys
|
|
59
|
+
try:
|
|
60
|
+
d = json.load(sys.stdin)
|
|
61
|
+
except Exception:
|
|
62
|
+
sys.exit(0)
|
|
63
|
+
print(d.get(sys.argv[1]) or "")
|
|
64
|
+
' "$1" 2>/dev/null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
[ "$(read_field stop_hook_active)" = "True" ] && exit 0
|
|
71
|
+
[ "$(read_field stop_hook_active)" = "true" ] && exit 0
|
|
72
|
+
|
|
73
|
+
cwd="$(read_field cwd)"; [ -n "$cwd" ] || cwd="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
74
|
+
session="$(read_field session_id)"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
result="$("$py" "$root/tools/record_turn.py" --cwd "$cwd" --session-id "$session" 2>/dev/null)"
|
|
78
|
+
[ -n "$result" ] || exit 0
|
|
79
|
+
|
|
80
|
+
printf '%s' "$result" | "$py" "${CLAUDE_PLUGIN_ROOT:-$(dirname "$0")/..}/hooks/ask-why.py" 2>/dev/null
|
|
81
|
+
exit 0
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
set -uo pipefail
|
|
10
|
+
if [ -t 0 ]; then payload=""; else payload=$(cat 2>/dev/null || true); fi
|
|
11
|
+
root="${OBSERVATORY_ROOT:-}"
|
|
12
|
+
if [ -z "$root" ] && [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
|
|
13
|
+
c="${CLAUDE_PLUGIN_ROOT}"
|
|
14
|
+
for _ in 1 2 3 4 5 6; do
|
|
15
|
+
[ -f "$c/observatory.py" ] && { root="$c"; break; }
|
|
16
|
+
n="$(cd "$c/.." 2>/dev/null && pwd)" || break
|
|
17
|
+
[ "$n" = "$c" ] && break
|
|
18
|
+
c="$n"
|
|
19
|
+
done
|
|
20
|
+
fi
|
|
21
|
+
[ -n "$root" ] || exit 0
|
|
22
|
+
[ -f "$root/tools/session_start.py" ] || exit 0
|
|
23
|
+
py="${OBSERVATORY_PYTHON:-$root/.venv/bin/python}" # set by `full agent install`
|
|
24
|
+
[ -x "$py" ] || py="$(command -v python3 2>/dev/null)"
|
|
25
|
+
[ -n "$py" ] || exit 0
|
|
26
|
+
printf '%s' "$payload" | "$py" "$root/tools/session_start.py" 2>/dev/null || true
|
|
27
|
+
exit 0
|