@event4u/agent-config 3.0.0 → 3.1.1

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 (208) hide show
  1. package/.agent-src/commands/install-via-agent.md +129 -0
  2. package/.agent-src/commands/video/from-script.md +1 -1
  3. package/.agent-src/commands/video.md +1 -1
  4. package/.agent-src/contexts/execution/cheap-question-mechanics.md +81 -0
  5. package/.agent-src/rules/caveman-speak.md +2 -2
  6. package/.agent-src/rules/context-hygiene.md +36 -0
  7. package/.agent-src/rules/engineering-safety-floor.md +102 -0
  8. package/.agent-src/rules/finance-safety-floor.md +114 -0
  9. package/.agent-src/rules/git-history-discipline.md +1 -1
  10. package/.agent-src/rules/no-cheap-questions.md +34 -32
  11. package/.agent-src/rules/provider-lifecycle-discipline.md +4 -4
  12. package/.agent-src/rules/strategy-safety-floor.md +114 -0
  13. package/.agent-src/skills/agents-md-thin-root/SKILL.md +15 -9
  14. package/.agent-src/skills/async-python-patterns/SKILL.md +1 -1
  15. package/.agent-src/skills/project-analysis-node-express/SKILL.md +1 -1
  16. package/.agent-src/skills/readme-reviewer/SKILL.md +52 -3
  17. package/.agent-src/skills/readme-writing/SKILL.md +52 -4
  18. package/.agent-src/skills/readme-writing-package/SKILL.md +48 -5
  19. package/.agent-src/skills/systematic-debugging/SKILL.md +41 -0
  20. package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  21. package/.agent-src/templates/hooks/pre-commit-frontmatter +66 -0
  22. package/.agent-src/templates/hooks/pre-commit-roadmap-progress +78 -39
  23. package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +4 -1
  24. package/.agent-src/templates/scripts/work_engine/orchestration.py +25 -11
  25. package/.claude-plugin/marketplace.json +2 -1
  26. package/AGENTS.md +10 -8
  27. package/CHANGELOG.md +233 -123
  28. package/README.md +165 -553
  29. package/config/agent-settings.template.yml +0 -7
  30. package/config/discovery/packs.yml +20 -0
  31. package/config/discovery/unassigned-artefacts.yml +2 -0
  32. package/config/gitignore-block.txt +19 -3
  33. package/dist/cli/commands/uiServe.js +13 -4
  34. package/dist/cli/commands/uiServe.js.map +1 -1
  35. package/dist/cli/registry.js +2 -0
  36. package/dist/cli/registry.js.map +1 -1
  37. package/dist/discovery/deprecation-report.md +7 -0
  38. package/dist/discovery/discovery-manifest.json +2107 -1409
  39. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  40. package/dist/discovery/discovery-manifest.summary.md +9 -9
  41. package/dist/discovery/orphan-report.md +10 -0
  42. package/dist/discovery/packs.json +1002 -0
  43. package/dist/discovery/trust-report.md +26 -0
  44. package/dist/discovery/workspaces.json +705 -0
  45. package/dist/mcp/registry-manifest.json +4 -4
  46. package/dist/router.json +1623 -0
  47. package/dist/server/app.js +11 -3
  48. package/dist/server/app.js.map +1 -1
  49. package/dist/server/io/atomicMultiWrite.js +3 -1
  50. package/dist/server/io/atomicMultiWrite.js.map +1 -1
  51. package/dist/server/io/yamlIO.js +22 -0
  52. package/dist/server/io/yamlIO.js.map +1 -1
  53. package/dist/server/routes/ping.js +8 -0
  54. package/dist/server/routes/ping.js.map +1 -1
  55. package/dist/server/routes/schema.js +2 -2
  56. package/dist/server/routes/schema.js.map +1 -1
  57. package/dist/server/routes/settings.js +104 -23
  58. package/dist/server/routes/settings.js.map +1 -1
  59. package/dist/server/routes/userMd.js +37 -27
  60. package/dist/server/routes/userMd.js.map +1 -1
  61. package/dist/server/routes/wizard.js +256 -20
  62. package/dist/server/routes/wizard.js.map +1 -1
  63. package/dist/server/schemas/settings.js +0 -1
  64. package/dist/server/schemas/settings.js.map +1 -1
  65. package/dist/server/token.js +10 -3
  66. package/dist/server/token.js.map +1 -1
  67. package/dist/server/writeRoot.js +28 -11
  68. package/dist/server/writeRoot.js.map +1 -1
  69. package/dist/server/writeRoot.test.js +22 -4
  70. package/dist/server/writeRoot.test.js.map +1 -1
  71. package/dist/shared/userMd/formAdapter.js +29 -51
  72. package/dist/shared/userMd/formAdapter.js.map +1 -1
  73. package/dist/shared/userMd/schema.js +32 -104
  74. package/dist/shared/userMd/schema.js.map +1 -1
  75. package/dist/shared/userMd/utils.js +64 -50
  76. package/dist/shared/userMd/utils.js.map +1 -1
  77. package/dist/ui/assets/index-D-DY1ywI.js +35 -0
  78. package/dist/ui/assets/index-D-DY1ywI.js.map +1 -0
  79. package/dist/ui/index.html +1 -1
  80. package/docs/adrs/router/0001-three-tier-routing.md +5 -5
  81. package/docs/adrs/smoke/0001-per-tier-smoke-scripts.md +1 -1
  82. package/docs/architecture.md +3 -3
  83. package/docs/archive/CHANGELOG-pre-3.1.0.md +167 -0
  84. package/docs/catalog.md +30 -26
  85. package/docs/contracts/CHANGELOG-conventions.md +1 -1
  86. package/docs/contracts/agent-user-schema.md +6 -9
  87. package/docs/contracts/consumer-bridge.md +79 -0
  88. package/docs/contracts/discovery-manifest.md +209 -0
  89. package/docs/contracts/discovery-manifest.schema.json +77 -4
  90. package/docs/contracts/explain-trace.schema.json +1 -1
  91. package/docs/contracts/file-ownership-matrix.json +197 -13
  92. package/docs/contracts/frontmatter-contract.md +140 -0
  93. package/docs/contracts/gui-wizard.md +223 -0
  94. package/docs/contracts/installer-agent-mode.md +137 -0
  95. package/docs/contracts/kernel-membership.md +1 -1
  96. package/docs/contracts/mcp-tool-inventory.md +9 -9
  97. package/docs/contracts/namespace.md +6 -6
  98. package/docs/contracts/provider-lifecycle.md +5 -5
  99. package/docs/contracts/rule-router.md +4 -4
  100. package/docs/contracts/settings-api.md +53 -6
  101. package/docs/contracts/smoke-contracts.md +3 -3
  102. package/docs/contracts/trust-and-safety.md +144 -0
  103. package/docs/customization.md +2 -2
  104. package/docs/decisions/ADR-007-agent-discovery-scopes.md +12 -0
  105. package/docs/decisions/ADR-013-discovery-frontmatter-contract.md +24 -0
  106. package/docs/decisions/ADR-015-discovery-manifest-contract.md +146 -0
  107. package/docs/decisions/ADR-016-installer-architecture.md +189 -0
  108. package/docs/decisions/ADR-017-monorepo-physical-layout.md +261 -0
  109. package/docs/decisions/ADR-018-trust-and-safety-layer.md +159 -0
  110. package/docs/decisions/ADR-019-router-json-dist-location.md +124 -0
  111. package/docs/decisions/ADR-020-global-only-consumer-scope.md +123 -0
  112. package/docs/decisions/ADR-021-deployment-shape.md +153 -0
  113. package/docs/decisions/INDEX.md +7 -0
  114. package/docs/deploy/connector-setup.md +129 -0
  115. package/docs/deploy/env-vars.md +70 -0
  116. package/docs/deploy/policy-cookbook.md +130 -0
  117. package/docs/deploy/quickstart.md +112 -0
  118. package/docs/distribution/public-install-smoke.md +68 -0
  119. package/docs/distribution/registries.md +55 -0
  120. package/docs/distribution/telemetry-privacy.md +128 -0
  121. package/docs/distribution/telemetry-schema.md +174 -0
  122. package/docs/featured-skills.md +95 -0
  123. package/docs/getting-started-by-role.md +19 -1
  124. package/docs/getting-started.md +2 -2
  125. package/docs/guidelines/agent-infra/installed-tools-manifest.md +11 -8
  126. package/docs/guidelines/docs/readme-size-and-splitting.md +53 -1
  127. package/docs/installation.md +27 -14
  128. package/docs/maintainers/dev-mode.md +105 -0
  129. package/docs/setup/per-ide/claude-desktop.md +3 -2
  130. package/docs/wizard.md +39 -4
  131. package/package.json +18 -1
  132. package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
  133. package/scripts/_cli/cmd_doctor.py +150 -2
  134. package/scripts/_cli/cmd_explain.py +2 -1
  135. package/scripts/_cli/cmd_migrate_to_global.py +415 -0
  136. package/scripts/_cli/cmd_settings_migrate.py +146 -0
  137. package/scripts/_cli/explain_last/route.py +2 -1
  138. package/scripts/_dispatch.bash +36 -3
  139. package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
  140. package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
  141. package/scripts/_lib/agent_settings.py +4 -1
  142. package/scripts/_lib/agent_src.py +157 -0
  143. package/scripts/agent-config +17 -6
  144. package/scripts/audit_skill_descriptions.py +18 -6
  145. package/scripts/build_discovery_manifest.py +373 -17
  146. package/scripts/check_artefact_checksums.py +104 -0
  147. package/scripts/check_cluster_patterns.py +20 -4
  148. package/scripts/check_command_count_messaging.py +33 -14
  149. package/scripts/check_council_references.py +43 -4
  150. package/scripts/check_overlay_cascade_subdirs.py +7 -3
  151. package/scripts/check_references.py +5 -2
  152. package/scripts/check_reply_consistency.py +32 -9
  153. package/scripts/check_template_pin_drift.py +24 -7
  154. package/scripts/check_token_optimizer_freshness.py +18 -3
  155. package/scripts/compile_router.py +34 -2
  156. package/scripts/compress.py +162 -44
  157. package/scripts/config/presets.py +19 -1
  158. package/scripts/config/profiles.py +16 -1
  159. package/scripts/discovery_stats.py +70 -0
  160. package/scripts/expected_perms.json +47 -0
  161. package/scripts/generate_index.py +78 -46
  162. package/scripts/generate_ownership_matrix.py +98 -43
  163. package/scripts/generate_pack_manifests.py +183 -0
  164. package/scripts/install +18 -1
  165. package/scripts/install.py +934 -59
  166. package/scripts/install.sh +27 -9
  167. package/scripts/lint_agents_layout.py +93 -13
  168. package/scripts/lint_agents_md.py +1 -1
  169. package/scripts/lint_archived_skills.py +32 -16
  170. package/scripts/lint_bench_corpus.py +14 -2
  171. package/scripts/lint_command_tiers.py +15 -2
  172. package/scripts/lint_featured_skills.py +139 -0
  173. package/scripts/lint_framework_leakage.py +33 -6
  174. package/scripts/lint_global_paths.py +147 -0
  175. package/scripts/lint_orchestration_dsl.py +6 -3
  176. package/scripts/lint_pack_boundaries.py +147 -0
  177. package/scripts/lint_pack_first_win.py +103 -0
  178. package/scripts/lint_readme_jargon.py +131 -0
  179. package/scripts/lint_readme_size.py +33 -0
  180. package/scripts/lint_rule_interactions.py +23 -5
  181. package/scripts/lint_rule_tiers.py +12 -3
  182. package/scripts/lint_trust_coherence.py +212 -0
  183. package/scripts/measure_rule_budget.py +22 -4
  184. package/scripts/move_artefact.py +143 -0
  185. package/scripts/new_skill.py +148 -0
  186. package/scripts/plan_physical_move.py +353 -0
  187. package/scripts/refine_ticket_detect.py +30 -7
  188. package/scripts/release.py +22 -2
  189. package/scripts/schemas/command.schema.json +4 -0
  190. package/scripts/skill_linter.py +248 -118
  191. package/scripts/skill_trigger_eval.py +28 -8
  192. package/scripts/smoke/kernel.sh +1 -1
  193. package/scripts/smoke/router.sh +24 -5
  194. package/scripts/smoke/skills.sh +15 -7
  195. package/scripts/smoke_quickstart.py +11 -2
  196. package/scripts/snapshot_agent_outputs.py +144 -0
  197. package/scripts/update_counts.py +45 -17
  198. package/scripts/validate_decision_engine.py +9 -1
  199. package/scripts/validate_discovery_manifest.py +94 -0
  200. package/scripts/validate_frontmatter.py +39 -20
  201. package/scripts/verify_physical_move.py +185 -0
  202. package/templates/agent-user.md +0 -1
  203. package/templates/agent-user.yml +21 -0
  204. package/templates/minimal/agents-overrides-readme.md +46 -0
  205. package/templates/minimal/overrides-gitkeep +2 -0
  206. package/dist/ui/assets/index-BTRcKDlB.js +0 -39
  207. package/dist/ui/assets/index-BTRcKDlB.js.map +0 -1
  208. package/templates/minimal/agents-gitkeep +0 -2
