@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
@@ -20,6 +20,7 @@ Exit codes: 0 = green; 1 = one or more checks failed; 2 = setup error.
20
20
  """
21
21
  from __future__ import annotations
22
22
 
23
+ import os
23
24
  import shutil
24
25
  import subprocess
25
26
  import sys
@@ -49,8 +50,10 @@ def _check_installer_runs(tmpdir: Path) -> tuple[int, Path | None]:
49
50
  str(ROOT),
50
51
  "--skip-bridges",
51
52
  ]
53
+ # ADR-020: --project is reserved for maintainers; CI is a maintainer context.
54
+ env = {**os.environ, "AGENT_CONFIG_DEV_MODE": "1"}
52
55
  try:
53
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
56
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60, env=env)
54
57
  except subprocess.TimeoutExpired:
55
58
  return _fail("installer timed out after 60s"), None
56
59
  if result.returncode != 0:
@@ -82,7 +85,13 @@ def _check_default_profile(settings: Path) -> int:
82
85
 
83
86
  def _check_decision_engine_block(settings: Path) -> int:
84
87
  """Step 3 — decision_engine block parses through the engine parser."""
85
- sys.path.insert(0, str(ROOT / ".agent-src.uncompressed" / "templates" / "scripts"))
88
+ sys.path.insert(0, str(ROOT / "scripts"))
89
+ from _lib.agent_src import resolve_logical # noqa: E402
90
+
91
+ template_scripts = resolve_logical("templates/scripts") or (
92
+ ROOT / ".agent-src.uncompressed" / "templates" / "scripts"
93
+ )
94
+ sys.path.insert(0, str(template_scripts))
86
95
  try:
87
96
  from work_engine.scoring.decision_engine import ( # type: ignore[import-not-found]
88
97
  DecisionEngineSettings,
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env python3
2
+ """Snapshot the agent-config build outputs for byte-identity verification.
3
+
4
+ Used by monorepo Phase 4 (physical layout move) to assert that the
5
+ pre-move and post-move `task sync` + `task build-discovery` outputs
6
+ match byte-for-byte except for `artefacts[].path` values.
7
+
8
+ Captures sha256 of every file under:
9
+ - .agent-src/
10
+ - .augment/
11
+ - dist/discovery/discovery-manifest.json (also stores parsed copy
12
+ with paths stripped so the post-move diff is path-only)
13
+
14
+ CLI:
15
+ --out PATH write JSON to this path (default: dist/migration/pre-move-snapshot.json)
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import hashlib
21
+ import json
22
+ import sys
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ ROOT = Path(__file__).resolve().parents[1]
27
+ DEFAULT_OUT = ROOT / "dist" / "migration" / "pre-move-snapshot.json"
28
+
29
+ TARGETS = (
30
+ ROOT / ".agent-src",
31
+ ROOT / ".augment",
32
+ )
33
+ MANIFEST = ROOT / "dist" / "discovery" / "discovery-manifest.json"
34
+
35
+
36
+ def _sha256(path: Path) -> str:
37
+ h = hashlib.sha256()
38
+ with path.open("rb") as f:
39
+ for chunk in iter(lambda: f.read(65536), b""):
40
+ h.update(chunk)
41
+ return h.hexdigest()
42
+
43
+
44
+ # Runtime artefacts that never participate in byte-identity verification.
45
+ # Eval last-run.json + pytest caches are gitignored; including them just
46
+ # adds noise when the worktree is clean.
47
+ _SKIP_NAMES = frozenset({"last-run.json"})
48
+ _SKIP_DIRS = frozenset({".pytest_cache", "__pycache__", ".mypy_cache",
49
+ ".ruff_cache", "node_modules", ".DS_Store"})
50
+
51
+
52
+ def _hash_tree(root: Path) -> dict[str, str]:
53
+ if not root.exists():
54
+ return {}
55
+ hashes: dict[str, str] = {}
56
+ for p in sorted(root.rglob("*")):
57
+ if not p.is_file():
58
+ continue
59
+ if p.name in _SKIP_NAMES:
60
+ continue
61
+ if any(part in _SKIP_DIRS for part in p.parts):
62
+ continue
63
+ hashes[p.relative_to(ROOT).as_posix()] = _sha256(p)
64
+ return hashes
65
+
66
+
67
+ def _logical_path(rel: str) -> str:
68
+ """Strip any source-root prefix (legacy or packages/*) so the diff
69
+ compares the artefact's logical identity, not its physical location.
70
+ Non-source paths are returned unchanged.
71
+ """
72
+ posix = rel.replace("\\", "/")
73
+ if posix.startswith(".agent-src.uncompressed/"):
74
+ return posix[len(".agent-src.uncompressed/"):]
75
+ if posix.startswith("packages/"):
76
+ marker = "/.agent-src.uncompressed/"
77
+ idx = posix.find(marker)
78
+ if idx != -1:
79
+ return posix[idx + len(marker):]
80
+ return posix
81
+
82
+
83
+ def _manifest_path_stripped(manifest_path: Path) -> dict[str, Any] | None:
84
+ if not manifest_path.exists():
85
+ return None
86
+ data = json.loads(manifest_path.read_text(encoding="utf-8"))
87
+ # Strip `path` from every artefact so the diff is path-only, then
88
+ # re-sort by (category, checksum) so the list order is content-stable
89
+ # — the original sort is path-based, which shifts when files move
90
+ # between roots even though no artefact body changed.
91
+ artefacts = data.get("artefacts", []) or []
92
+ for a in artefacts:
93
+ a.pop("path", None)
94
+ artefacts.sort(key=lambda a: (a.get("category", ""), a.get("checksum", "")))
95
+ data["artefacts"] = artefacts
96
+ # Normalise unassigned / documented_unassigned to logical paths and
97
+ # re-sort so the post-move diff is content-only.
98
+ for key in ("unassigned", "documented_unassigned"):
99
+ entries = data.get(key) or []
100
+ for e in entries:
101
+ if isinstance(e, dict) and "path" in e:
102
+ e["path"] = _logical_path(e["path"])
103
+ entries.sort(key=lambda e: (e.get("path", ""), e.get("category", "")))
104
+ data[key] = entries
105
+ # Drop volatile fields: timestamp, the manifest's own checksum (which
106
+ # covers everything above and changes with any path text), and the
107
+ # scanner_version (sha of the build script — moves with code edits).
108
+ data.pop("generated_at", None)
109
+ data.pop("checksum", None)
110
+ data.pop("scanner_version", None)
111
+ return data
112
+
113
+
114
+ def _build_snapshot() -> dict[str, Any]:
115
+ snap: dict[str, Any] = {"schema_version": "1", "trees": {}}
116
+ for tgt in TARGETS:
117
+ key = tgt.relative_to(ROOT).as_posix()
118
+ snap["trees"][key] = _hash_tree(tgt)
119
+ snap["manifest_sha256"] = _sha256(MANIFEST) if MANIFEST.exists() else None
120
+ snap["manifest_path_stripped"] = _manifest_path_stripped(MANIFEST)
121
+ return snap
122
+
123
+
124
+ def main() -> int:
125
+ ap = argparse.ArgumentParser(description=__doc__)
126
+ ap.add_argument("--out", type=Path, default=DEFAULT_OUT)
127
+ args = ap.parse_args()
128
+
129
+ snap = _build_snapshot()
130
+ args.out.parent.mkdir(parents=True, exist_ok=True)
131
+ args.out.write_text(
132
+ json.dumps(snap, indent=2, sort_keys=True, ensure_ascii=False) + "\n",
133
+ encoding="utf-8",
134
+ )
135
+ n_files = sum(len(t) for t in snap["trees"].values())
136
+ print(f"Snapshot: {args.out.relative_to(ROOT)}")
137
+ print(f" files hashed : {n_files}")
138
+ print(f" trees : {list(snap['trees'])}")
139
+ print(f" manifest sha256 : {snap['manifest_sha256'][:16] if snap['manifest_sha256'] else 'MISSING'}")
140
+ return 0
141
+
142
+
143
+ if __name__ == "__main__":
144
+ sys.exit(main())
@@ -20,31 +20,59 @@ import re
20
20
  import sys
