@event4u/agent-config 1.36.1 → 1.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,274 @@
1
+ """Pack agent-config content into a Worker-bundle JSON blob.
2
+
3
+ Walks `.agent-src/skills/`, `.agent-src/commands/`, `.agent-src/rules/`,
4
+ `docs/guidelines/`, `.agent-src/contexts/` via the same Python loaders
5
+ that drive the local stdio kernel, emits one JSON blob and a sidecar
6
+ manifest for `workers/mcp/`.
7
+
8
+ Outputs (relative to repo root):
9
+ - `workers/mcp/content.json` — uncompressed, bundled by `wrangler deploy`.
10
+ - `workers/mcp/content.json.gz` — gzipped archival copy for R2.
11
+ - `workers/mcp/manifest.json` — manifest only (RCA / R2 sidecar).
12
+
13
+ Hard-fail thresholds (Phase 2-5 council verdict D2):
14
+ - Uncompressed JSON > 2 MB → SystemExit(1).
15
+ - Empty content (zero URIs) → SystemExit(2). Catches a broken
16
+ `.agent-src/` tree before deploy.
17
+
18
+ Cloud signature divergence vs local kernel (`metadata.compute_skill_set_signature`):
19
+ - Local kernel: SHA-256 over `(uri, mtime)` pairs — reproducible only
20
+ within one filesystem.
21
+ - This packer: SHA-256 over `(uri, body)` pairs — reproducible across
22
+ CI runs, machines, and re-clones. Same 12-char prefix.
23
+
24
+ Governed by `docs/contracts/mcp-cloud-scope.md` §A0-cloud invariant 5.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import argparse
29
+ import gzip
30
+ import hashlib
31
+ import json
32
+ import os
33
+ import subprocess
34
+ import sys
35
+ from datetime import datetime, timezone
36
+ from pathlib import Path
37
+ from typing import Any
38
+
39
+ # Re-use the local kernel's loaders so the live-replay baseline stays
40
+ # trivially comparable.
41
+ _SCRIPTS_DIR = Path(__file__).resolve().parent
42
+ sys.path.insert(0, str(_SCRIPTS_DIR))
43
+
44
+ from mcp_server.prompts import scan_commands, scan_skills # noqa: E402
45
+ from mcp_server.resources import scan_contexts, scan_guidelines, scan_rules # noqa: E402
46
+
47
+ SCHEMA_VERSION = 1
48
+ PACKER_VERSION = "1.0.0"
49
+ # Worker bundle is the compact JSON; gzipped copy lives in R2. Cloudflare's
50
+ # compressed-bundle limit is 3 MB (free) / 10 MB (paid); 778 KB gz today
51
+ # (438 entries) leaves ample headroom. Hard-fail at 5 MB uncompressed so
52
+ # the build dies before the Worker upload does.
53
+ MAX_UNCOMPRESSED_BYTES = 5 * 1024 * 1024
54
+
55
+
56
+ def _repo_root() -> Path:
57
+ return Path(__file__).resolve().parent.parent
58
+
59
+
60
+ def _git_sha(root: Path) -> str:
61
+ """Resolve HEAD SHA. Falls back to env var, then to all-zeros."""
62
+ for env_var in ("GITHUB_SHA", "CI_COMMIT_SHA", "GIT_COMMIT"):
63
+ sha = os.environ.get(env_var)
64
+ if sha and len(sha) >= 7:
65
+ return sha
66
+ try:
67
+ out = subprocess.run(
68
+ ["git", "rev-parse", "HEAD"],
69
+ cwd=root,
70
+ capture_output=True,
71
+ text=True,
72
+ check=True,
73
+ timeout=5,
74
+ )
75
+ return out.stdout.strip()
76
+ except (subprocess.SubprocessError, FileNotFoundError):
77
+ return "0" * 40
78
+
79
+
80
+ def _package_version(root: Path) -> str:
81
+ data = json.loads((root / "package.json").read_text(encoding="utf-8"))
82
+ return str(data.get("version", "0.0.0"))
83
+
84
+
85
+ def _collect_entries(root: Path) -> tuple[dict[str, dict[str, Any]], list[str]]:
86
+ """Run all 5 scanners and project entries into the wire shape."""
87
+ uris: dict[str, dict[str, Any]] = {}
88
+ errors: list[str] = []
89
+
90
+ skills, e = scan_skills(root)
91
+ errors.extend(e)
92
+ for s in skills:
93
+ key = f"skill://{s.name}"
94
+ uris[key] = {
95
+ "uri": key,
96
+ "name": s.name,
97
+ "description": s.description,
98
+ "body": s.body,
99
+ "source": s.source,
100
+ "kind": "skill",
101
+ }
102
+
103
+ commands, e = scan_commands(root)
104
+ errors.extend(e)
105
+ for c in commands:
106
+ key = f"command://{c.name.replace(':', '.')}"
107
+ uris[key] = {
108
+ "uri": key,
109
+ "name": c.name,
110
+ "description": c.description,
111
+ "body": c.body,
112
+ "source": c.source,
113
+ "kind": "command",
114
+ }
115
+
116
+ for scan in (scan_rules, scan_guidelines, scan_contexts):
117
+ items, e = scan(root)
118
+ errors.extend(e)
119
+ for r in items:
120
+ uris[r.uri] = {
121
+ "uri": r.uri,
122
+ "name": r.name,
123
+ "description": r.description,
124
+ "body": r.body,
125
+ "source": r.source,
126
+ "kind": r.kind,
127
+ "mime_type": r.mime_type,
128
+ }
129
+
130
+ return uris, errors
131
+
132
+
133
+ def _content_signature(uris: dict[str, dict[str, Any]]) -> tuple[str, str]:
134
+ """SHA-256 over sorted (uri, body) pairs.
135
+
136
+ Returns (full_hex, 12-char prefix). The prefix is the wire-surface
137
+ `skillSetSignature`; the full hex is the diagnostic `content_hash_sha256`.
138
+ """
139
+ hasher = hashlib.sha256()
140
+ for uri in sorted(uris):
141
+ hasher.update(uri.encode("utf-8"))
142
+ hasher.update(b"\x00")
143
+ hasher.update(uris[uri]["body"].encode("utf-8"))
144
+ hasher.update(b"\x1e")
145
+ digest = hasher.hexdigest()
146
+ return digest, digest[:12]
147
+
148
+
149
+ def _count_kinds(uris: dict[str, dict[str, Any]]) -> dict[str, int]:
150
+ counts = {"skill": 0, "command": 0, "rule": 0, "guideline": 0, "context": 0}
151
+ for entry in uris.values():
152
+ kind = entry["kind"]
153
+ if kind in counts:
154
+ counts[kind] += 1
155
+ return counts
156
+
157
+
158
+
159
+ def _build_manifest(
160
+ *,
161
+ signature: str,
162
+ content_hash: str,
163
+ package_version: str,
164
+ git_sha: str,
165
+ built_at: str,
166
+ counts: dict[str, int],
167
+ ) -> dict[str, Any]:
168
+ short = git_sha[:7] if git_sha and git_sha != "0" * 40 else "unknown"
169
+ return {
170
+ "schema_version": SCHEMA_VERSION,
171
+ "signature": signature,
172
+ "content_hash_sha256": content_hash,
173
+ "package_version": package_version,
174
+ "release_key": f"v{package_version}-{short}",
175
+ "git_sha": git_sha,
176
+ "built_at": built_at,
177
+ "packer_version": PACKER_VERSION,
178
+ "content_uri_count": counts,
179
+ }
180
+
181
+
182
+ def pack(root: Path, out_dir: Path) -> dict[str, Any]:
183
+ """Run the full pack. Returns the manifest dict."""
184
+ uris, errors = _collect_entries(root)
185
+ if not uris:
186
+ sys.stderr.write("pack: empty content (zero URIs)\n")
187
+ for line in errors:
188
+ sys.stderr.write(f" - {line}\n")
189
+ raise SystemExit(2)
190
+
191
+ content_hash, signature = _content_signature(uris)
192
+ counts = _count_kinds(uris)
193
+ built_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
194
+ manifest = _build_manifest(
195
+ signature=signature,
196
+ content_hash=content_hash,
197
+ package_version=_package_version(root),
198
+ git_sha=_git_sha(root),
199
+ built_at=built_at,
200
+ counts=counts,
201
+ )
202
+
203
+ blob = {
204
+ "schema_version": SCHEMA_VERSION,
205
+ "uris": uris,
206
+ "manifest": manifest,
207
+ }
208
+ # Compact JSON for the bundle (saves ~20 KB vs indent=2). The R2
209
+ # archival copy is gzipped, so legibility there is moot.
210
+ payload = json.dumps(blob, ensure_ascii=False, sort_keys=True)
211
+ payload_bytes = payload.encode("utf-8")
212
+
213
+ if len(payload_bytes) > MAX_UNCOMPRESSED_BYTES:
214
+ sys.stderr.write(
215
+ f"pack: uncompressed content {len(payload_bytes)} bytes "
216
+ f"exceeds limit {MAX_UNCOMPRESSED_BYTES}\n"
217
+ )
218
+ raise SystemExit(1)
219
+
220
+ out_dir.mkdir(parents=True, exist_ok=True)
221
+ (out_dir / "content.json").write_bytes(payload_bytes)
222
+ # mtime=0 keeps the gzip header byte-stable across CI runs so the
223
+ # R2 archival copy hashes deterministically.
224
+ with open(out_dir / "content.json.gz", "wb") as raw:
225
+ with gzip.GzipFile(
226
+ fileobj=raw, mode="wb", compresslevel=9, mtime=0
227
+ ) as gz:
228
+ gz.write(payload_bytes)
229
+ (out_dir / "manifest.json").write_text(
230
+ json.dumps(manifest, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
231
+ encoding="utf-8",
232
+ )
233
+
234
+ if errors:
235
+ sys.stderr.write("pack: non-fatal frontmatter errors:\n")
236
+ for line in errors:
237
+ sys.stderr.write(f" - {line}\n")
238
+
239
+ return manifest
240
+
241
+
242
+ def main(argv: list[str] | None = None) -> int:
243
+ parser = argparse.ArgumentParser(description=__doc__)
244
+ parser.add_argument(
245
+ "--root", type=Path, default=_repo_root(), help="Repository root."
246
+ )
247
+ parser.add_argument(
248
+ "--out",
249
+ type=Path,
250
+ default=None,
251
+ help="Output directory (defaults to <root>/workers/mcp).",
252
+ )
253
+ parser.add_argument(
254
+ "--quiet", action="store_true", help="Suppress success summary."
255
+ )
256
+ args = parser.parse_args(argv)
257
+
258
+ out_dir = args.out or (args.root / "workers" / "mcp")
259
+ manifest = pack(args.root, out_dir)
260
+
261
+ if not args.quiet:
262
+ c = manifest["content_uri_count"]
263
+ sys.stderr.write(
264
+ f"pack: ok signature={manifest['signature']} "
265
+ f"release={manifest['release_key']} "
266
+ f"skills={c['skill']} commands={c['command']} "
267
+ f"rules={c['rule']} guidelines={c['guideline']} "
268
+ f"contexts={c['context']}\n"
269
+ )
270
+ return 0
271
+
272
+
273
+ if __name__ == "__main__":
274
+ raise SystemExit(main())
@@ -88,7 +88,7 @@ GENERIC_BOILERPLATE = [
88
88
  r"(?i)\blightweight yet powerful\b",
89
89
  ]
90
90
 
91
- OVERLOADED_LINE_THRESHOLD = 500
91
+ OVERLOADED_LINE_THRESHOLD = 750
92
92
  WEAK_QUICKSTART_LINE_GAP = 80
93
93
 
94
94
 
@@ -2948,6 +2948,13 @@ def main() -> int:
2948
2948
 
2949
2949
  paths = sorted(set(paths))
2950
2950
  if not paths:
2951
+ # Emit a valid empty payload when a structured format was requested
2952
+ # so downstream parsers (e.g. PR-summary workflows) don't fail on an
2953
+ # empty stdout. stderr keeps the human-readable note.
2954
+ if args.report:
2955
+ print(format_report([]))
2956
+ elif args.format == "json":
2957
+ print(format_json([]))
2951
2958
  print("No matching skill/rule files found.", file=sys.stderr)
2952
2959
  return 0
2953
2960