@@ -35,7 +35,14 @@ from typing import Iterable
35
35
  import yaml
36
36
 
37
37
  ROOT = Path(__file__).resolve().parent.parent
38
- SRC_ROOT = ROOT / ".agent-src.uncompressed"
38
+ sys.path.insert(0, str(ROOT / "scripts"))
39
+ from _lib.agent_src import artefact_roots, resolve_logical, strip_source_prefix # noqa: E402
40
+
41
+ # Canonical anchor used in the committed matrix. Paths are always
42
+ # emitted as ".agent-src.uncompressed/<sub>/<...>" regardless of which
43
+ # physical root (legacy or packages/*) contains the file, so the matrix
44
+ # stays stable across the monorepo migration.
45
+ CANONICAL_SRC_PREFIX = ".agent-src.uncompressed"
39
46
 
40
47
  SCAN_DIRS = ("rules", "skills", "commands", "contexts", "personas")
41
48
 
@@ -88,38 +95,48 @@ def _parse_frontmatter(p: Path) -> dict:
88
95
  return data if isinstance(data, dict) else {}
89
96
 
90
97
 
91
- def _collect_files(src_root: Path) -> list[Path]:
92
- out: list[Path] = []
93
- for sub in SCAN_DIRS:
94
- d = src_root / sub
95
- if d.exists():
96
- out.extend(sorted(d.rglob("*.md")))
97
- return out
98
+ def _collect_files(root: Path | None = None) -> list[tuple[Path, str]]:
99
+ """Walk every artefact root and yield ``(physical_path, canonical_rel)``.
98
100
 
101
+ ``canonical_rel`` is always anchored at ``.agent-src.uncompressed/`` so
102
+ the matrix is byte-identical pre- and post-monorepo-move. Duplicates
103
+ across roots resolve to the first hit (legacy first, then packages
104
+ alphabetically) — matches the priority in ``artefact_roots()``.
99
105
 
100
- def _resolve(target: str, src_root: Path) -> Path | None:
101
- """Resolve a path string (repo-relative or short) into an absolute Path
102
- under src_root or the repo root. Return None if not under a scanned root."""
103
- cand = src_root.parent / target if "/" in target else src_root / target
104
- try:
105
- rel = cand.resolve().relative_to(src_root.parent)
106
- except ValueError:
107
- return None
108
- parts = rel.parts
109
- if len(parts) >= 3 and parts[0] == ".agent-src.uncompressed" and parts[1] in SCAN_DIRS:
110
- return cand if cand.exists() else None
111
- return None
106
+ When ``root`` is given, only that single directory is scanned — used by
107
+ tests against a ``tmp_path`` fixture so they stay isolated from the
108
+ real package layout.
109
+ """
110
+ roots = [root] if root is not None else list(artefact_roots())
111
+ out: list[tuple[Path, str]] = []
112
+ seen: set[str] = set()
113
+ for r in roots:
114
+ for sub in SCAN_DIRS:
115
+ d = r / sub
116
+ if not d.exists():
117
+ continue
118
+ for f in sorted(d.rglob("*.md")):
119
+ logical = f.relative_to(r).as_posix()
120
+ canonical = f"{CANONICAL_SRC_PREFIX}/{logical}"
121
+ if canonical in seen:
122
+ continue
123
+ seen.add(canonical)
124
+ out.append((f, canonical))
125
+ out.sort(key=lambda pair: pair[1])
126
+ return out
112
127
 
