@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
@@ -248,7 +248,7 @@ create_symlink() {
248
248
  local link_dir
249
249
  link_dir="$(dirname "$link_abs")"
250
250
 
251
- mkdir -p "$link_dir"
251
+ $DRY_RUN || mkdir -p "$link_dir"
252
252
 
253
253
  # Remove existing file/symlink
254
254
  if [[ -L "$link_abs" ]] || [[ -f "$link_abs" ]]; then
@@ -336,7 +336,7 @@ sync_hybrid() {
336
336
  local target_dir
337
337
  target_dir="$(dirname "$target_file")"
338
338
 
339
- mkdir -p "$target_dir"
339
+ $DRY_RUN || mkdir -p "$target_dir"
340
340
 
341
341
  if should_copy "$rel_path"; then
342
342
  # Remove existing symlink before copying
@@ -446,7 +446,7 @@ create_tool_symlinks() {
446
446
  local project_root="$1"
447
447
  local rules_dir="$project_root/.augment/rules"
448
448
 
449
- [[ -d "$rules_dir" ]] || return
449
+ [[ -d "$rules_dir" ]] || return 0
450
450
 
451
451
  local -a tool_ids=()
452
452
  local -a tool_dirs=()
@@ -466,7 +466,7 @@ create_tool_symlinks() {
466
466
  local rel_prefix="${rel_prefixes[$i]}"
467
467
  local target_dir="$project_root/$dir"
468
468
 
469
- mkdir -p "$target_dir"
469
+ $DRY_RUN || mkdir -p "$target_dir"
470
470
 
471
471
  for rule_file in "$rules_dir"/*.md; do
472
472
  [[ -f "$rule_file" ]] || continue
@@ -516,11 +516,11 @@ create_skill_symlinks() {
516
516
  local project_root="$1"
517
517
  local skills_dir="$project_root/.augment/skills"
518
518
 
519
- [[ -d "$skills_dir" ]] || return
519
+ [[ -d "$skills_dir" ]] || return 0
520
520
  is_tool_enabled "claude-code" || { log_verbose "skip .claude/skills/ (claude-code not selected)"; return 0; }
521
521
 
522
522
  local target_dir="$project_root/.claude/skills"
523
- mkdir -p "$target_dir"
523
+ $DRY_RUN || mkdir -p "$target_dir"
524
524
 
525
525
  local count=0
526
526
  for skill_dir in "$skills_dir"/*/; do
@@ -569,7 +569,7 @@ generate_windsurfrules() {
569
569
  local project_root="$1"
570
570
  local rules_dir="$project_root/.augment/rules"
571
571
 
572
- [[ -d "$rules_dir" ]] || return
572
+ [[ -d "$rules_dir" ]] || return 0
573
573
  is_tool_enabled "windsurf" || { log_verbose "skip .windsurfrules (windsurf not selected)"; return 0; }
574
574
 
575
575
  local output="$project_root/.windsurfrules"
@@ -629,8 +629,8 @@ copy_if_missing() {
629
629
  local source="$1"
630
630
  local target="$2"
631
631
 
632
- [[ -f "$source" ]] || return
633
- [[ -f "$target" ]] && return
632
+ [[ -f "$source" ]] || return 0
633
+ [[ -f "$target" ]] && return 0
634
634
 
635
635
  if $DRY_RUN; then
636
636
  log_verbose "copy $(basename "$target") (missing)"
@@ -834,8 +834,26 @@ install_cli_wrapper() {
834
834
  }
835
835
 
836
836
  # --- Main ---
837
+ # Phase 6 of monorepo-phase-3-typescript-installer (ADR-016 § Distribution):
838
+ # direct `bash install.sh` invocations are deprecated in favor of the
839
+ # TypeScript installer (`npx @event4u/agent-config init`). The banner only
840
+ # fires when this script is invoked directly — the orchestrator
841
+ # (scripts/install) and the consumer `./agent-config` wrapper set
842
+ # AGENT_CONFIG_FROM_ORCHESTRATOR=1 to suppress the noise. Removal target:
843
+ # the cutover release that flips the npx entry point to the TS installer.
844
+ emit_deprecation_banner() {
845
+ $QUIET && return 0
846
+ [[ "${AGENT_CONFIG_FROM_ORCHESTRATOR:-0}" == "1" ]] && return 0
847
+ [[ "${AGENT_CONFIG_SUPPRESS_DEPRECATION:-0}" == "1" ]] && return 0
848
+ echo " ⚠️ Direct \`bash install.sh\` is deprecated (ADR-016 § Distribution)." >&2
849
+ echo " Prefer: npx @event4u/agent-config init" >&2
850
+ echo " Or: bash scripts/install (orchestrator, suppresses this banner)" >&2
851
+ echo "" >&2
852
+ }
853
+
837
854
  main() {
838
855
  parse_args "$@"
856
+ emit_deprecation_banner
839
857
 
840
858
  # Minimal-init short-circuit (Step 7 Phase 2): skip every payload-sync
841
859
  # stage and only install the project-local `./agent-config` wrapper.
@@ -7,19 +7,28 @@ runtime artefacts, settings, audits, and policies. Flat files at the
7
7
  everything else lives in a typed subdirectory (`runtime/`, `settings/`,
8
8
  `audits/`, `roadmaps/`, `policies/`, `contexts/`, etc.).
9
9
 
10
- Categories:
10
+ Two layout tiers:
11
11
 
12
- ALLOWED — Whitelisted flat files. Linter is silent.
13
- UNKNOWN Anything else. Linter fails.
12
+ MAINTAINER (this source repo, identified by ``.agent-src.uncompressed/``)
13
+ Full `agents/` tree allowed only the flat-file whitelist is
14
+ enforced. Phase 4 of road-to-global-only-install keeps the
15
+ maintainer surface unchanged.
16
+
17
+ CONSUMER (any repo without ``.agent-src.uncompressed/``)
18
+ Global-only target shape: `agents/overrides/` + the bridge
19
+ marker `agents/.event4u-bridge.yml` are the **only** expected
20
+ artefacts. Anything else surfaces as a WARNING with a pointer
21
+ to ``agent-config settings migrate`` (exit code 0). Hard
22
+ violations (unknown flat files at the root) still fail.
14
23
 
15
24
  Exit codes:
16
- 0 — layout is clean.
17
- 1 — at least one UNKNOWN file.
25
+ 0 — layout is clean (warnings ok in consumer mode).
26
+ 1 — at least one UNKNOWN flat-file violation.
18
27
 
19
28
  Invocation (from project root):
20
29
  python3 scripts/lint_agents_layout.py
21
- python3 scripts/lint_agents_layout.py --strict
22
30
  python3 scripts/lint_agents_layout.py --quiet
31
+ python3 scripts/lint_agents_layout.py --strict # warnings → errors
23
32
  """
24
33
 
25
34
  from __future__ import annotations
@@ -40,15 +49,51 @@ ALLOWED_FLAT_FILES: frozenset[str] = frozenset(
40
49
  # D1 anchor / progress dashboard — kept at root by the
41
50
  # roadmap-progress-sync rule so consumers see it first.
42
51
  "roadmaps-progress.md",
43
- # Worked example for the ai-video pipeline. Stays adjacent to
44
- # the agents/reference/ai-video/ dir as a reference template.
45
- ".ai-video.xml.example",
46
52
  # Empty-tree sentinel so agents/ survives a fresh checkout
47
53
  # before any runtime artefact lands.
48
54
  ".gitkeep",
55
+ # Consumer bridge marker (Phase 4 of road-to-global-only-install).
56
+ # Spec: docs/contracts/consumer-bridge.md (event4u-bridge/v1).
57
+ ".event4u-bridge.yml",
49
58
  }
50
59
  )
51
60
 
61
+ # Consumer-target layout: only these top-level entries are expected in
62
+ # the global-only world. Anything else is a WARNING in consumer mode.
63
+ CONSUMER_EXPECTED_ENTRIES: frozenset[str] = frozenset(
64
+ {"overrides", ".event4u-bridge.yml", ".gitkeep"},
65
+ )
66
+
67
+ MIGRATE_HINT = (
68
+ "Run `agent-config settings migrate` (or `npx @event4u/agent-config "
69
+ "migrate-to-global`) to move legacy project-scope artefacts under "
70
+ "`~/.event4u/agent-config/` and leave `agents/overrides/` + "
71
+ "`agents/.event4u-bridge.yml` as the only consumer-side files."
72
+ )
73
+
74
+
75
+ def is_source_repo(project_root: Path) -> bool:
76
+ """True when running inside the agent-config source repo.
77
+
78
+ The maintainer surface is identified by **any** of:
79
+ - ``.agent-src.uncompressed/`` at the workspace root (legacy / single-pack layout),
80
+ - ``packages/<pack>/.agent-src.uncompressed/`` (current monorepo layout — see ``AGENTS.md``),
81
+ - ``.agent-src/`` at the workspace root (compressed authoring tree).
82
+
83
+ Consumer repos ship none of these — they only carry the deployed
84
+ `.augment/`, `.claude/`, etc. plus `agents/overrides/`.
85
+ """
86
+ if (project_root / ".agent-src.uncompressed").is_dir():
87
+ return True
88
+ if (project_root / ".agent-src").is_dir():
89
+ return True
90
+ packages = project_root / "packages"
91
+ if packages.is_dir():
92
+ for sub in packages.iterdir():
93
+ if (sub / ".agent-src.uncompressed").is_dir():
94
+ return True
95
+ return False
96
+
52
97
 
53
98
  def find_violations(root: Path) -> list[str]:
54
99
  """Return UNKNOWN flat-file violations at the agents/ root."""
@@ -73,14 +118,40 @@ def find_violations(root: Path) -> list[str]:
73
118
  return unknown
74
119
 
75
120
 
121
+ def find_consumer_warnings(root: Path) -> list[str]:
122
+ """Return WARNINGs for consumer repos that hold legacy artefacts.
123
+
124
+ Consumer-target shape (Phase 4 of road-to-global-only-install):
125
+ `agents/overrides/` + `agents/.event4u-bridge.yml` are the only
126
+ expected entries. Anything else is a soft warning — the linter
127
+ still exits 0, but the message points the user at the migration
128
+ subcommand so the legacy directory can be reclaimed.
129
+ """
130
+ warnings: list[str] = []
131
+ if not root.is_dir():
132
+ return warnings
133
+
134
+ for path in sorted(root.iterdir()):
135
+ if path.name in CONSUMER_EXPECTED_ENTRIES:
136
+ continue
137
+ # Flat-file UNKNOWNs are already an error — don't double-count.
138
+ if path.is_file() and path.name not in ALLOWED_FLAT_FILES:
139
+ continue
140
+ kind = "dir" if path.is_dir() else "file"
141
+ warnings.append(f"{path} ({kind}): legacy artefact outside the consumer-target shape.")
142
+
143
+ return warnings
144
+
145
+
76
146
  def main() -> int:
77
147
  args = sys.argv[1:]
78
- # --strict kept for backward-compat; no longer affects exit code now
79
- # that the LEGACY tier is gone.
80
- _ = "--strict" in args
148
+ strict = "--strict" in args
81
149
  quiet = "--quiet" in args
82
150
 
151
+ project_root = Path.cwd()
83
152
  unknown = find_violations(AGENTS_ROOT)
153
+ consumer_mode = not is_source_repo(project_root)
154
+ warnings = find_consumer_warnings(AGENTS_ROOT) if consumer_mode else []
84
155
 
85
156
  if unknown:
86
157
  print("❌ agents/ layout violations (unknown flat files):\n")
@@ -94,7 +165,16 @@ def main() -> int:
94
165
  )
95
166
  return 1
96
167
 
97
- if not quiet:
168
+ if warnings:
169
+ if not quiet:
170
+ print("⚠️ agents/ consumer-shape warnings:\n")
171
+ for w in warnings:
172
+ print(f" - {w}")
173
+ print(f"\n{MIGRATE_HINT}")
174
+ if strict:
175
+ return 1
176
+
177
+ if not unknown and not warnings and not quiet:
98
178
  print("✅ agents/ layout clean.")
99
179
  return 0
100
180
 
@@ -65,7 +65,7 @@ class Target:
65
65
  TARGETS = [
66
66
  Target(ROOT / "AGENTS.md", "package-root", 3000, 2800, template=False),
67
67
  Target(
68
- ROOT / ".agent-src.uncompressed" / "templates" / "AGENTS.md",
68
+ ROOT / "packages" / "core" / ".agent-src.uncompressed" / "templates" / "AGENTS.md",
69
69
  "consumer-template", 2500, 2300, template=True,
70
70
  ),
71
71
  ]
@@ -29,8 +29,16 @@ from pathlib import Path
29
29
  QUIET = "--quiet" in sys.argv
30
30
 
31
31
  REPO = Path(__file__).resolve().parents[1]
32
- ARCHIVE_DIR = REPO / "agents" / "archived-skills"
33
- SKILLS_DIR = REPO / ".agent-src.uncompressed" / "skills"
32
+ # Archive notes moved under agents/evidence/ in the privilege-first
33
+ # taxonomy refactor (commit d2ce6748).
34
+ ARCHIVE_DIR = REPO / "agents" / "evidence" / "archived-skills"
35
+
36
+ # Live skill directories live under every artefact root post-monorepo
37
+ # Phase 4 (legacy + packages/*/.agent-src.uncompressed/skills/).
38
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
39
+ from _lib.agent_src import artefact_roots # noqa: E402
40
+
41
+ SKILLS_DIRS = [root / "skills" for root in artefact_roots() if (root / "skills").is_dir()]
34
42
 
35
43
  REQUIRED_FIELDS = ("slug", "archived_on", "last_seen_count", "reason", "replacement", "last_known_callers")
36
44
  VALID_REASONS = frozenset({"unused", "merged", "superseded", "deprecated"})
@@ -57,7 +65,13 @@ def archived_slugs() -> list[Path]:
57
65
 
58
66
 
59
67
  def live_skill_slugs() -> set[str]:
60
- return {p.name for p in SKILLS_DIR.iterdir() if p.is_dir() and (p / "SKILL.md").exists()}
68
+ slugs: set[str] = set()
69
+ for skills_dir in SKILLS_DIRS:
70
+ slugs.update(
71
+ p.name for p in skills_dir.iterdir()
72
+ if p.is_dir() and (p / "SKILL.md").exists()
73
+ )
74
+ return slugs
61
75
 
62
76
 
63
77
  def main() -> int:
@@ -100,15 +114,16 @@ def main() -> int:
100
114
 
101
115
  replacement = fm["replacement"]
102
116
  reason = fm["reason"]
117
+ skills_label = ", ".join(str(d) for d in SKILLS_DIRS) or "<no skills root>"
103
118
  if reason in {"merged", "superseded"}:
104
119
  if replacement == "none" or not replacement:
105
120
  errors.append(f"{note.name}: reason={reason} requires a replacement slug, got 'none'")
106
121
  elif replacement not in live:
107
- errors.append(f"{note.name}: replacement '{replacement}' not found under {SKILLS_DIR}")
122
+ errors.append(f"{note.name}: replacement '{replacement}' not found under {skills_label}")
108
123
  elif reason in {"unused", "deprecated"}:
109
124
  if replacement not in {"none", ""}:
110
125
  if replacement not in live:
111
- errors.append(f"{note.name}: replacement '{replacement}' not found under {SKILLS_DIR}")
126
+ errors.append(f"{note.name}: replacement '{replacement}' not found under {skills_label}")
112
127
 
113
128
  if fm["slug"] in live:
114
129
  errors.append(f"{note.name}: slug '{fm['slug']}' still has a live SKILL.md (zombie)")
@@ -116,17 +131,18 @@ def main() -> int:
116
131
  archived_keys.add(fm["slug"])
117
132
 
118
133
  # Cross-check: live skills must not list an archived slug as replaced_by.
119
- for skill_dir in sorted(SKILLS_DIR.iterdir()):
120
- skill_md = skill_dir / "SKILL.md"
121
- if not skill_md.exists():
122
- continue
123
- text = skill_md.read_text(encoding="utf-8")
124
- fm = parse_frontmatter(text)
125
- if fm is None:
126
- continue
127
- rb = fm.get("replaced_by", "").strip()
128
- if rb and rb in archived_keys:
129
- errors.append(f"{skill_dir.name}/SKILL.md: replaced_by '{rb}' points at an archived slug")
134
+ for skills_dir in SKILLS_DIRS:
135
+ for skill_dir in sorted(skills_dir.iterdir()):
136
+ skill_md = skill_dir / "SKILL.md"
137
+ if not skill_md.exists():
138
+ continue
139
+ text = skill_md.read_text(encoding="utf-8")
140
+ fm = parse_frontmatter(text)
141
+ if fm is None:
142
+ continue
143
+ rb = fm.get("replaced_by", "").strip()
144
+ if rb and rb in archived_keys:
145
+ errors.append(f"{skill_dir.name}/SKILL.md: replaced_by '{rb}' points at an archived slug")
130
146
 
131
147
  if errors:
132
148
  print(f"❌ lint_archived_skills: {len(errors)} violation(s) across {len(notes)} note(s)", file=sys.stderr)
@@ -38,7 +38,13 @@ REQUIRE_FULL = "--require-full" in sys.argv
38
38
 
39
39
  REPO = Path(__file__).resolve().parents[1]
40
40
  CORPUS_DIR = REPO / "tests" / "eval"
41
- SKILLS_DIR = REPO / ".agent-src.uncompressed" / "skills"
41
+
42
+ # Live skill directories live under every artefact root post-monorepo
43
+ # Phase 4 (legacy + packages/*/.agent-src.uncompressed/skills/).
44
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
45
+ from _lib.agent_src import artefact_roots # noqa: E402
46
+
47
+ SKILLS_DIRS = [root / "skills" for root in artefact_roots() if (root / "skills").is_dir()]
42
48
 
43
49
  VALID_CATEGORIES = frozenset({"canonical", "ambiguous", "destructive", "long-context"})
44
50
  # Non-dev corpus (pre-spec) uses legacy categories — accept them so the
@@ -51,7 +57,13 @@ FULL_COUNTS = {"canonical": 10, "ambiguous": 8, "destructive": 5, "long-context"
51
57
 
52
58
 
53
59
  def live_skills() -> set[str]:
54
- return {p.name for p in SKILLS_DIR.iterdir() if p.is_dir() and (p / "SKILL.md").exists()}
60
+ slugs: set[str] = set()
61
+ for skills_dir in SKILLS_DIRS:
62
+ slugs.update(
63
+ p.name for p in skills_dir.iterdir()
64
+ if p.is_dir() and (p / "SKILL.md").exists()
65
+ )
66
+ return slugs
55
67
 
56
68
 
57
69
  def lint_corpus(path: Path, skills: set[str]) -> list[str]:
@@ -19,7 +19,12 @@ from pathlib import Path
19
19
  QUIET = "--quiet" in sys.argv
20
20
 
21
21
  REPO = Path(__file__).resolve().parents[1]
22
- COMMANDS_DIR = REPO / ".agent-src.uncompressed" / "commands"
22
+
23
+ # Commands live under every artefact root post-monorepo Phase 4.
24
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
25
+ from _lib.agent_src import artefact_roots # noqa: E402
26
+
27
+ COMMANDS_DIRS = [root / "commands" for root in artefact_roots() if (root / "commands").is_dir()]
23
28
  # Consumer-facing projection — must also carry tier so .augment/commands/
24
29
  # (which symlinks to .agent-src/commands/) renders the tier filter.
25
30
  COMMANDS_DIR_COMPRESSED = REPO / ".agent-src" / "commands"
@@ -102,7 +107,15 @@ def lint(commands_dir: Path, *, quiet: bool = False) -> int:
102
107
 
103
108
 
104
109
  def main() -> int:
105
- rc = lint(COMMANDS_DIR, quiet=QUIET)
110
+ if not COMMANDS_DIRS:
111
+ print(
112
+ "lint_command_tiers: no commands dir found under any artefact root",
113
+ file=sys.stderr,
114
+ )
115
+ return 1
116
+ rc = 0
117
+ for commands_dir in COMMANDS_DIRS:
118
+ rc |= lint(commands_dir, quiet=QUIET)
106
119
  # The compressed projection is the consumer-facing tree (via the
107
120
  # .augment/commands → .agent-src/commands symlink). It must also
108
121
  # carry tier so the surface stays uniform.
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env python3
2
+ """CI guard for docs/featured-skills.md entry validity.
3
+
4
+ Every artefact referenced from the three Featured tables (Founders &
5
+ Consultants, Content Creators, Engineering Leads) MUST resolve in
6
+ `dist/discovery/discovery-manifest.json`. Stale entries (renamed or
7
+ removed skills / commands) fail the build.
8
+
9
+ Detection:
10
+
11
+ - Scan `docs/featured-skills.md` for inline links of shape
12
+ `[`<token>`](../.agent-src/{skills|commands}/<path>.md)` inside the
13
+ Featured tables. Strip the `/` prefix on commands and the leading
14
+ slash on skill names.
15
+ - Cross-check each token against the manifest's `artefacts` array
16
+ (`category` in {`skill`, `command`} + `name` match, namespaced
17
+ commands like `video/from-script` → `video:from-script`).
18
+ - Verify `--pack <slug>` install hints reference packs that exist in
19
+ `manifest.packs[].id`.
20
+
21
+ Exit codes:
22
+ 0 — every entry resolves; install-pack hints are valid.
23
+ 1 — at least one stale entry or unknown pack.
24
+
25
+ Invocation:
26
+ python3 scripts/lint_featured_skills.py
27
+ python3 scripts/lint_featured_skills.py --quiet
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import json
33
+ import re
34
+ import sys
35
+ from pathlib import Path
36
+
37
+ DOC = Path("docs/featured-skills.md")
38
+ MANIFEST = Path("dist/discovery/discovery-manifest.json")
39
+
40
+ # Matches `[`token`](../.agent-src/skills/<slug>/SKILL.md)` or
41
+ # `[`/token`](../.agent-src/commands/<path>.md)`. Captures (category, slug-path).
42
+ LINK_RE = re.compile(
43
+ r"\[`/?[^`]+`\]\(\.\./\.agent-src/(skills|commands)/([^)]+?)\.md\)"
44
+ )
45
+ PACK_HINT_RE = re.compile(r"--pack\s+([a-z][a-z0-9-]*)")
46
+
47
+
48
+ def load_manifest() -> dict:
49
+ if not MANIFEST.exists():
50
+ print(f"error: manifest not found at {MANIFEST}", file=sys.stderr)
51
+ sys.exit(1)
52
+ return json.loads(MANIFEST.read_text(encoding="utf-8"))
53
+
54
+
55
+ def manifest_names(manifest: dict) -> tuple[set[str], set[str], set[str]]:
56
+ """Return (skill-names, command-names, pack-ids)."""
57
+ skills: set[str] = set()
58
+ commands: set[str] = set()
59
+ for art in manifest.get("artefacts", []):
60
+ cat = art.get("category")
61
+ name = art.get("name")
62
+ if not name:
63
+ continue
64
+ if cat == "skill":
65
+ skills.add(name)
66
+ elif cat == "command":
67
+ commands.add(name)
68
+ packs = {p.get("id") for p in manifest.get("packs", []) if p.get("id")}
69
+ return skills, commands, packs
70
+
71
+
72
+ def slug_from_path(category: str, raw: str) -> str:
73
+ """Convert a doc-link path to the manifest `name` form.
74
+
75
+ skills/<slug>/SKILL → <slug>
76
+ commands/<group>/<leaf> → <group>:<leaf>
77
+ commands/<leaf> → <leaf>
78
+ """
79
+ if category == "skills":
80
+ # raw looks like "<slug>/SKILL"; strip trailing /SKILL if present.
81
+ return raw.split("/", 1)[0]
82
+ # commands
83
+ parts = raw.split("/")
84
+ return ":".join(parts) if len(parts) > 1 else parts[0]
85
+
86
+
87
+ def main() -> int:
88
+ quiet = "--quiet" in sys.argv
89
+ if not DOC.exists():
90
+ print(f"error: {DOC} not found", file=sys.stderr)
91
+ return 1
92
+
93
+ manifest = load_manifest()
94
+ skills, commands, packs = manifest_names(manifest)
95
+ body = DOC.read_text(encoding="utf-8")
96
+
97
+ missing: list[str] = []
98
+ seen: set[tuple[str, str]] = set()
99
+ for cat, raw in LINK_RE.findall(body):
100
+ slug = slug_from_path(cat, raw)
101
+ key = (cat, slug)
102
+ if key in seen:
103
+ continue
104
+ seen.add(key)
105
+ pool = skills if cat == "skills" else commands
106
+ if slug not in pool:
107
+ missing.append(f" - {cat}/{slug} (linked path: ../.agent-src/{cat}/{raw}.md)")
108
+
109
+ unknown_packs: list[str] = []
110
+ for pack in PACK_HINT_RE.findall(body):
111
+ if pack not in packs:
112
+ unknown_packs.append(f" - --pack {pack}")
113
+
114
+ if missing or unknown_packs:
115
+ print(f"FAIL {DOC}: stale Featured Skills entries detected.")
116
+ if missing:
117
+ print("\nMissing artefacts (not in discovery-manifest.json):")
118
+ for line in missing:
119
+ print(line)
120
+ if unknown_packs:
121
+ print("\nUnknown pack ids referenced in install hints:")
122
+ for line in unknown_packs:
123
+ print(line)
124
+ print(
125
+ "\nFix: either restore the artefact, update the doc entry to a "
126
+ "current name, or substitute with the nearest existing artefact."
127
+ )
128
+ return 1
129
+
130
+ if not quiet:
131
+ print(
132
+ f"OK {DOC}: {len(seen)} artefact entries + "
133
+ f"{len(set(PACK_HINT_RE.findall(body)))} pack hints validated."
134
+ )
135
+ return 0
136
+
137
+
138
+ if __name__ == "__main__":
139
+ sys.exit(main())
@@ -33,11 +33,30 @@ from pathlib import Path
33
33
  from typing import Iterable
34
34
 
35
35
  REPO_ROOT = Path(__file__).resolve().parent.parent
36
- DEFAULT_PATHS = (
37
- ".agent-src.uncompressed/skills",
38
- ".agent-src.uncompressed/rules",
39
- ".agent-src.uncompressed/commands",
40
- )
36
+ sys.path.insert(0, str(REPO_ROOT / "scripts"))
37
+ from _lib.agent_src import artefact_roots # noqa: E402
38
+
39
+ # Post-ADR-017 source artefacts live under every packages/*/.agent-src.uncompressed/;
40
+ # pre-move the flat .agent-src.uncompressed/ root still wins. The lint walks
41
+ # the three artefact subtrees (skills/, rules/, commands/) under each active root.
42
+ _SUBDIRS = ("skills", "rules", "commands")
43
+
44
+
45
+ def _default_paths() -> tuple[str, ...]:
46
+ out: list[str] = []
47
+ for root in artefact_roots():
48
+ try:
49
+ rel = root.relative_to(REPO_ROOT)
50
+ except ValueError:
51
+ continue
52
+ for sub in _SUBDIRS:
53
+ target = root / sub
54
+ if target.is_dir():
55
+ out.append((rel / sub).as_posix())
56
+ return tuple(out)
57
+
58
+
59
+ DEFAULT_PATHS = _default_paths()
41
60
  ALLOWLIST_FILE = REPO_ROOT / "scripts/lint_framework_leakage_allowlist.json"
42
61
 
43
62
  CARVE_OUT_PATTERNS = [
@@ -185,8 +204,16 @@ def _load_allowlist() -> dict:
185
204
 
186
205
 
187
206
  def _allowlisted(rel_path: str, line_no: int, allowlist: dict) -> bool:
207
+ # Allowlist entries cite paths under the legacy .agent-src.uncompressed/
208
+ # prefix; post-ADR-017 files live under packages/*/.agent-src.uncompressed/.
209
+ # Match either the literal repo-relative path or its logical id.
210
+ from _lib.agent_src import strip_source_prefix # noqa: E402
211
+
212
+ logical = strip_source_prefix(rel_path)
188
213
  for entry in allowlist.get("entries", []):
189
- if entry.get("file") != rel_path:
214
+ entry_file = entry.get("file")
215
+ entry_logical = strip_source_prefix(entry_file) if isinstance(entry_file, str) else None
216
+ if entry_file != rel_path and (logical is None or entry_logical != logical):
190
217
  continue
191
218
  lines = entry.get("lines")
192
219
  if lines == "*":