@event4u/agent-config 5.6.1 → 5.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/.agent-src/commands/agent-handoff.md +1 -1
  2. package/.agent-src/commands/agent-status.md +1 -1
  3. package/.agent-src/commands/agents/audit.md +1 -1
  4. package/.agent-src/commands/agents/init.md +1 -1
  5. package/.agent-src/commands/agents/user/accept.md +3 -3
  6. package/.agent-src/commands/agents/user/init.md +4 -4
  7. package/.agent-src/commands/agents/user/show.md +3 -3
  8. package/.agent-src/commands/agents/user/update.md +3 -3
  9. package/.agent-src/commands/agents/user.md +1 -1
  10. package/.agent-src/commands/agents.md +1 -1
  11. package/.agent-src/commands/analytics/prune.md +1 -1
  12. package/.agent-src/commands/analytics/show.md +1 -1
  13. package/.agent-src/commands/analytics.md +1 -1
  14. package/.agent-src/commands/bug-fix.md +1 -1
  15. package/.agent-src/commands/challenge-me.md +1 -1
  16. package/.agent-src/commands/chat-history/import.md +1 -1
  17. package/.agent-src/commands/chat-history/learn.md +1 -1
  18. package/.agent-src/commands/chat-history/show.md +1 -1
  19. package/.agent-src/commands/chat-history.md +1 -1
  20. package/.agent-src/commands/check-current-md.md +1 -1
  21. package/.agent-src/commands/condense.md +1 -1
  22. package/.agent-src/commands/context.md +1 -1
  23. package/.agent-src/commands/cost-report.md +13 -8
  24. package/.agent-src/commands/council.md +3 -3
  25. package/.agent-src/commands/create-pr/description-only.md +1 -1
  26. package/.agent-src/commands/create-pr.md +1 -1
  27. package/.agent-src/commands/e2e-heal.md +1 -1
  28. package/.agent-src/commands/e2e-plan.md +1 -1
  29. package/.agent-src/commands/feature.md +1 -1
  30. package/.agent-src/commands/fix/ci.md +1 -1
  31. package/.agent-src/commands/fix/portability.md +1 -1
  32. package/.agent-src/commands/fix/pr-bot-comments.md +1 -1
  33. package/.agent-src/commands/fix/pr-comments.md +1 -1
  34. package/.agent-src/commands/fix/pr-developer-comments.md +1 -1
  35. package/.agent-src/commands/fix/refs.md +1 -1
  36. package/.agent-src/commands/fix/seeder.md +1 -1
  37. package/.agent-src/commands/fix.md +1 -1
  38. package/.agent-src/commands/judge.md +1 -1
  39. package/.agent-src/commands/knowledge/cross-repo.md +1 -1
  40. package/.agent-src/commands/knowledge/forget.md +1 -1
  41. package/.agent-src/commands/knowledge/ingest.md +1 -1
  42. package/.agent-src/commands/knowledge/list.md +1 -1
  43. package/.agent-src/commands/knowledge.md +1 -1
  44. package/.agent-src/commands/memory/add.md +1 -1
  45. package/.agent-src/commands/memory/learn-low-impact.md +1 -1
  46. package/.agent-src/commands/memory/load.md +1 -1
  47. package/.agent-src/commands/memory/mine-session.md +1 -1
  48. package/.agent-src/commands/memory/promote.md +1 -1
  49. package/.agent-src/commands/memory/propose.md +1 -1
  50. package/.agent-src/commands/memory.md +1 -1
  51. package/.agent-src/commands/mode.md +1 -1
  52. package/.agent-src/commands/optimize/agents-dir.md +1 -1
  53. package/.agent-src/commands/optimize/augmentignore.md +1 -1
  54. package/.agent-src/commands/optimize/rtk.md +1 -1
  55. package/.agent-src/commands/optimize/skills.md +1 -1
  56. package/.agent-src/commands/optimize.md +1 -1
  57. package/.agent-src/commands/orchestrate.md +1 -1
  58. package/.agent-src/commands/override/create.md +1 -1
  59. package/.agent-src/commands/override/manage.md +1 -1
  60. package/.agent-src/commands/override.md +1 -1
  61. package/.agent-src/commands/package-reset.md +1 -1
  62. package/.agent-src/commands/prediction-pool.md +234 -0
  63. package/.agent-src/commands/profile/activate.md +81 -0
  64. package/.agent-src/commands/profile/deactivate.md +68 -0
  65. package/.agent-src/commands/profile/show.md +70 -0
  66. package/.agent-src/commands/profile.md +68 -0
  67. package/.agent-src/commands/project-health.md +1 -1
  68. package/.agent-src/commands/quality-fix.md +1 -1
  69. package/.agent-src/commands/roadmap/process-full.md +1 -1
  70. package/.agent-src/commands/roadmap/process-phase.md +1 -1
  71. package/.agent-src/commands/roadmap/process-step.md +1 -1
  72. package/.agent-src/commands/roadmap.md +1 -1
  73. package/.agent-src/commands/set-cost-profile.md +9 -9
  74. package/.agent-src/commands/skill/preview.md +3 -3
  75. package/.agent-src/commands/skill.md +1 -1
  76. package/.agent-src/commands/skills/discover.md +1 -1
  77. package/.agent-src/commands/skills.md +1 -1
  78. package/.agent-src/commands/sync-agent-settings.md +3 -3
  79. package/.agent-src/commands/sync-gitignore/fix.md +1 -1
  80. package/.agent-src/commands/sync-gitignore.md +1 -1
  81. package/.agent-src/commands/update-form-request-messages.md +1 -1
  82. package/.agent-src/presets/README.md +1 -1
  83. package/.agent-src/profiles/README.md +1 -1
  84. package/.agent-src/rules/non-destructive-by-default.md +2 -1
  85. package/.agent-src/skills/check-refs/SKILL.md +1 -1
  86. package/.agent-src/skills/finishing-a-development-branch/SKILL.md +1 -1
  87. package/.agent-src/skills/git-workflow/SKILL.md +1 -1
  88. package/.agent-src/skills/jira-integration/SKILL.md +1 -1
  89. package/.agent-src/skills/markitdown/SKILL.md +1 -1
  90. package/.agent-src/skills/prediction-pool-optimizer/SKILL.md +314 -0
  91. package/.agent-src/skills/prediction-pool-optimizer/evals/triggers.json +20 -0
  92. package/.agent-src/skills/prediction-pool-optimizer/reference/ev-fixtures.md +175 -0
  93. package/.agent-src/skills/prediction-pool-optimizer/reference/odds-and-bonus.md +109 -0
  94. package/.agent-src/skills/rtk-output-filtering/SKILL.md +1 -1
  95. package/.agent-src/skills/script-writing/SKILL.md +1 -1
  96. package/.agent-src/skills/token-optimizer/SKILL.md +1 -1
  97. package/.agent-src/skills/using-git-worktrees/SKILL.md +1 -1
  98. package/.agent-src/templates/agent-settings.md +7 -7
  99. package/.agent-src/templates/agents/agent-project-settings.example.yml +2 -2
  100. package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +54 -6
  101. package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +1 -1
  102. package/.agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.py +9 -7
  103. package/.agent-src/templates/scripts/work_engine/hooks/settings.py +9 -10
  104. package/.agent-src/templates/scripts/work_engine/scoring/memory_visibility.py +17 -4
  105. package/.claude-plugin/marketplace.json +370 -364
  106. package/CHANGELOG.md +108 -0
  107. package/README.md +2 -2
  108. package/config/agent-settings.template.yml +11 -2
  109. package/config/discovery/packs.yml +11 -0
  110. package/config/discovery/session-profiles.yml +37 -0
  111. package/config/discovery/workspaces.yml +1 -1
  112. package/config/profiles/balanced.ini +1 -1
  113. package/config/profiles/full.ini +1 -1
  114. package/config/profiles/minimal.ini +1 -1
  115. package/dist/discovery/deprecation-report.md +1 -1
  116. package/dist/discovery/discovery-manifest.json +254 -100
  117. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  118. package/dist/discovery/discovery-manifest.summary.md +4 -3
  119. package/dist/discovery/orphan-report.md +1 -1
  120. package/dist/discovery/packs.json +41 -6
  121. package/dist/discovery/trust-report.md +3 -3
  122. package/dist/discovery/workspaces.json +19 -6
  123. package/dist/mcp/registry-manifest.json +3 -3
  124. package/dist/server/io/substituteTemplate.js +3 -3
  125. package/dist/server/io/substituteTemplate.js.map +1 -1
  126. package/dist/server/routes/settings.js +2 -2
  127. package/dist/server/routes/settings.js.map +1 -1
  128. package/dist/server/schemas/settings.js +4 -2
  129. package/dist/server/schemas/settings.js.map +1 -1
  130. package/dist/ui/assets/{index-DVsyUMZe.js → index-5lFqAKL0.js} +2 -2
  131. package/dist/ui/assets/index-5lFqAKL0.js.map +1 -0
  132. package/dist/ui/index.html +1 -1
  133. package/docs/architecture/current-onboard-baseline.md +3 -3
  134. package/docs/architecture.md +2 -2
  135. package/docs/catalog.md +11 -5
  136. package/docs/contracts/adr-level-6-productization.md +1 -1
  137. package/docs/contracts/command-clusters.md +2 -0
  138. package/docs/contracts/config-presets.md +2 -2
  139. package/docs/contracts/cost-profile-defaults.md +5 -5
  140. package/docs/contracts/discovery-manifest.schema.json +1 -1
  141. package/docs/contracts/explain-trace.schema.json +3 -3
  142. package/docs/contracts/memory-visibility-v1.md +15 -7
  143. package/docs/contracts/profile-system.md +2 -2
  144. package/docs/contracts/session-profile-overlay.md +120 -0
  145. package/docs/contracts/settings-api.md +3 -3
  146. package/docs/contracts/value-report-schema.md +14 -1
  147. package/docs/customization.md +47 -5
  148. package/docs/decisions/ADR-010-profile-pack-preset-boundary.md +47 -11
  149. package/docs/decisions/ADR-013-discovery-frontmatter-contract.md +16 -2
  150. package/docs/decisions/ADR-034-per-skill-model-recommendation-transport.md +1 -1
  151. package/docs/decisions/ADR-036-global-install-browser-wizard-handoff.md +106 -0
  152. package/docs/decisions/ADR-037-cost-profile-untangle.md +117 -0
  153. package/docs/decisions/ADR-038-canonical-settings-path.md +66 -0
  154. package/docs/decisions/ADR-039-claude-skills-untracked.md +139 -0
  155. package/docs/decisions/ADR-rule-kernel-and-router.md +1 -1
  156. package/docs/decisions/INDEX.md +4 -0
  157. package/docs/development.md +12 -0
  158. package/docs/getting-started.md +2 -2
  159. package/docs/guidelines/agent-infra/layered-settings.md +10 -4
  160. package/docs/installation.md +3 -3
  161. package/docs/setup/mcp-client-config.md +1 -1
  162. package/docs/skills-catalog.md +5 -1
  163. package/docs/value.md +9 -7
  164. package/docs/wizard.md +1 -1
  165. package/llms.txt +4 -0
  166. package/package.json +1 -1
  167. package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
  168. package/scripts/_cli/cmd_doctor.py +3 -2
  169. package/scripts/_cli/cmd_explain.py +1 -1
  170. package/scripts/_cli/cmd_versions.py +2 -2
  171. package/scripts/_cli/explain_last/inputs.py +11 -8
  172. package/scripts/_cli/explain_last/sections/inputs.py +1 -1
  173. package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
  174. package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
  175. package/scripts/_lib/agent_settings.py +54 -6
  176. package/scripts/_lib/agent_src.py +30 -0
  177. package/scripts/_lib/value_ladder.py +99 -2
  178. package/scripts/_lib/value_report.py +30 -16
  179. package/scripts/ai_council/modes.py +1 -1
  180. package/scripts/ai_council/session.py +5 -1
  181. package/scripts/audit_command_surface.py +7 -1
  182. package/scripts/audit_initial_context.py +26 -2
  183. package/scripts/check_gate_paths.py +117 -0
  184. package/scripts/check_references.py +51 -2
  185. package/scripts/check_skill_requires.py +143 -0
  186. package/scripts/check_test_coverage_diff.py +180 -0
  187. package/scripts/compile_router.py +5 -1
  188. package/scripts/condense.py +92 -4
  189. package/scripts/config/session_profiles.py +492 -0
  190. package/scripts/council_cli.py +5 -1
  191. package/scripts/first-run.sh +11 -11
  192. package/scripts/hook_manifest.yaml +15 -7
  193. package/scripts/hooks/dispatch_hook.py +8 -0
  194. package/scripts/install +14 -1
  195. package/scripts/install-hooks.sh +2 -1
  196. package/scripts/install.py +203 -433
  197. package/scripts/install_anthropic_key.sh +1 -1
  198. package/scripts/install_openai_key.sh +1 -1
  199. package/scripts/inventory_abstraction_budget.py +6 -1
  200. package/scripts/lint_agents_md.py +11 -4
  201. package/scripts/lint_discovery_vocabulary.py +5 -5
  202. package/scripts/lint_hook_concern_budget.py +5 -1
  203. package/scripts/lint_marketplace.py +18 -7
  204. package/scripts/lint_roadmap_ci_steps.py +5 -1
  205. package/scripts/lint_roadmap_complexity.py +5 -1
  206. package/scripts/lint_value_dashboard.py +1 -1
  207. package/scripts/mcp_server/prompts.py +5 -1
  208. package/scripts/prediction-pool/adapters/_schema.md +42 -0
  209. package/scripts/prediction-pool/adapters/kicktipp.yml +23 -0
  210. package/scripts/prediction-pool/poisson_sim.py +167 -0
  211. package/scripts/prediction-pool/pool_winsim.py +236 -0
  212. package/scripts/prediction-pool/score_ev.py +188 -0
  213. package/scripts/profile_staleness_hook.py +69 -0
  214. package/scripts/render_value_md.py +1 -0
  215. package/scripts/roadmap_progress_hook.py +56 -6
  216. package/scripts/schemas/agent-settings.schema.json +77 -0
  217. package/scripts/schemas/skill.schema.json +7 -0
  218. package/scripts/smoke_quickstart.py +7 -6
  219. package/scripts/sync_agent_settings.py +12 -5
  220. package/scripts/validate_agent_settings.py +124 -0
  221. package/scripts/validate_decision_engine.py +5 -1
  222. package/templates/minimal/.agent-settings.yml +1 -1
  223. package/dist/ui/assets/index-DVsyUMZe.js.map +0 -1
  224. package/scripts/measure_roadmap_trajectory.py +0 -112
  225. package/scripts/verify_roadmap_closure.py +0 -327
