@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
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env python3
2
+ """Lint trust/safety coherence across the discovery manifest.
3
+
4
+ Phase 5.4 of the monorepo trust-and-safety layer. Walks the freshly
5
+ built `dist/discovery/discovery-manifest.json` and asserts three
6
+ invariants:
7
+
8
+ 1. Every pack whose ``trust_summary`` declares ``advisory`` or
9
+ ``restricted`` artefacts ships at least one ``*safety-floor*``
10
+ rule in the same pack.
11
+ 2. Every artefact with ``trust.human_review_required: true`` carries
12
+ the ``_HRR_BANNER_MARKER`` in its compiled output under
13
+ ``.agent-src/<logical>``.
14
+ 3. Every rule listed in ``router.json`` ``kernel[]`` declares
15
+ ``trust.level: core`` (no escalation to advisory/restricted,
16
+ no demotion to experimental).
17
+
18
+ Exits 0 clean, 1 on any violation. Stdlib + pyyaml. Cap: ≤ 200 LOC.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import json
24
+ import sys
25
+ from pathlib import Path
26
+ from typing import Any
27
+
28
+ ROOT = Path(__file__).resolve().parents[1]
29
+ MANIFEST = ROOT / "dist" / "discovery" / "discovery-manifest.json"
30
+ ROUTER = ROOT / "dist" / "router.json"
31
+ COMPILED_SRC = ROOT / ".agent-src"
32
+
33
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
34
+ from _lib.agent_src import strip_source_prefix # noqa: E402
35
+
36
+ # Imported lazily inside _banner_marker() to keep the cap loose if compress.py
37
+ # grows additional top-level side effects.
38
+ _BANNER_MARKER = "<!-- agent-config:human-review-banner -->"
39
+
40
+ # Trust levels that demand a domain-safety floor in the same pack.
41
+ _FLAGGED_LEVELS = ("advisory", "restricted")
42
+ _SAFETY_FLOOR_FRAGMENT = "safety-floor"
43
+
44
+
45
+ def _load_manifest(path: Path) -> dict[str, Any]:
46
+ if not path.exists():
47
+ raise SystemExit(
48
+ f"ERROR: manifest not found: {path}\n"
49
+ " Run `task build-discovery` first."
50
+ )
51
+ return json.loads(path.read_text("utf-8"))
52
+
53
+
54
+ def _load_kernel(path: Path) -> set[str]:
55
+ if not path.exists():
56
+ raise SystemExit(f"ERROR: router.json not found: {path}")
57
+ data = json.loads(path.read_text("utf-8"))
58
+ kernel = data.get("kernel") or []
59
+ if not isinstance(kernel, list):
60
+ raise SystemExit("ERROR: router.json `kernel` must be a list")
61
+ return {str(name) for name in kernel}
62
+
63
+
64
+ def _check_pack_safety_floors(manifest: dict[str, Any]) -> list[str]:
65
+ """Check 1: advisory/restricted packs ship a *safety-floor* rule."""
66
+ errs: list[str] = []
67
+ # Build pack -> [artefact path] index from the artefact list so we can
68
+ # spot the safety-floor regardless of how trust_summary was computed.
69
+ pack_paths: dict[str, list[str]] = {}
70
+ for art in manifest.get("artefacts", []):
71
+ for pack in art.get("packs", []) or []:
72
+ pack_paths.setdefault(pack, []).append(art["path"])
73
+
74
+ for pack in manifest.get("packs", []):
75
+ summary = pack.get("trust_summary", {}) or {}
76
+ flagged_total = sum(int(summary.get(lvl, 0)) for lvl in _FLAGGED_LEVELS)
77
+ if flagged_total == 0:
78
+ continue
79
+ paths = pack_paths.get(pack["id"], [])
80
+ has_floor = any(_SAFETY_FLOOR_FRAGMENT in p for p in paths)
81
+ if not has_floor:
82
+ counts = ", ".join(
83
+ f"{lvl}={int(summary.get(lvl, 0))}" for lvl in _FLAGGED_LEVELS
84
+ )
85
+ errs.append(
86
+ f"pack `{pack['id']}` declares flagged artefacts ({counts})"
87
+ f" but ships no `*{_SAFETY_FLOOR_FRAGMENT}*` rule"
88
+ )
89
+ return errs
90
+
91
+
92
+ def _check_human_review_banners(
93
+ manifest: dict[str, Any], compiled_src: Path
94
+ ) -> list[str]:
95
+ """Check 2: every human_review_required artefact has the banner."""
96
+ errs: list[str] = []
97
+ for art in manifest.get("artefacts", []):
98
+ trust = art.get("trust", {}) or {}
99
+ if not trust.get("human_review_required"):
100
+ continue
101
+ rel = art["path"]
102
+ logical = strip_source_prefix(rel)
103
+ if logical is None:
104
+ errs.append(
105
+ f"{rel}: human_review_required=true but path is not under"
106
+ " any known source root"
107
+ )
108
+ continue
109
+ compiled = compiled_src / logical
110
+ if not compiled.exists():
111
+ errs.append(
112
+ f"{rel}: human_review_required=true but compiled output"
113
+ f" missing at `{compiled.relative_to(ROOT)}`"
114
+ )
115
+ continue
116
+ body = compiled.read_text("utf-8", errors="replace")
117
+ if _BANNER_MARKER not in body:
118
+ errs.append(
119
+ f"{rel}: human_review_required=true but compiled output"
120
+ f" `{compiled.relative_to(ROOT)}` is missing the HRR banner"
121
+ f" (`{_BANNER_MARKER}`) — re-run `task compress`."
122
+ )
123
+ return errs
124
+
125
+
126
+ def _check_kernel_trust(
127
+ manifest: dict[str, Any], kernel: set[str]
128
+ ) -> list[str]:
129
+ """Check 3: every kernel rule declares trust.level=core."""
130
+ errs: list[str] = []
131
+ # name -> artefact for category=rule entries. Manifest does not always
132
+ # populate `name` for rules, so fall back to the logical filename stem.
133
+ rule_by_name: dict[str, dict[str, Any]] = {}
134
+ for art in manifest.get("artefacts", []):
135
+ if art.get("category") != "rule":
136
+ continue
137
+ name = art.get("name")
138
+ if not name:
139
+ logical = strip_source_prefix(art.get("path", ""))
140
+ if logical is None:
141
+ continue
142
+ stem = Path(logical).stem
143
+ name = stem
144
+ rule_by_name[name] = art
145
+
146
+ for kname in sorted(kernel):
147
+ art = rule_by_name.get(kname)
148
+ if art is None:
149
+ errs.append(
150
+ f"kernel rule `{kname}` listed in router.json but no"
151
+ " matching artefact in manifest"
152
+ )
153
+ continue
154
+ level = (art.get("trust", {}) or {}).get("level")
155
+ if level != "core":
156
+ errs.append(
157
+ f"kernel rule `{kname}` has trust.level=`{level}`"
158
+ " — must be `core` (router.json kernel guarantees Iron-Law"
159
+ " floor)"
160
+ )
161
+ return errs
162
+
163
+
164
+ def main(argv: list[str] | None = None) -> int:
165
+ parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
166
+ parser.add_argument("--quiet", action="store_true")
167
+ parser.add_argument(
168
+ "--manifest", type=Path, default=MANIFEST, help="discovery manifest"
169
+ )
170
+ parser.add_argument(
171
+ "--router", type=Path, default=ROUTER, help="router.json with kernel[]"
172
+ )
173
+ parser.add_argument(
174
+ "--compiled-src",
175
+ type=Path,
176
+ default=COMPILED_SRC,
177
+ help="compiled output root (.agent-src/)",
178
+ )
179
+ args = parser.parse_args(argv)
180
+
181
+ manifest = _load_manifest(args.manifest)
182
+ kernel = _load_kernel(args.router)
183
+
184
+ errs: list[str] = []
185
+ errs.extend(_check_pack_safety_floors(manifest))
186
+ errs.extend(_check_human_review_banners(manifest, args.compiled_src))
187
+ errs.extend(_check_kernel_trust(manifest, kernel))
188
+
189
+ if errs:
190
+ for e in errs:
191
+ print(f"ERROR: {e}", file=sys.stderr)
192
+ print(
193
+ f"\n{len(errs)} trust-coherence violation(s) across"
194
+ f" {len(manifest.get('packs', []))} pack(s) and"
195
+ f" {len(manifest.get('artefacts', []))} artefact(s).",
196
+ file=sys.stderr,
197
+ )
198
+ return 1
199
+
200
+ if not args.quiet:
201
+ print(
202
+ "✅ lint-trust-coherence:"
203
+ f" {len(manifest.get('packs', []))} pack(s),"
204
+ f" {len(kernel)} kernel rule(s),"
205
+ f" {sum(1 for a in manifest.get('artefacts', []) if (a.get('trust') or {}).get('human_review_required'))}"
206
+ " HRR artefact(s) clean."
207
+ )
208
+ return 0
209
+
210
+
211
+ if __name__ == "__main__":
212
+ raise SystemExit(main())
@@ -26,7 +26,9 @@ import sys
26
26
  from pathlib import Path
27
27
 
28
28
  REPO_ROOT = Path(__file__).resolve().parent.parent
29
- RULES_DIR = REPO_ROOT / ".agent-src.uncompressed" / "rules"
29
+ sys.path.insert(0, str(REPO_ROOT / "scripts"))
30
+ from _lib.agent_src import artefact_roots # noqa: E402
31
+
30
32
  OVERRIDES_FILE = REPO_ROOT / "docs" / "contracts" / "iron-law-overrides.txt"
31
33
  TREND_FILE = REPO_ROOT / "agents" / "runtime" / ".rule-budget-history.jsonl"
32
34
 
@@ -93,8 +95,24 @@ def measure_rule(path: Path) -> dict[str, object]:
93
95
 
94
96
 
95
97
  def collect() -> list[dict[str, object]]:
96
- rules = [measure_rule(p) for p in sorted(RULES_DIR.glob("*.md"))]
97
- return rules
98
+ """Collect rule measurements from every source root (multi-root aware).
99
+
100
+ Pre-move: reads .agent-src.uncompressed/rules/*.md.
101
+ Post-move (ADR-017): reads packages/*/.agent-src.uncompressed/rules/*.md.
102
+ Deduplicates on logical rule id (stem) — first root wins.
103
+ """
104
+ seen: set[str] = set()
105
+ rules: list[dict[str, object]] = []
106
+ for root in artefact_roots():
107
+ rules_dir = root / "rules"
108
+ if not rules_dir.is_dir():
109
+ continue
110
+ for p in sorted(rules_dir.glob("*.md")):
111
+ if p.stem in seen:
112
+ continue
113
+ seen.add(p.stem)
114
+ rules.append(measure_rule(p))
115
+ return sorted(rules, key=lambda r: r["id"])
98
116
 
99
117
 
100
118
  def load_overrides() -> set[str]:
@@ -138,7 +156,7 @@ def aggregate(rules: list[dict[str, object]]) -> dict[str, object]:
138
156
 
139
157
  def render_table(rules: list[dict[str, object]], agg: dict[str, object]) -> str:
140
158
  lines: list[str] = []
141
- lines.append("Rule budget — source: .agent-src.uncompressed/rules/")
159
+ lines.append("Rule budget — source: rules/ under every artefact root (multi-root aware, ADR-017)")
142
160
  lines.append("")
143
161
  lines.append(f"{'id':<40} {'type':<7} {'tier':<5} {'chars':>7}")
144
162
  lines.append("-" * 62)
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env python3
2
+ """Move a single artefact between packs via ``git mv`` (history-preserving).
3
+
4
+ Phase 4.5 of the monorepo migration (ADR-017). Locates the artefact by
5
+ slug or logical path, computes its destination under the requested
6
+ pack, runs ``git mv`` for the artefact directory (skills/commands) or
7
+ the single file (rules), and rewrites the ``packs:`` frontmatter so the
8
+ discovery manifest stays in sync.
9
+
10
+ CLI:
11
+ --id ID artefact slug (skill/command name or rule stem)
12
+ --type TYPE skill | rule | command (required when --id ambiguous)
13
+ --to PACK target pack id (e.g. ``laravel``, ``core``)
14
+ --dry-run print the planned move and frontmatter edit, no FS changes
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import subprocess
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ import yaml
24
+
25
+ ROOT = Path(__file__).resolve().parents[1]
26
+ PACKAGES = ROOT / "packages"
27
+ PACKS_VOCAB = ROOT / "config" / "discovery" / "packs.yml"
28
+
29
+
30
+ def _list_pack_ids() -> set[str]:
31
+ data = yaml.safe_load(PACKS_VOCAB.read_text(encoding="utf-8")) or []
32
+ return {p["id"] for p in data} | {"core"}
33
+
34
+
35
+ def _pack_dir(pack_id: str) -> Path:
36
+ return PACKAGES / ("core" if pack_id == "core" else f"pack-{pack_id}")
37
+
38
+
39
+ def _find_artefact(slug: str, kind: str | None) -> tuple[Path, str, str]:
40
+ """Return (physical_path, detected_kind, current_pack_id)."""
41
+ hits: list[tuple[Path, str, str]] = []
42
+ for pkg in sorted(PACKAGES.iterdir()):
43
+ src = pkg / ".agent-src.uncompressed"
44
+ if not src.is_dir():
45
+ continue
46
+ pid = "core" if pkg.name == "core" else pkg.name.removeprefix("pack-")
47
+ for k, rel in (("skill", f"skills/{slug}/SKILL.md"),
48
+ ("rule", f"rules/{slug}.md"),
49
+ ("command", f"commands/{slug}.md")):
50
+ p = src / rel
51
+ if p.exists() and (kind is None or kind == k):
52
+ hits.append((p, k, pid))
53
+ if not hits:
54
+ raise SystemExit(f"error: artefact '{slug}' not found under any pack")
55
+ if len(hits) > 1 and kind is None:
56
+ kinds = ", ".join(sorted({h[1] for h in hits}))
57
+ raise SystemExit(f"error: '{slug}' ambiguous (found as: {kinds}); pass --type")
58
+ return hits[0]
59
+
60
+
61
+ def _move_root(path: Path, kind: str) -> Path:
62
+ """Return the path to git-mv (directory for skills, file for rule/command)."""
63
+ return path.parent if kind == "skill" else path
64
+
65
+
66
+ def _rewrite_packs(md_path: Path, new_pack: str, dry_run: bool) -> bool:
67
+ text = md_path.read_text(encoding="utf-8")
68
+ if not text.startswith("---"):
69
+ return False
70
+ end = text.find("\n---", 4)
71
+ if end == -1:
72
+ return False
73
+ head = text[4:end]
74
+ body = text[end:]
75
+ fm = yaml.safe_load(head) or {}
76
+ if not isinstance(fm, dict):
77
+ return False
78
+ current = fm.get("packs") or []
79
+ desired = [] if new_pack == "core" else [new_pack]
80
+ if current == desired:
81
+ return False
82
+ if desired:
83
+ fm["packs"] = desired
84
+ else:
85
+ fm.pop("packs", None)
86
+ new_text = "---\n" + yaml.safe_dump(fm, sort_keys=False, allow_unicode=True) + body[1:]
87
+ if dry_run:
88
+ print(f" would rewrite frontmatter packs: {current} -> {desired}")
89
+ else:
90
+ md_path.write_text(new_text, encoding="utf-8")
91
+ print(f" rewrote frontmatter packs: {current} -> {desired}")
92
+ return True
93
+
94
+
95
+ def main() -> int:
96
+ ap = argparse.ArgumentParser(description=__doc__)
97
+ ap.add_argument("--id", required=True, help="artefact slug")
98
+ ap.add_argument("--to", required=True, help="target pack id")
99
+ ap.add_argument("--type", choices=["skill", "rule", "command"])
100
+ ap.add_argument("--dry-run", action="store_true")
101
+ args = ap.parse_args()
102
+
103
+ vocab = _list_pack_ids()
104
+ if args.to not in vocab:
105
+ print(f"error: target pack '{args.to}' not in {sorted(vocab)}", file=sys.stderr)
106
+ return 2
107
+
108
+ src_md, kind, current_pack = _find_artefact(args.id, args.type)
109
+ if current_pack == args.to:
110
+ print(f"no-op: '{args.id}' already lives in pack '{args.to}'")
111
+ return 0
112
+
113
+ src_root = _move_root(src_md, kind)
114
+ dest_pkg_src = _pack_dir(args.to) / ".agent-src.uncompressed"
115
+ rel_under_pack = src_root.relative_to(_pack_dir(current_pack) / ".agent-src.uncompressed")
116
+ dest_root = dest_pkg_src / rel_under_pack
117
+
118
+ print(f"plan: {kind} '{args.id}' : {current_pack} -> {args.to}")
119
+ print(f" git mv {src_root.relative_to(ROOT)} {dest_root.relative_to(ROOT)}")
120
+
121
+ # Frontmatter must be rewritten BEFORE the move so the new physical
122
+ # location matches the declared pack. Discovery scanner cross-checks.
123
+ _rewrite_packs(src_md, args.to, args.dry_run)
124
+
125
+ if args.dry_run:
126
+ print("dry-run: no FS changes")
127
+ return 0
128
+
129
+ dest_root.parent.mkdir(parents=True, exist_ok=True)
130
+ result = subprocess.run(
131
+ ["git", "mv", str(src_root.relative_to(ROOT)), str(dest_root.relative_to(ROOT))],
132
+ cwd=ROOT, capture_output=True, text=True,
133
+ )
134
+ if result.returncode != 0:
135
+ print(f"git mv failed: {result.stderr}", file=sys.stderr)
136
+ return result.returncode
137
+ print(f"moved: {src_root.relative_to(ROOT)} -> {dest_root.relative_to(ROOT)}")
138
+ print("next: run `task sync` and `task lint-pack-boundaries`")
139
+ return 0
140
+
141
+
142
+ if __name__ == "__main__":
143
+ sys.exit(main())
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env python3
2
+ """Interactive scaffolder for new skills under the packages/ layout.
3
+
4
+ Phase 4.5 of the monorepo migration (ADR-017). Asks for pack, type,
5
+ name, workspaces, and description, then drops a templated artefact
6
+ into ``packages/<pack-dir>/.agent-src.uncompressed/<type>s/<name>/SKILL.md``.
7
+
8
+ Type → directory mapping:
9
+ - skill → skills/<name>/SKILL.md
10
+ - rule → rules/<name>.md
11
+ - command → commands/<name>.md
12
+
13
+ CLI (non-interactive overrides):
14
+ --pack PACK pack id (e.g. ``laravel`` or ``core``)
15
+ --type TYPE skill | rule | command (default: skill)
16
+ --name NAME artefact slug (kebab-case)
17
+ --description TEXT one-line description (trigger phrasing)
18
+ --workspace WS repeatable; defaults to pack's owner list
19
+ --force overwrite if file exists
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import sys
25
+ from pathlib import Path
26
+ from typing import Any
27
+
28
+ import yaml
29
+
30
+ ROOT = Path(__file__).resolve().parents[1]
31
+ PACKAGES = ROOT / "packages"
32
+ PACKS_VOCAB = ROOT / "config" / "discovery" / "packs.yml"
33
+
34
+ TEMPLATES = {
35
+ "skill": "skills/{name}/SKILL.md",
36
+ "rule": "rules/{name}.md",
37
+ "command": "commands/{name}.md",
38
+ }
39
+
40
+
41
+ def _load_vocab() -> dict[str, dict[str, Any]]:
42
+ if not PACKS_VOCAB.exists():
43
+ return {}
44
+ data = yaml.safe_load(PACKS_VOCAB.read_text(encoding="utf-8")) or []
45
+ return {p["id"]: p for p in data}
46
+
47
+
48
+ def _list_packs() -> list[str]:
49
+ if not PACKAGES.exists():
50
+ return []
51
+ return sorted([
52
+ ("core" if p.name == "core" else p.name.removeprefix("pack-"))
53
+ for p in PACKAGES.iterdir() if p.is_dir()
54
+ ])
55
+
56
+
57
+ def _pack_dir(pack_id: str) -> Path:
58
+ return PACKAGES / ("core" if pack_id == "core" else f"pack-{pack_id}")
59
+
60
+
61
+ def _prompt(label: str, default: str | None = None, choices: list[str] | None = None) -> str:
62
+ suffix = f" [{default}]" if default else ""
63
+ if choices:
64
+ suffix = f" ({'/'.join(choices)})" + suffix
65
+ while True:
66
+ raw = input(f"{label}{suffix}: ").strip()
67
+ if not raw and default is not None:
68
+ return default
69
+ if choices and raw not in choices:
70
+ print(f" must be one of: {', '.join(choices)}")
71
+ continue
72
+ if raw:
73
+ return raw
74
+
75
+
76
+ def _frontmatter(name: str, description: str, workspaces: list[str], pack: str) -> str:
77
+ fm: dict[str, Any] = {
78
+ "name": name,
79
+ "description": description,
80
+ "source": "package",
81
+ "workspaces": workspaces,
82
+ "packs": [pack] if pack != "core" else [],
83
+ "lifecycle": "active",
84
+ "trust": {"level": "professional", "confidence": "medium", "human_review_required": False},
85
+ "install": {"default": False, "removable": True},
86
+ }
87
+ if not fm["packs"]:
88
+ del fm["packs"]
89
+ return "---\n" + yaml.safe_dump(fm, sort_keys=False, allow_unicode=True) + "---\n"
90
+
91
+
92
+ def _body(kind: str, name: str, description: str) -> str:
93
+ if kind == "skill":
94
+ return (
95
+ f"\n# {name}\n\n## When to use\n\n{description}\n\n## Procedure\n\n"
96
+ "1. _TODO: replace with the real step-by-step._\n\n"
97
+ "## Examples\n\n_TODO: copy-pasteable example._\n"
98
+ )
99
+ if kind == "rule":
100
+ return f"\n# {name}\n\n{description}\n\n## Iron Law\n\n```\nTODO\n```\n"
101
+ return f"\n# {name}\n\n{description}\n\n## Steps\n\n1. _TODO_\n"
102
+
103
+
104
+ def main() -> int:
105
+ ap = argparse.ArgumentParser(description=__doc__)
106
+ ap.add_argument("--pack")
107
+ ap.add_argument("--type", dest="kind", choices=list(TEMPLATES))
108
+ ap.add_argument("--name")
109
+ ap.add_argument("--description")
110
+ ap.add_argument("--workspace", action="append", default=[])
111
+ ap.add_argument("--force", action="store_true")
112
+ args = ap.parse_args()
113
+
114
+ packs = _list_packs()
115
+ if not packs:
116
+ print("error: no packages/ tree found", file=sys.stderr)
117
+ return 2
118
+ vocab = _load_vocab()
119
+ interactive = sys.stdin.isatty()
120
+ pack = args.pack or (_prompt("pack", default="core", choices=packs) if interactive else "core")
121
+ if pack not in packs:
122
+ print(f"error: pack '{pack}' not in {packs}", file=sys.stderr)
123
+ return 2
124
+ kind = args.kind or (_prompt("type", default="skill", choices=list(TEMPLATES)) if interactive else "skill")
125
+ name = args.name or (_prompt("name (kebab-case)") if interactive else "")
126
+ if not name or " " in name or name != name.lower():
127
+ print(f"error: name '{name}' must be lowercase kebab-case", file=sys.stderr)
128
+ return 2
129
+ description = args.description or (_prompt("description (one line)") if interactive else "TODO: describe trigger")
130
+ workspaces = args.workspace or vocab.get(pack, {}).get("workspaces") or ["engineering"]
131
+
132
+ rel = TEMPLATES[kind].format(name=name)
133
+ out = _pack_dir(pack) / ".agent-src.uncompressed" / rel
134
+ if out.exists() and not args.force:
135
+ print(f"error: {out.relative_to(ROOT)} exists (use --force)", file=sys.stderr)
136
+ return 1
137
+ out.parent.mkdir(parents=True, exist_ok=True)
138
+ out.write_text(_frontmatter(name, description, workspaces, pack) + _body(kind, name, description), encoding="utf-8")
139
+ print(f"created: {out.relative_to(ROOT)}")
140
+ print("next steps:")
141
+ print(" 1. flesh out the body")
142
+ print(" 2. run `task sync` to project into .agent-src/ and .augment/")
143
+ print(" 3. run `task lint-skills` for validation")
144
+ return 0
145
+
146
+
147
+ if __name__ == "__main__":
148
+ sys.exit(main())