21
21
  from pathlib import Path
22
22
 
23
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
24
+ from _lib.agent_src import artefact_roots # noqa: E402
25
+
23
26
  REPO_ROOT = Path(__file__).resolve().parent.parent
24
- SRC = REPO_ROOT / ".agent-src.uncompressed"
25
27
 
26
28
 
27
29
  def count(kind: str) -> int:
28
- if kind == "skills":
29
- return sum(1 for _ in (SRC / "skills").rglob("SKILL.md"))
30
30
  if kind == "guidelines":
31
31
  # Guidelines live under docs/guidelines/{topic}/ — they are reference
32
32
  # material, not packaged artefacts. Recursive walk to count every .md.
33
33
  return sum(1 for _ in (REPO_ROOT / "docs" / "guidelines").rglob("*.md"))
34
- if kind == "personas":
35
- # personas live as flat .md files, README excluded
36
- pdir = SRC / "personas"
37
- if not pdir.exists():
38
- return 0
39
- return sum(1 for f in pdir.glob("*.md") if f.name != "README.md")
40
- if kind == "commands":
41
- # Commands may be flat (`commands/<name>.md`) or nested under a
42
- # cluster directory (`commands/<cluster>/<sub>.md`). Walk the tree
43
- # and skip the AGENTS.md reference orchestrator.
44
- return sum(
45
- 1 for f in (SRC / kind).rglob("*.md") if f.name != "AGENTS.md"
46
- )
47
- return sum(1 for _ in (SRC / kind).glob("*.md"))
34
+ total = 0
35
+ seen: set[str] = set()
36
+ for root in artefact_roots():
37
+ subdir = root / kind
38
+ if not subdir.exists():
39
+ continue
40
+ if kind == "skills":
41
+ for f in subdir.rglob("SKILL.md"):
42
+ rel = f.relative_to(root).as_posix()
43
+ if rel in seen:
44
+ continue
45
+ seen.add(rel)
46
+ total += 1
47
+ elif kind == "personas":
48
+ # personas live as flat .md files, README excluded
49
+ for f in subdir.glob("*.md"):
50
+ if f.name == "README.md":
51
+ continue
52
+ rel = f.relative_to(root).as_posix()
53
+ if rel in seen:
54
+ continue
55
+ seen.add(rel)
56
+ total += 1
57
+ elif kind == "commands":
58
+ # Commands may be flat or nested under a cluster directory.
59
+ # Skip the AGENTS.md reference orchestrator.
60
+ for f in subdir.rglob("*.md"):
61
+ if f.name == "AGENTS.md":
62
+ continue
63
+ rel = f.relative_to(root).as_posix()
64
+ if rel in seen:
65
+ continue
66
+ seen.add(rel)
67
+ total += 1
68
+ else:
69
+ for f in subdir.glob("*.md"):
70
+ rel = f.relative_to(root).as_posix()
71
+ if rel in seen:
72
+ continue
73
+ seen.add(rel)
74
+ total += 1
75
+ return total
48
76
 