@@ -43,10 +43,12 @@ try:
43
43
  condense_rung_from_telegraph_v2,
44
44
  destructive_stops_metric,
45
45
  load_rung_from_frugality,
46
+ load_rung_from_projection,
46
47
  load_rung_from_router,
47
48
  rtk_rung_from_report,
48
49
  selection_metric_from_dev_reports,
49
50
  terse_rung_from_telegraph_v1,
51
+ thin_rung_from_projection,
50
52
  )
51
53
  except ImportError:
52
54
  from scripts._lib.value_ladder import ( # type: ignore[no-redef]
@@ -59,15 +61,18 @@ except ImportError:
59
61
  condense_rung_from_telegraph_v2,
60
62
  destructive_stops_metric,
61
63
  load_rung_from_frugality,
64
+ load_rung_from_projection,
62
65
  load_rung_from_router,
63
66
  rtk_rung_from_report,
64
67
  selection_metric_from_dev_reports,
65
68
  terse_rung_from_telegraph_v1,
69
+ thin_rung_from_projection,
66
70
  )
67
71
 
68
72
 
69
73
  REPO_ROOT = Path(__file__).resolve().parent.parent.parent
70
74
  ROUTER_JSON = REPO_ROOT / "dist" / "router.json"
75
+ PROJECTION_COST = REPO_ROOT / "internal" / "bench" / "reports" / "projection-cost.json"
71
76
  RULES_DIR = REPO_ROOT / ".agent-src" / "rules"