113
128
 
114
- def build_matrix(src_root: Path) -> tuple[dict[str, FileEntry], list[Edge], list[str]]:
129
+ def build_matrix(root: Path | None = None) -> tuple[dict[str, FileEntry], list[Edge], list[str]]:
115
130
  """Build the file map + edge list. Returns (files, edges, depth3_chains).
116
131
 
117
132
  depth3_chains is non-empty iff the depth invariant is violated; the
118
- caller must abort with exit code 2.
133
+ caller must abort with exit code 2. When ``root`` is given, only that
134
+ single directory is scanned (test isolation).
119
135
  """
120
136
  files: dict[str, FileEntry] = {}
121
- for f in _collect_files(src_root):
122
- rel = f.relative_to(src_root.parent).as_posix()
137
+ physical_by_canonical: dict[str, Path] = {}
138
+ for f, rel in _collect_files(root):
139
+ physical_by_canonical[rel] = f
123
140
  fm = _parse_frontmatter(f)
124
141
  rtype = fm.get("type")
125
142
  if isinstance(rtype, str):
@@ -149,14 +166,15 @@ def build_matrix(src_root: Path) -> tuple[dict[str, FileEntry], list[Edge], list
149
166
 
150
167
  # Body markdown links — only count edges to files we know about
151
168
  for rel, entry in files.items():
152
- body = (src_root.parent / rel).read_text(encoding="utf-8")
169
+ phys = physical_by_canonical[rel]
170
+ body = phys.read_text(encoding="utf-8")
153
171
  body = body.split("\n---\n", 1)[-1] if body.startswith("---\n") else body
154
172
  seen_targets: set[str] = set()
155
173
  for m in LINK_RE.finditer(body):
156
174
  href = m.group(1).strip()
157
175
  if href.startswith("http"):
158
176
  continue
159
- resolved = _resolve_link(rel, href, src_root)
177
+ resolved = _resolve_link(rel, phys, href)
160
178
  if resolved is None or resolved == rel or resolved in seen_targets:
161
179
  continue
162
180
  if resolved in files:
@@ -191,21 +209,57 @@ def build_matrix(src_root: Path) -> tuple[dict[str, FileEntry], list[Edge], list
191
209
  return files, edges, depth3
192
210
 
193
211
 
194
- def _resolve_link(source_rel: str, href: str, src_root: Path) -> str | None:
195
- """Resolve a markdown link href (relative to source file) to a repo-relative
196
- path inside a scanned root, or None."""
197
- if href.startswith(".agent-src.uncompressed/") or href.startswith("agents/"):
198
- cand = (src_root.parent / href).resolve()
212
+ def _resolve_link(source_rel: str, source_phys: Path, href: str) -> str | None:
213
+ """Resolve a markdown link href to a canonical scanned-root path, or None.
214
+
215
+ ``source_rel`` is the canonical (``.agent-src.uncompressed/...``)
216
+ identity of the source file. Relative hrefs are resolved against
217
+ the source's *logical* directory, then looked up across every
218
+ artefact root via :func:`resolve_logical`. This keeps the matrix
219
+ stable when source and target live in different physical packages.
220
+
221
+ Repo-rooted hrefs (``agents/...``, ``packages/...``, or those
222
+ starting with ``.agent-src.uncompressed/``) are resolved against
223
+ the repo root and normalised through :func:`strip_source_prefix`.
224
+ """
225
+ if href.startswith(".agent-src.uncompressed/") or href.startswith("agents/") \
226
+ or href.startswith("packages/"):
227
+ cand = (ROOT / href).resolve()
228
+ if not cand.exists():
229
+ return None
230
+ try:
231
+ rel = cand.relative_to(ROOT).as_posix()
232
+ except ValueError:
233
+ return None
234
+ logical = strip_source_prefix(rel)
235
+ if logical is None:
236
+ return None
199
237
  else:
200
- base = (src_root.parent / source_rel).parent
201
- cand = (base / href).resolve()
202
- try:
203
- rel = cand.relative_to(src_root.parent).as_posix()
204
- except ValueError:
205
- return None
206
- parts = rel.split("/")
207
- if len(parts) >= 3 and parts[0] == ".agent-src.uncompressed" and parts[1] in SCAN_DIRS:
208
- return rel if cand.exists() else None
238
+ # Logical resolution: walk relative hops on the canonical path
239
+ # so a `../skills/laravel/SKILL.md` link from
240
+ # `rules/architecture.md` resolves to `skills/laravel/SKILL.md`
241
+ # regardless of which package physically hosts either file.
242
+ source_logical = strip_source_prefix(source_rel)
243
+ if source_logical is None:
244
+ return None
245
+ base_parts = source_logical.split("/")[:-1] # drop file name
246
+ href_parts = href.split("/")
247
+ for part in href_parts:
248
+ if part == "" or part == ".":
249
+ continue
250
+ if part == "..":
251
+ if not base_parts:
252
+ return None
253
+ base_parts.pop()
254
+ else:
255
+ base_parts.append(part)
256
+ logical = "/".join(base_parts)
257
+ # Existence is validated downstream by the caller against the scanned
258
+ # ``files`` dict — that handles both real ``artefact_roots()`` scans
259
+ # and ``tmp_path`` test fixtures uniformly.
260
+ parts = logical.split("/")
261
+ if len(parts) >= 2 and parts[0] in SCAN_DIRS:
262
+ return f"{CANONICAL_SRC_PREFIX}/{logical}"
209
263
  return None
210
264
 
211
265
 
@@ -287,11 +341,12 @@ def main(argv: Iterable[str] | None = None) -> int:
287
341
  help="Regenerate to memory and diff against committed JSON.")
288
342
  args = ap.parse_args(list(argv) if argv is not None else None)
289
343
 
290
- if not SRC_ROOT.is_dir():
291
- print(f"❌ source dir missing: {SRC_ROOT}", file=sys.stderr)
344
+ if not artefact_roots():
345
+ print("❌ no artefact roots found (legacy or packages/*/.agent-src.uncompressed/)",
346
+ file=sys.stderr)
292
347
  return 3
293
348
 
294
- files, edges, depth3 = build_matrix(SRC_ROOT)
349
+ files, edges, depth3 = build_matrix()
295
350
  if depth3:
296
351
  print("❌ load_context depth-3 chain detected (limit is 2):", file=sys.stderr)
297
352
  for chain in depth3:
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env python3
2
+ """Generate ``pack.yaml`` + ``README.md`` for every ``packages/*/`` tree.
3
+
4
+ Phase 4.4 of the monorepo migration (ADR-017). The manifests are
5
+ *derived* from frontmatter + ``config/discovery/packs.yml``; they are
6
+ regenerated on every ``task sync`` and must never be hand-edited.
7
+
8
+ For each package directory under ``packages/`` we emit:
9
+ - ``packages/<pkg>/pack.yaml`` — id, label, owner (workspaces),
10
+ requires, version, artefact_count
11
+ - ``packages/<pkg>/README.md`` — pack description + per-artefact
12
+ description table
13
+
14
+ CLI:
15
+ --check exit non-zero if any output would change (CI gate)
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import json
21
+ import sys
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ import yaml
26
+
27
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
28
+ from _lib.agent_src import iter_all_sources # noqa: E402
29
+
30
+ ROOT = Path(__file__).resolve().parents[1]
31
+ PACKAGES = ROOT / "packages"
32
+ PACKS_VOCAB = ROOT / "config" / "discovery" / "packs.yml"
33
+ PACKAGE_JSON = ROOT / "package.json"
34
+ GENERATED_HEADER = "# Generated by scripts/generate_pack_manifests.py — DO NOT EDIT BY HAND\n"
35
+
36
+
37
+ def _load_yaml(path: Path) -> Any:
38
+ if not path.exists():
39
+ return None
40
+ return yaml.safe_load(path.read_text(encoding="utf-8"))
41
+
42
+
43
+ def _read_frontmatter(path: Path) -> dict[str, Any]:
44
+ text = path.read_text(encoding="utf-8")
45
+ if not text.startswith("---"):
46
+ return {}
47
+ end = text.find("\n---", 4)
48
+ if end == -1:
49
+ return {}
50
+ try:
51
+ data = yaml.safe_load(text[4:end])
52
+ except yaml.YAMLError:
53
+ return {}
54
+ return data if isinstance(data, dict) else {}
55
+
56
+
57
+ def _pack_id_from_dir(pkg_dir: Path) -> str:
58
+ name = pkg_dir.name
59
+ return "core" if name == "core" else name.removeprefix("pack-")
60
+
61
+
62
+ def _vocab_lookup(packs_vocab: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
63
+ return {p["id"]: p for p in (packs_vocab or [])}
64
+
65
+
66
+ def _package_version() -> str:
67
+ if not PACKAGE_JSON.exists():
68
+ return "0.0.0"
69
+ return json.loads(PACKAGE_JSON.read_text(encoding="utf-8")).get("version", "0.0.0")
70
+
71
+
72
+ def _collect_artefacts(pkg_dir: Path) -> list[dict[str, Any]]:
73
+ src_root = pkg_dir / ".agent-src.uncompressed"
74
+ if not src_root.is_dir():
75
+ return []
76
+ items: list[dict[str, Any]] = []
77
+ for p in sorted(src_root.rglob("*.md")):
78
+ if not p.is_file():
79
+ continue
80
+ fm = _read_frontmatter(p)
81
+ if not fm:
82
+ continue
83
+ items.append({
84
+ "path": p.relative_to(src_root).as_posix(),
85
+ "name": fm.get("name") or (p.parent.name if p.name == "SKILL.md" else p.stem),
86
+ "description": (fm.get("description") or "").strip(),
87
+ "category": _category_for(p, src_root),
88
+ })
89
+ return items
90
+
91
+
92
+ def _category_for(path: Path, src_root: Path) -> str:
93
+ rel = path.relative_to(src_root).as_posix()
94
+ top = rel.split("/", 1)[0]
95
+ mapping = {"skills": "skill", "rules": "rule", "commands": "command",
96
+ "personas": "persona", "guidelines": "guideline",
97
+ "contexts": "context", "presets": "preset", "profiles": "profile"}
98
+ return mapping.get(top, top)
99
+
100
+
101
+ def _build_pack_yaml(pkg_dir: Path, vocab: dict[str, dict[str, Any]],
102
+ artefacts: list[dict[str, Any]], version: str) -> dict[str, Any]:
103
+ pid = _pack_id_from_dir(pkg_dir)
104
+ meta = vocab.get(pid) or {}
105
+ label = meta.get("label") or (pid.title() if pid == "core" else pid)
106
+ out: dict[str, Any] = {
107
+ "id": pid,
108
+ "label": label,
109
+ "description": meta.get("description") or ("Core framework-neutral artefacts." if pid == "core" else ""),
110
+ "owner": meta.get("workspaces") or (["agent-config-maintainer"] if pid == "core" else []),
111
+ "requires": meta.get("requires_hint") or [],
112
+ "trust_level_default": meta.get("trust_level_default") or "core",
113
+ "version": version,
114
+ "artefact_count": len(artefacts),
115
+ }
116
+ if isinstance(meta.get("onboarding"), dict):
117
+ out["onboarding"] = meta["onboarding"]
118
+ return out
119
+
120
+
121
+ def _render_readme(pack_meta: dict[str, Any], artefacts: list[dict[str, Any]]) -> str:
122
+ lines = [GENERATED_HEADER, f"# {pack_meta['label']}", ""]
123
+ if pack_meta.get("description"):
124
+ lines += [pack_meta["description"], ""]
125
+ lines += [f"- **id**: `{pack_meta['id']}`",
126
+ f"- **version**: `{pack_meta['version']}`",
127
+ f"- **owner**: {', '.join(pack_meta['owner']) or '—'}",
128
+ f"- **requires**: {', '.join(pack_meta['requires']) or '—'}",
129
+ f"- **artefacts**: {pack_meta['artefact_count']}", ""]
130
+ by_cat: dict[str, list[dict[str, Any]]] = {}
131
+ for a in artefacts:
132
+ by_cat.setdefault(a["category"], []).append(a)
133
+ for cat in sorted(by_cat):
134
+ lines += [f"## {cat.title()}s ({len(by_cat[cat])})", ""]
135
+ for a in sorted(by_cat[cat], key=lambda x: x["name"]):
136
+ desc = a["description"] or "_(no description)_"
137
+ lines.append(f"- **`{a['name']}`** — {desc}")
138
+ lines.append("")
139
+ return "\n".join(lines).rstrip() + "\n"
140
+
141
+
142
+ def _write_if_changed(path: Path, content: str, check: bool) -> bool:
143
+ existing = path.read_text(encoding="utf-8") if path.exists() else None
144
+ if existing == content:
145
+ return False
146
+ if check:
147
+ return True
148
+ path.parent.mkdir(parents=True, exist_ok=True)
149
+ path.write_text(content, encoding="utf-8")
150
+ return True
151
+
152
+
153
+ def main() -> int:
154
+ ap = argparse.ArgumentParser(description=__doc__)
155
+ ap.add_argument("--check", action="store_true", help="exit non-zero on drift")
156
+ args = ap.parse_args()
157
+ vocab = _vocab_lookup(_load_yaml(PACKS_VOCAB) or [])
158
+ version = _package_version()
159
+ if not PACKAGES.exists():
160
+ print("packages/ does not exist — nothing to generate", file=sys.stderr)
161
+ return 0
162
+ drift = 0
163
+ for pkg in sorted(PACKAGES.iterdir()):
164
+ if not pkg.is_dir():
165
+ continue
166
+ artefacts = _collect_artefacts(pkg)
167
+ meta = _build_pack_yaml(pkg, vocab, artefacts, version)
168
+ yaml_body = GENERATED_HEADER + yaml.safe_dump(meta, sort_keys=True, allow_unicode=True)
169
+ readme_body = _render_readme(meta, artefacts)
170
+ if _write_if_changed(pkg / "pack.yaml", yaml_body, args.check):
171
+ drift += 1
172
+ print(f"{'drift' if args.check else 'wrote'}: {pkg.name}/pack.yaml")
173
+ if _write_if_changed(pkg / "README.md", readme_body, args.check):
174
+ drift += 1
175
+ print(f"{'drift' if args.check else 'wrote'}: {pkg.name}/README.md")
176
+ if args.check and drift:
177
+ print(f"pack manifests out of date ({drift} file(s)) — run `task generate-pack-manifests`", file=sys.stderr)
178
+ return 1
179
+ return 0
180
+
181
+
182
+ if __name__ == "__main__":
183
+ sys.exit(main())
package/scripts/install CHANGED
@@ -183,6 +183,20 @@ if $LIST_TOOLS; then
183
183
  exit 0
184
184
  fi
185
185
 
186
+ # road-to-global-only-install § Phase 3.3 — consumer-floor gate.
187
+ #
188
+ # Consumer installs are global-only (ADR-020). Explicit --scope=project
189
+ # (and its --scope project / --scope=project / SCOPE shorthand) is the
190
+ # maintainer-only escape hatch behind AGENT_CONFIG_DEV_MODE=1. Without
191
+ # the env flag we refuse here so the bash orchestrator fails fast with
192
+ # a directive error pointing at the maintainer doc — install.py's
193
+ # Python-side _enforce_consumer_global_only still backstops the same
194
+ # check for direct python3 scripts/install.py invocations.
195
+ if [[ "${SCOPE:-}" == "project" && "${AGENT_CONFIG_DEV_MODE:-}" != "1" ]]; then
196
+ err "--scope=project is reserved for maintainers (ADR-020 — consumer installs are global-only). Set AGENT_CONFIG_DEV_MODE=1 to opt in. See docs/maintainers/dev-mode.md."
197
+ exit 1
198
+ fi
199
+
186
200
  # Interactive --tools picker (S9). Fires only when:
187
201
  # - --tools was not explicitly passed
188
202
  # - --yes / -y was not passed (CI / non-interactive opt-out)
@@ -298,7 +312,10 @@ run_sync() {
298
312
  $QUIET && args+=(--quiet)
299
313
  $MINIMAL && args+=(--minimal)
300
314
  args+=(--tools="$TOOLS")
301
- bash "$INSTALL_SH" "${args[@]}"
315
+ # Suppress the install.sh deprecation banner when called through the
316
+ # orchestrator (this script). Direct `bash install.sh` invocations
317
+ # still see it. See ADR-016 § Distribution / Phase 6.
318
+ AGENT_CONFIG_FROM_ORCHESTRATOR=1 bash "$INSTALL_SH" "${args[@]}"
302
319
  }
303
320
 
304
321
  run_bridges() {