49
77
 
50
78
  # file → list of (regex, kind)
@@ -27,7 +27,15 @@ except ImportError: # pragma: no cover — bootstrap guard
27
27
  sys.exit(3)
28
28
 
29
29
  REPO_ROOT = Path(__file__).resolve().parent.parent
30
- TEMPLATE_SCRIPTS = REPO_ROOT / ".agent-src.uncompressed" / "templates" / "scripts"
30
+ sys.path.insert(0, str(REPO_ROOT / "scripts"))
31
+ from _lib.agent_src import resolve_logical # noqa: E402
32
+
33
+ # Post-ADR-017 the templates/ tree lives under packages/core/; fall back
34
+ # to the legacy root for pre-move checkouts.
35
+ _template_scripts = resolve_logical("templates/scripts")
36
+ TEMPLATE_SCRIPTS = _template_scripts or (
37
+ REPO_ROOT / ".agent-src.uncompressed" / "templates" / "scripts"
38
+ )
31
39
  if str(TEMPLATE_SCRIPTS) not in sys.path:
32
40
  sys.path.insert(0, str(TEMPLATE_SCRIPTS))
33
41
 
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env python3
2
+ """Stale-manifest guard — re-builds the manifest in memory and diffs it
3
+ against the committed ``dist/discovery/discovery-manifest.json``.
4
+
5
+ CI runs this after a freshly-checked-out tree; non-zero diff = somebody
6
+ forgot to regenerate the manifest after touching artefact frontmatter.
7
+
8
+ The ``generated_at`` field is normalised on both sides (wall-clock).
9
+ Everything else MUST match byte-for-byte.
10
+
11
+ CLI:
12
+ python scripts/validate_discovery_manifest.py [--quiet]
13
+
14
+ Exit codes:
15
+ 0 manifest on disk matches a fresh re-build
16
+ 1 drift detected (committed manifest is stale)
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import json
22
+ import subprocess
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ ROOT = Path(__file__).resolve().parents[1]
27
+ SCANNER = ROOT / "scripts" / "build_discovery_manifest.py"
28
+ COMMITTED = ROOT / "dist" / "discovery" / "discovery-manifest.json"
29
+
30
+
31
+ def _normalise(manifest: dict) -> str:
32
+ out = dict(manifest)
33
+ out["generated_at"] = "<normalised>"
34
+ return json.dumps(out, indent=2, sort_keys=True, ensure_ascii=False) + "\n"
35
+
36
+
37
+ def _fresh_build() -> dict:
38
+ proc = subprocess.run(
39
+ [sys.executable, str(SCANNER)],
40
+ capture_output=True,
41
+ text=True,
42
+ check=False,
43
+ cwd=str(ROOT),
44
+ )
45
+ if proc.returncode != 0:
46
+ print(proc.stderr, file=sys.stderr)
47
+ raise SystemExit(f"scanner failed: exit {proc.returncode}")
48
+ return json.loads(proc.stdout)
49
+
50
+
51
+ def main(argv: list[str] | None = None) -> int:
52
+ parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
53
+ parser.add_argument("--quiet", action="store_true")
54
+ args = parser.parse_args(argv)
55
+
56
+ if not COMMITTED.exists():
57
+ print(
58
+ f"error: committed manifest not found at {COMMITTED.relative_to(ROOT)} "
59
+ "— run `task build-discovery` and commit the output.",
60
+ file=sys.stderr,
61
+ )
62
+ return 1
63
+
64
+ committed = json.loads(COMMITTED.read_text(encoding="utf-8"))
65
+ fresh = _fresh_build()
66
+ sa = _normalise(committed)
67
+ sb = _normalise(fresh)
68
+ if sa != sb:
69
+ print(
70
+ "DRIFT: committed discovery-manifest.json differs from a fresh re-build.",
71
+ file=sys.stderr,
72
+ )
73
+ print(
74
+ " Run `task build-discovery` and commit dist/discovery/.",
75
+ file=sys.stderr,
76
+ )
77
+ # first divergence — single most useful line
78
+ for i, (la, lb) in enumerate(zip(sa.splitlines(), sb.splitlines()), 1):
79
+ if la != lb:
80
+ print(f" first diff at line {i}:", file=sys.stderr)
81
+ print(f" committed: {la}", file=sys.stderr)
82
+ print(f" fresh: {lb}", file=sys.stderr)
83
+ break
84
+ return 1
85
+ if not args.quiet:
86
+ print(
87
+ f"OK {COMMITTED.relative_to(ROOT)} matches fresh re-build "
88
+ f"({committed['stats']['total_artefacts']} artefacts)."
89
+ )
90
+ return 0
91
+
92
+
93
+ if __name__ == "__main__":
94
+ sys.exit(main())
@@ -428,32 +428,51 @@ def _main() -> int:
428
428
  )
429
429
  parser.add_argument(
430
430
  "--root",
431
- default=".agent-src.uncompressed",
432
- help="Source root to scan (default: .agent-src.uncompressed).",
431
+ default=None,
432
+ help=(
433
+ "Source root to scan. Default: every artefact root discovered by "
434
+ "scripts/_lib/agent_src.artefact_roots() (legacy + packages/*)."
435
+ ),
433
436
  )
434
437
  args = parser.parse_args()
435
438
 
436
- root = Path(args.root)
437
- if not root.is_dir():
438
- print(f"error: source root not found: {root}", file=sys.stderr)
439
- return 2
439
+ if args.root is not None:
440
+ root = Path(args.root)
441
+ if not root.is_dir():
442
+ print(f"error: source root not found: {root}", file=sys.stderr)
443
+ return 2
444
+ roots = [root]
445
+ else:
446
+ # Late import keeps the validator usable as a library without the
447
+ # monorepo-helper dependency on the import path.
448
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
449
+ from _lib.agent_src import artefact_roots # noqa: E402
450
+ roots = artefact_roots()
451
+ if not roots:
452
+ print(
453
+ "error: no artefact roots found "
454
+ "(checked .agent-src.uncompressed/ and packages/*/.agent-src.uncompressed/)",
455
+ file=sys.stderr,
456
+ )
457
+ return 2
440
458
 
441
459
  total = 0
442
460
  failing = 0
443
- for artefact_type, path in _iter_artefacts(root):
444
- total += 1
445
- text = path.read_text(encoding="utf-8")
446
- data, _offset = parse_frontmatter(text)
447
- if data is None:
448
- # Other tooling flags missing frontmatter; don't double-report.
449
- continue
450
- schema = load_schema(artefact_type)
451
- errors = validate(data, schema)
452
- if errors:
453
- failing += 1
454
- for error in errors:
455
- print(f"[{artefact_type}] {path}: {error.rule} at "
456
- f"{error.path} {error.message}")
461
+ for root in roots:
462
+ for artefact_type, path in _iter_artefacts(root):
463
+ total += 1
464
+ text = path.read_text(encoding="utf-8")
465
+ data, _offset = parse_frontmatter(text)
466
+ if data is None:
467
+ # Other tooling flags missing frontmatter; don't double-report.
468
+ continue
469
+ schema = load_schema(artefact_type)
470
+ errors = validate(data, schema)
471
+ if errors:
472
+ failing += 1
473
+ for error in errors:
474
+ print(f"[{artefact_type}] {path}: {error.rule} at "
475
+ f"{error.path} – {error.message}")
457
476
 
458
477
  print(f"\n== Frontmatter schema: {total} artefacts, "
459
478
  f"{failing} failing ==")
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env python3
2
+ """Verify the post-move state matches the pre-move snapshot byte-for-byte.
3
+
4
+ Re-runs `task sync` + `task build-discovery` (caller invokes them
5
+ ahead of this script), then loads the fresh outputs and compares them
6
+ against `dist/migration/pre-move-snapshot.json`. The contract:
7
+
8
+ - `.agent-src/` tree hashes must match exactly
9
+ - `.augment/` tree hashes must match exactly
10
+ - `dist/discovery/discovery-manifest.json` with `artefacts[].path`
11
+ stripped + `generated_at` dropped must match exactly
12
+
13
+ Anything else is a regression — exit non-zero with a diff summary.
14
+
15
+ CLI:
16
+ --snapshot PATH path to pre-move snapshot JSON
17
+ --json machine-readable verdict to stdout
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import json
23
+ import sys
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
28
+ from snapshot_agent_outputs import ( # noqa: E402
29
+ _build_snapshot,
30
+ _logical_path,
31
+ _SKIP_DIRS,
32
+ _SKIP_NAMES,
33
+ )
34
+
35
+
36
+ def _normalise_loaded_snapshot(snap: dict[str, Any]) -> None:
37
+ """Re-apply current snapshot filters to a previously-captured snapshot.
38
+
39
+ The pre-move snapshot file is immutable history; this lets verify
40
+ compare it against a freshly-captured post-move snapshot whose
41
+ filters have evolved (runtime-cache exclusion, logical-path stripping,
42
+ volatile-field drop) without regenerating the reference.
43
+ """
44
+ for key, tree in (snap.get("trees") or {}).items():
45
+ keep = {}
46
+ for path, sha in tree.items():
47
+ name = path.rsplit("/", 1)[-1]
48
+ if name in _SKIP_NAMES:
49
+ continue
50
+ if any(part in _SKIP_DIRS for part in path.split("/")):
51
+ continue
52
+ keep[path] = sha
53
+ snap["trees"][key] = keep
54
+ m = snap.get("manifest_path_stripped")
55
+ if isinstance(m, dict):
56
+ for k in ("unassigned", "documented_unassigned"):
57
+ entries = m.get(k) or []
58
+ normalised: list[dict[str, Any]] = []
59
+ for e in entries:
60
+ if not isinstance(e, dict):
61
+ normalised.append(e)
62
+ continue
63
+ if "path" in e:
64
+ e["path"] = _logical_path(e["path"])
65
+ path = e.get("path", "")
66
+ name = path.rsplit("/", 1)[-1]
67
+ if name in _SKIP_NAMES:
68
+ continue
69
+ if any(part in _SKIP_DIRS for part in path.split("/")):
70
+ continue
71
+ normalised.append(e)
72
+ normalised.sort(key=lambda e: (e.get("path", ""), e.get("category", "")))
73
+ m[k] = normalised
74
+ # Recompute the two counts that ride on the filtered lists so the
75
+ # stats block stays consistent with the normalised entries.
76
+ stats = m.get("stats")
77
+ if isinstance(stats, dict):
78
+ stats["documented_unassigned_count"] = len(m.get("documented_unassigned") or [])
79
+ stats["unassigned_count"] = len(m.get("unassigned") or [])
80
+ # Re-sort artefacts by (category, checksum) — pre-move snapshot
81
+ # was sorted by path; that order shifts when files move roots.
82
+ arts = m.get("artefacts") or []
83
+ for a in arts:
84
+ a.pop("path", None)
85
+ arts.sort(key=lambda a: (a.get("category", ""), a.get("checksum", "")))
86
+ m["artefacts"] = arts
87
+ m.pop("checksum", None)
88
+ m.pop("scanner_version", None)
89
+
90
+ ROOT = Path(__file__).resolve().parents[1]
91
+ DEFAULT_SNAPSHOT = ROOT / "dist" / "migration" / "pre-move-snapshot.json"
92
+
93
+
94
+ def _diff_tree(name: str, before: dict[str, str], after: dict[str, str]) -> list[str]:
95
+ issues: list[str] = []
96
+ keys = sorted(set(before) | set(after))
97
+ for k in keys:
98
+ b = before.get(k)
99
+ a = after.get(k)
100
+ if b is None:
101
+ issues.append(f" {name}: added {k}")
102
+ elif a is None:
103
+ issues.append(f" {name}: removed {k}")
104
+ elif a != b:
105
+ issues.append(f" {name}: changed {k} ({b[:12]}… → {a[:12]}…)")
106
+ return issues
107
+
108
+
109
+ def _diff_manifest(before: dict[str, Any] | None, after: dict[str, Any] | None) -> list[str]:
110
+ if before is None and after is None:
111
+ return []
112
+ if before is None:
113
+ return [" manifest: pre-move snapshot missing"]
114
+ if after is None:
115
+ return [" manifest: post-move manifest missing"]
116
+ before_str = json.dumps(before, sort_keys=True, ensure_ascii=False)
117
+ after_str = json.dumps(after, sort_keys=True, ensure_ascii=False)
118
+ if before_str == after_str:
119
+ return []
120
+ # Field-level diff for visibility.
121
+ issues = [" manifest: path-stripped content differs"]
122
+ b_arts = {a.get("name", "?"): a for a in (before.get("artefacts") or [])}
123
+ a_arts = {a.get("name", "?"): a for a in (after.get("artefacts") or [])}
124
+ only_b = sorted(set(b_arts) - set(a_arts))
125
+ only_a = sorted(set(a_arts) - set(b_arts))
126
+ for n in only_b[:10]:
127
+ issues.append(f" artefact removed: {n}")
128
+ for n in only_a[:10]:
129
+ issues.append(f" artefact added: {n}")
130
+ common_changed = []
131
+ for n in sorted(set(b_arts) & set(a_arts)):
132
+ if json.dumps(b_arts[n], sort_keys=True) != json.dumps(a_arts[n], sort_keys=True):
133
+ common_changed.append(n)
134
+ for n in common_changed[:10]:
135
+ issues.append(f" artefact changed: {n}")
136
+ return issues
137
+
138
+
139
+ def main() -> int:
140
+ ap = argparse.ArgumentParser(description=__doc__)
141
+ ap.add_argument("--snapshot", type=Path, default=DEFAULT_SNAPSHOT)
142
+ ap.add_argument("--json", action="store_true", help="emit machine-readable verdict to stdout")
143
+ args = ap.parse_args()
144
+
145
+ if not args.snapshot.exists():
146
+ print(f"ERROR: snapshot missing: {args.snapshot}", file=sys.stderr)
147
+ return 2
148
+
149
+ before = json.loads(args.snapshot.read_text(encoding="utf-8"))
150
+ after = _build_snapshot()
151
+
152
+ # The pre-move snapshot was captured before _hash_tree / manifest
153
+ # stripping learned to filter runtime artefacts. Re-apply the current
154
+ # filter to the loaded snapshot so the diff is apples-to-apples.
155
+ _normalise_loaded_snapshot(before)
156
+
157
+ issues: list[str] = []
158
+ for key in (".agent-src", ".augment"):
159
+ issues.extend(_diff_tree(key, before["trees"].get(key, {}), after["trees"].get(key, {})))
160
+ issues.extend(_diff_manifest(before.get("manifest_path_stripped"), after.get("manifest_path_stripped")))
161
+
162
+ ok = not issues
163
+ if args.json:
164
+ print(json.dumps({
165
+ "ok": ok,
166
+ "issue_count": len(issues),
167
+ "issues": issues,
168
+ }, indent=2))
169
+ else:
170
+ if ok:
171
+ print("verify_physical_move: byte-identity OK")
172
+ print(f" .agent-src/ files: {len(after['trees'].get('.agent-src', {}))}")
173
+ print(f" .augment/ files: {len(after['trees'].get('.augment', {}))}")
174
+ print(f" manifest: path-stripped content matches")
175
+ else:
176
+ print(f"verify_physical_move: FAIL ({len(issues)} issue(s))")
177
+ for line in issues[:50]:
178
+ print(line)
179
+ if len(issues) > 50:
180
+ print(f" … and {len(issues) - 50} more")
181
+ return 0 if ok else 1
182
+
183
+
184
+ if __name__ == "__main__":
185
+ sys.exit(main())
@@ -2,7 +2,6 @@
2
2
  version: 1
3
3
  identity:
4
4
  name: ""
5
- nickname: ""
6
5
  language: "en"
7
6
  role:
8
7
  - ""
@@ -0,0 +1,21 @@
1
+ version: 1
2
+ identity:
3
+ name: ""
4
+ language: "en"
5
+ role:
6
+ - ""
7
+ style:
8
+ formality: "informal"
9
+ pace: "pragmatic"
10
+ voice_sample: |
11
+ Replace this block with one to three sentences in your own
12
+ writing style. The agent uses it as a tone anchor — paste the
13
+ way you would actually message a colleague, not a polished pitch.
14
+ last_updated: "1970-01-01"
15
+ # notes: |
16
+ # Optional free-form prose. Anything you want the agent to remember
17
+ # across sessions — preferred terminology, recurring projects,
18
+ # conventions you want enforced. Keep it short; everything here
19
+ # loads into every reply. Hard cap: 8 000 chars.
20
+ #
21
+ # Schema reference: docs/contracts/agent-user-schema.md