72
77
  CHARTER_PATH = REPO_ROOT / ".agent-src" / "contexts" / "contracts" / "frugality-charter.md"
73
78
  FRUGALITY_BASELINE = REPO_ROOT / "agents" / "runtime" / "frugality" / "baseline.jsonl"
@@ -240,28 +245,37 @@ def assemble_value_v1(
240
245
  # Load rung — prefer the canonical kernel list from dist/router.json
241
246
  # (real always-loaded footprint), fall back to the frugality canon
242
247
  # baseline only when the router is missing on disk.
243
- router = safe_load_json(ROUTER_JSON)
244
- if router and "kernel" in router:
245
- rule_chars = {
246
- p.stem: len(p.read_text())
247
- for p in RULES_DIR.glob("*.md")
248
- } if RULES_DIR.exists() else {}
249
- charter_chars = (
250
- len(CHARTER_PATH.read_text()) if CHARTER_PATH.exists() else 0
251
- )
252
- load_rung = load_rung_from_router(
253
- router, rule_chars, charter_chars, ref, pricing_row
254
- )
255
- else:
256
- load_rung = load_rung_from_frugality(
257
- latest_frugality_record(), ref, pricing_row
258
- )
248
+ # Prefer the REAL eager footprint (projection-cost.json) — 0B.6 confirmed
249
+ # the primary tool eager-loads every rule body. Fall back to the
250
+ # kernel-only router rung, then the frugality canon, when the projection
251
+ # report is missing.
252
+ projection = safe_load_json(PROJECTION_COST)
253
+ load_rung = load_rung_from_projection(projection, ref, pricing_row)
254
+ if load_rung is None:
255
+ router = safe_load_json(ROUTER_JSON)
256
+ if router and "kernel" in router:
257
+ rule_chars = {
258
+ p.stem: len(p.read_text())
259
+ for p in RULES_DIR.glob("*.md")
260
+ } if RULES_DIR.exists() else {}
261
+ charter_chars = (
262
+ len(CHARTER_PATH.read_text()) if CHARTER_PATH.exists() else 0
263
+ )
264
+ load_rung = load_rung_from_router(
265
+ router, rule_chars, charter_chars, ref, pricing_row
266
+ )
267
+ else:
268
+ load_rung = load_rung_from_frugality(
269
+ latest_frugality_record(), ref, pricing_row
270
+ )
271
+ thin_rung = thin_rung_from_projection(projection, ref, pricing_row)
259
272
  t2 = safe_load_json(TELEGRAPH_V2)
260
273
  t1 = safe_load_json(TELEGRAPH_V1)
261
274
  rtk = safe_load_json(RTK_LATEST)
262
275
  ladder: List[Dict[str, Any]] = [
263
276
  baseline_rung(ref),
264
277
  load_rung,
278
+ thin_rung,
265
279
  condense_rung_from_telegraph_v2(t2, baseline_input_tokens, ref, pricing_row),
266
280
  rtk_rung_from_report(rtk, ref, pricing_row),
267
281
  terse_rung_from_telegraph_v1(t1, ref, pricing_row),
@@ -17,7 +17,7 @@ Resolution precedence — first non-empty wins:
17
17
  3. Global setting ``ai_council.mode``
18
18
  4. Built-in default ``manual``
19
19
 
20
- This mirrors how ``cost_profile`` resolves in
20
+ This mirrors how ``rule_loading_tier`` resolves in
21
21
  ``.augment/guidelines/agent-infra/layered-settings.md``.
22
22
 
23
23
  The resolver is pure — it never touches the filesystem or environment.
@@ -28,6 +28,10 @@ import shutil
28
28
  import sys
29
29
  from dataclasses import dataclass, field
30
30
  from pathlib import Path
31
+ try: # invocation-agnostic import (repo-root-on-path vs scripts-on-path)
32
+ from scripts._lib.agent_settings import project_settings_path
33
+ except ModuleNotFoundError: # pragma: no cover
34
+ from _lib.agent_settings import project_settings_path
31
35
  from typing import Iterable
32
36
 
33
37
  from scripts.ai_council.clients import CouncilResponse
@@ -37,7 +41,7 @@ REPO_ROOT = Path(__file__).resolve().parents[2]
37
41
  SESSIONS_DIR = REPO_ROOT / "agents" / "runtime" / "council" / "sessions"
38
42
  QUESTIONS_DIR = REPO_ROOT / "agents" / "runtime" / "council" / "questions"
39
43
  RESPONSES_DIR = REPO_ROOT / "agents" / "runtime" / "council" / "responses"
40
- SETTINGS_FILE = REPO_ROOT / ".agent-settings.yml"
44
+ SETTINGS_FILE = project_settings_path(REPO_ROOT)
41
45
 
42
46
  # Default retention for all council artefacts (questions, responses,
43
47
  # sessions). Overridden by `ai_council.session_retention_days`
@@ -37,12 +37,18 @@ from pathlib import Path
37
37
  from typing import List
38
38
 
39
39
  REPO_ROOT = Path(__file__).resolve().parent.parent
40
+ sys.path.insert(0, str(REPO_ROOT / "scripts"))
41
+ from _lib.agent_src import resolve_package_core_path # noqa: E402
42
+
40
43
  # Pre-monorepo: REPO_ROOT/.agent-src.uncondensed/commands. Post-move (ADR-017)
41
44
  # the core command surface lives under packages/core/.agent-src.uncondensed.
42
45
  # Fall back to the legacy path only if the packages layout is absent.
43
- _CORE_COMMANDS = REPO_ROOT / "packages" / "core" / ".agent-src.uncondensed" / "commands"
46
+ _CORE_COMMANDS = resolve_package_core_path(".agent-src.uncondensed/commands")
44
47
  _LEGACY_COMMANDS = REPO_ROOT / ".agent-src.uncondensed" / "commands"
45
48
  DEFAULT_ROOT = _CORE_COMMANDS if _CORE_COMMANDS.is_dir() else _LEGACY_COMMANDS
49
+ # Enforced packages/core target — read by scripts/check_gate_paths.py so a
50
+ # future move that desyncs this path fails CI instead of silently no-opping.
51
+ GATE_CORE_PATHS = (_CORE_COMMANDS,)
46
52
  REPORT_DIR = REPO_ROOT / "agents" / "reports"
47
53
  OUT_JSON = REPORT_DIR / "command-surface.json"
48
54
  OUT_MD = REPORT_DIR / "command-surface.md"
@@ -35,6 +35,13 @@ from pathlib import Path
35
35
  REPO_ROOT = Path(__file__).resolve().parent.parent
36
36
  sys.path.insert(0, str(REPO_ROOT / "scripts"))
37
37
  from _lib import token_count # noqa: E402
38
+ from _lib.agent_src import resolve_package_core_path # noqa: E402
39
+
40
+ _CORE_SRC = resolve_package_core_path(".agent-src.uncondensed")
41
+ # Enforced packages/core targets — the skills + commands dirs the
42
+ # description-catalog globs scan. Read by scripts/check_gate_paths.py so a
43
+ # future move that desyncs them fails CI instead of silently no-opping.
44
+ GATE_CORE_PATHS = (_CORE_SRC / "skills", _CORE_SRC / "commands")
38
45
 
39
46
  try:
40
47
  import yaml
@@ -111,10 +118,11 @@ def _catalog(glob_pat: str) -> dict:
111
118
 
112
119
  def description_catalog() -> dict:
113
120
  """0B.4 — description-catalog cost (eager progressive-disclosure surface)."""
121
+ core_rel = _CORE_SRC.relative_to(REPO_ROOT).as_posix()
114
122
  return {
115
123
  "skills_projected": _catalog(".claude/skills/*/SKILL.md"),
116
- "skills_core_source": _catalog("packages/core/.agent-src.uncondensed/skills/*/SKILL.md"),
117
- "commands_core_source": _catalog("packages/core/.agent-src.uncondensed/commands/**/*.md"),
124
+ "skills_core_source": _catalog(f"{core_rel}/skills/*/SKILL.md"),
125
+ "commands_core_source": _catalog(f"{core_rel}/commands/**/*.md"),
118
126
  }
119
127
 
120
128
 
@@ -132,11 +140,27 @@ def longest_rules(top: int = 10) -> list[dict]:
132
140
  return rows[:top]
133
141
 
134
142
 
143
+ def thin_projection() -> dict:
144
+ """Eager-vs-thin rule-layer footprint (Phase 3.1 lever).
145
+
146
+ Reuses `scripts/project_thin_rules.py::measure` so the value dashboard can
147
+ cite a single persisted source for both the eager always-on cost and the
148
+ thin-projection saving. Returns an empty dict if the measurer is
149
+ unavailable, so the audit never hard-fails on it.
150
+ """
151
+ try:
152
+ from project_thin_rules import measure as _measure # noqa: E402
153
+ return _measure()
154
+ except Exception: # pragma: no cover — best-effort enrichment
155
+ return {}
156
+
157
+
135
158
  def build() -> dict:
136
159
  return {
137
160
  "generated": _dt.datetime.now(_dt.timezone.utc).isoformat(timespec="seconds"),
138
161
  "token_method": token_count.method_note(),
139
162
  "rule_footprint": rule_footprint(),
163
+ "thin_projection": thin_projection(),
140
164
  "description_catalog": description_catalog(),
141
165
  "longest_rules": longest_rules(),
142
166
  }
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env python3
2
+ """Gate path-integrity check (R2 of road-to-test-and-gate-integrity).
3
+
4
+ Asserts that every security/quality gate which enforces something against
5
+ a fixed ``packages/core/`` target still resolves that target on disk. A
6
+ ``packages/core/`` move that desyncs a gate's hard-coded path fails CI here
7
+ instead of silently no-opping (the ``aab5755`` class: the Iron-Law SHA gate
8
+ pointed at a stale path and enforced nothing while CI stayed green).
9
+
10
+ Design (AI council, claude-sonnet-4-5 + gpt-4o, 2026-06-02):
11
+
12
+ - The check reads each gate's ACTUAL enforced paths via its module-level
13
+ ``GATE_CORE_PATHS`` attribute — it does NOT re-declare a copy of the path
14
+ strings. A hand-maintained path registry would reintroduce the very
15
+ desync risk this guards against, one layer down.
16
+ - Scope is strictly the single-root hard-coders. Multi-root gates that
17
+ resolve via ``artefact_roots()`` (e.g. ``iron_law_sha``) are excluded:
18
+ asserting a single ``packages/core/`` path for them would false-pass on a
19
+ legacy layout or false-fail on a pack-only layout.
20
+
21
+ The input set is this gate list (no separate config file).
22
+
23
+ Usage:
24
+ python3 scripts/check_gate_paths.py
25
+ Exit codes: 0 = all enforced targets resolve under packages/core/ ·
26
+ 1 = at least one missing / out-of-tree target · 2 = a gate failed to import.
27
+ """
28
+ from __future__ import annotations
29
+
30
+ import importlib
31
+ import sys
32
+ from pathlib import Path
33
+
34
+ REPO_ROOT = Path(__file__).resolve().parent.parent
35
+ sys.path.insert(0, str(REPO_ROOT / "scripts"))
36
+ from _lib.agent_src import resolve_package_core_path # noqa: E402
37
+
38
+ PACKAGE_CORE = resolve_package_core_path("")
39
+
40
+ # Single-root gates that enforce against a fixed packages/core/ target and
41
+ # expose it via a module-level GATE_CORE_PATHS tuple. Adding a gate here is
42
+ # the only manual step; its paths are read from the gate, never copied.
43
+ GATES: tuple[str, ...] = (
44
+ "inventory_abstraction_budget",
45
+ "audit_command_surface",
46
+ "lint_agents_md",
47
+ "audit_initial_context",
48
+ )
49
+
50
+
51
+ def _is_under_core(p: Path) -> bool:
52
+ try:
53
+ p.resolve().relative_to(PACKAGE_CORE.resolve())
54
+ return True
55
+ except ValueError:
56
+ return False
57
+
58
+
59
+ def collect_gate_paths(gate_modules: tuple[str, ...]) -> dict[str, list[Path]]:
60
+ """Import each gate and read its declared ``GATE_CORE_PATHS``.
61
+
62
+ Raises ``ImportError`` (surfaced as exit 2 by ``main``) if a gate cannot
63
+ be imported — a gate whose path logic broke at import time is itself a
64
+ failure this check should not swallow.
65
+ """
66
+ out: dict[str, list[Path]] = {}
67
+ for name in gate_modules:
68
+ mod = importlib.import_module(name)
69
+ paths = getattr(mod, "GATE_CORE_PATHS", None)
70
+ if not paths:
71
+ raise AttributeError(
72
+ f"{name} has no non-empty GATE_CORE_PATHS — gate cannot be "
73
+ f"checked. Declare the packages/core targets it enforces."
74
+ )
75
+ out[name] = [Path(p) for p in paths]
76
+ return out
77
+
78
+
79
+ def check_paths(named: dict[str, list[Path]]) -> list[tuple[str, str, Path]]:
80
+ """Return ``(gate, reason, path)`` for every target that fails.
81
+
82
+ Pure (no import side effects) so tests can drive it with fixtures.
83
+ A target fails when it does not resolve under ``packages/core/`` or
84
+ does not exist on disk.
85
+ """
86
+ failures: list[tuple[str, str, Path]] = []
87
+ for gate, paths in named.items():
88
+ for p in paths:
89
+ if not _is_under_core(p):
90
+ failures.append((gate, "not under packages/core/", p))
91
+ elif not p.exists():
92
+ failures.append((gate, "target does not exist", p))
93
+ return failures
94
+
95
+
96
+ def main() -> int:
97
+ try:
98
+ named = collect_gate_paths(GATES)
99
+ except (ImportError, AttributeError) as exc:
100
+ print(f"❌ check-gate-paths: {exc}", file=sys.stderr)
101
+ return 2
102
+ failures = check_paths(named)
103
+ if failures:
104
+ print("❌ check-gate-paths: gate target(s) do not resolve under packages/core/:")
105
+ for gate, reason, path in failures:
106
+ print(f" {gate}: {reason} → {path}")
107
+ print("\n A packages/core/ move likely desynced a gate. Fix the gate's")
108
+ print(" GATE_CORE_PATHS (built via resolve_package_core_path) or the move.")
109
+ return 1
110
+ total = sum(len(v) for v in named.values())
111
+ print(f"✅ check-gate-paths: {total} enforced target(s) across "
112
+ f"{len(named)} gate(s) resolve under packages/core/.")
113
+ return 0
114
+
115
+
116
+ if __name__ == "__main__":
117
+ raise SystemExit(main())
@@ -133,6 +133,53 @@ EXAMPLE_PATH_PATTERNS = [
133
133
  ]
134
134
 
135
135
 
136
+ @dataclass(frozen=True)
137
+ class AllowlistPattern:
138
+ """A token-class allowlist entry. `reason` is mandatory and auditable."""
139
+ pattern: "re.Pattern[str]"
140
+ reason: str
141
+
142
+
143
+ # Content-class allowlist for known NON-reference token shapes.
144
+ #
145
+ # The skill/rule prose patterns (`X` skill / `X` rule) occasionally match
146
+ # a backtick token that is not an artifact id — an execution-type enum
147
+ # value, a pack identifier, or a bare meta-qualifier keyword. Historically
148
+ # each such false positive was dodged by *rewording the prose per file*
149
+ # (e.g. dc84ed01 "reword execution-type mentions to dodge check-refs
150
+ # false positive", bd02ef0b "avoid check-refs false-positive on pack
151
+ # name"), a treadmill that distorts natural wording release after release.
152
+ # This layer matches the token *class* centrally instead, so the natural
153
+ # wording passes without per-file edits. It is distinct from:
154
+ # - SKIP_DIRS (path-level, whole-directory)
155
+ # - FILE_SKIP_MARKER (file-level opt-out)
156
+ # - LINE_IGNORE_MARKER (per-line opt-out)
157
+ # Every entry carries a mandatory `reason` so the allowlist stays
158
+ # auditable and a future reader can tell why a class is exempt.
159
+ ALLOWLIST_PATTERNS: List[AllowlistPattern] = [
160
+ AllowlistPattern(
161
+ re.compile(r"^(?:manual|assisted|automated)$"),
162
+ "execution-type enum value (runtime-safety frontmatter), e.g. a "
163
+ "`manual` skill — not a skill/rule id (dc84ed01)",
164
+ ),
165
+ AllowlistPattern(
166
+ re.compile(r"^pack-[\w-]+$"),
167
+ "pack / workspace identifier, e.g. `pack-ai-video` skills — not a "
168
+ "skill/rule id (bd02ef0b)",
169
+ ),
170
+ AllowlistPattern(
171
+ re.compile(r"^(?:skill|rule|command|guideline|persona|context|pack|workspace)$"),
172
+ "bare meta-qualifier keyword used in prose (the `command` vs "
173
+ "`skill` distinction, etc.) — not an artifact id",
174
+ ),
175
+ ]
176
+
177
+
178
+ def _is_allowlisted(name: str) -> bool:
179
+ """True when `name` matches a known non-reference token class."""
180
+ return any(entry.pattern.match(name) for entry in ALLOWLIST_PATTERNS)
181
+
182
+
136
183
  def collect_artifacts(root: Path) -> dict[str, set[str]]:
137
184
  """Build lookup sets for skills, rules, commands, guidelines, personas."""
138
185
  arts: dict[str, set[str]] = {
@@ -345,7 +392,8 @@ def check_file(filepath: Path, artifacts: dict[str, set[str]], root: Path) -> Li
345
392
  # Skill name references
346
393
  for m in SKILL_REF_PATTERN.finditer(line):
347
394
  name = m.group(1)
348
- if name not in artifacts["skills"] and name not in _SKIP_NAMES:
395
+ if name not in artifacts["skills"] and name not in _SKIP_NAMES \
396
+ and not _is_allowlisted(name):
349
397
  broken.append(BrokenRef(
350
398
  file=str(filepath), line=i, ref=name,
351
399
  ref_type="skill", severity="warning",
@@ -355,7 +403,8 @@ def check_file(filepath: Path, artifacts: dict[str, set[str]], root: Path) -> Li
355
403
  # Rule name references
356
404
  for m in RULE_REF_PATTERN.finditer(line):
357
405
  name = m.group(1)
358
- if name not in artifacts["rules"] and name not in _SKIP_NAMES:
406
+ if name not in artifacts["rules"] and name not in _SKIP_NAMES \
407
+ and not _is_allowlisted(name):
359
408
  broken.append(BrokenRef(
360
409
  file=str(filepath), line=i, ref=name,
361
410
  ref_type="rule", severity="warning",
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env python3
2
+ """Skill-composition graph gate (roadmap 3.4).
3
+
4
+ Validates the `requires_skills:` frontmatter field (the skill→skill
5
+ composition graph) against two invariants:
6
+
7
+ 1. **Referential integrity** — every `requires_skills` target names a real
8
+ skill in the suite.
9
+ 2. **Co-availability** — whenever a parent skill ships, every sub-skill its
10
+ body assumes must ship too. A sub-skill is co-available under a parent's
11
+ pack `P` iff one of the sub-skill's packs is in `{P}` ∪ the transitive
12
+ `requires_hint` closure of `P` (from `config/discovery/packs.yml`), or
13
+ the sub-skill is always-on (no pack). A parent with no pack (always-on)
14
+ may only require always-on sub-skills.
15
+
16
+ This is distinct from the ADR-015 artefact→pack `requires` field; this gate
17
+ operates on `requires_skills` (skill→skill) only.
18
+
19
+ Exit 0 = clean · 1 = at least one violation.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
27
+ sys.path.insert(0, str(Path(__file__).resolve().parent / "_lib"))
28
+
29
+ import yaml # noqa: E402
30
+
31
+ from _lib.agent_src import ROOT, iter_artefacts # noqa: E402
32
+ from validate_frontmatter import parse_frontmatter # noqa: E402
33
+
34
+ PACKS_YML = ROOT / "config" / "discovery" / "packs.yml"
35
+
36
+
37
+ def _load_pack_closure() -> dict[str, set[str]]:
38
+ """pack_id → transitive set of {self} ∪ requires_hint closure."""
39
+ raw = yaml.safe_load(PACKS_YML.read_text(encoding="utf-8")) or []
40
+ direct: dict[str, set[str]] = {}
41
+ for entry in raw:
42
+ pid = entry["id"]
43
+ direct[pid] = set(entry.get("requires_hint") or [])
44
+
45
+ closure: dict[str, set[str]] = {}
46
+
47
+ def resolve(pid: str, seen: set[str]) -> set[str]:
48
+ if pid in closure:
49
+ return closure[pid]
50
+ acc = {pid}
51
+ for dep in direct.get(pid, set()):
52
+ if dep in seen:
53
+ continue
54
+ acc |= resolve(dep, seen | {pid})
55
+ closure[pid] = acc
56
+ return acc
57
+
58
+ for pid in direct:
59
+ resolve(pid, set())
60
+ return closure
61
+
62
+
63
+ def _collect_skills() -> dict[str, dict]:
64
+ """skill_id (directory name) → {packs: set[str], requires_skills: list[str], path}."""
65
+ skills: dict[str, dict] = {}
66
+ for path in iter_artefacts("SKILL.md"):
67
+ # logical id = the skill's directory name
68
+ skill_id = path.parent.name
69
+ fm, _ = parse_frontmatter(path.read_text(encoding="utf-8"))
70
+ if fm is None:
71
+ continue
72
+ skills[skill_id] = {
73
+ "packs": set(fm.get("packs") or []),
74
+ "requires_skills": list(fm.get("requires_skills") or []),
75
+ "path": path.relative_to(ROOT).as_posix(),
76
+ }
77
+ return skills
78
+
79
+
80
+ def main() -> int:
81
+ closure = _load_pack_closure()
82
+ skills = _collect_skills()
83
+ errors: list[str] = []
84
+
85
+ for skill_id, info in sorted(skills.items()):
86
+ reqs = info["requires_skills"]
87
+ if not reqs:
88
+ continue
89
+ parent_packs: set[str] = info["packs"]
90
+ for req in reqs:
91
+ target = skills.get(req)
92
+ # (1) referential integrity
93
+ if target is None:
94
+ errors.append(
95
+ f"{info['path']}: requires_skills → unknown skill '{req}' "
96
+ f"(no skills/{req}/SKILL.md in the suite)."
97
+ )
98
+ continue
99
+ # (2) co-availability
100
+ req_packs: set[str] = target["packs"]
101
+ if not req_packs:
102
+ # always-on sub-skill is reachable from anywhere
103
+ continue
104
+ if not parent_packs:
105
+ # always-on parent may only require an always-on sub-skill
106
+ errors.append(
107
+ f"{info['path']}: always-on skill '{skill_id}' requires "
108
+ f"'{req}' which is pack-gated ({sorted(req_packs)}); a base "
109
+ f"install would ship '{skill_id}' without '{req}'."
110
+ )
111
+ continue
112
+ for p in sorted(parent_packs):
113
+ reachable = closure.get(p, {p})
114
+ if req_packs & reachable:
115
+ continue
116
+ hint = sorted(req_packs - reachable)
117
+ errors.append(
118
+ f"{info['path']}: skill '{skill_id}' (pack '{p}') requires "
119
+ f"'{req}' (pack {sorted(req_packs)}), but '{p}' does not reach "
120
+ f"it. Add requires_hint: {hint} to pack '{p}' in "
121
+ f"config/discovery/packs.yml, or move '{req}' into a reachable pack."
122
+ )
123
+
124
+ if errors:
125
+ print("❌ check_skill_requires: skill-composition graph has unmet edges:")
126
+ for e in errors:
127
+ print(f" 🔴 {e}")
128
+ print(
129
+ "\nEvery sub-skill a parent's body invokes must ship wherever the "
130
+ "parent ships. Declare the missing pack dependency or co-locate the skill."
131
+ )
132
+ return 1
133
+
134
+ n_edges = sum(len(i["requires_skills"]) for i in skills.values())
135
+ print(
136
+ f"✅ check_skill_requires: {n_edges} composition edge(s) across "
137
+ f"{sum(1 for i in skills.values() if i['requires_skills'])} skill(s) — all sub-skills co-available."
138
+ )
139
+ return 0
140
+
141
+
142
+ if __name__ == "__main__":
143
+ raise SystemExit(main())