@event4u/agent-config 3.0.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/.agent-src/commands/install-via-agent.md +129 -0
  2. package/.agent-src/commands/video/from-script.md +1 -1
  3. package/.agent-src/commands/video.md +1 -1
  4. package/.agent-src/contexts/execution/cheap-question-mechanics.md +81 -0
  5. package/.agent-src/rules/caveman-speak.md +2 -2
  6. package/.agent-src/rules/context-hygiene.md +36 -0
  7. package/.agent-src/rules/engineering-safety-floor.md +102 -0
  8. package/.agent-src/rules/finance-safety-floor.md +114 -0
  9. package/.agent-src/rules/git-history-discipline.md +1 -1
  10. package/.agent-src/rules/no-cheap-questions.md +34 -32
  11. package/.agent-src/rules/provider-lifecycle-discipline.md +4 -4
  12. package/.agent-src/rules/strategy-safety-floor.md +114 -0
  13. package/.agent-src/skills/agents-md-thin-root/SKILL.md +15 -9
  14. package/.agent-src/skills/async-python-patterns/SKILL.md +1 -1
  15. package/.agent-src/skills/project-analysis-node-express/SKILL.md +1 -1
  16. package/.agent-src/skills/readme-reviewer/SKILL.md +52 -3
  17. package/.agent-src/skills/readme-writing/SKILL.md +52 -4
  18. package/.agent-src/skills/readme-writing-package/SKILL.md +48 -5
  19. package/.agent-src/skills/systematic-debugging/SKILL.md +41 -0
  20. package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  21. package/.agent-src/templates/hooks/pre-commit-frontmatter +66 -0
  22. package/.agent-src/templates/hooks/pre-commit-roadmap-progress +78 -39
  23. package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +4 -1
  24. package/.agent-src/templates/scripts/work_engine/orchestration.py +25 -11
  25. package/.claude-plugin/marketplace.json +2 -1
  26. package/AGENTS.md +10 -8
  27. package/CHANGELOG.md +233 -123
  28. package/README.md +165 -553
  29. package/config/agent-settings.template.yml +0 -7
  30. package/config/discovery/packs.yml +20 -0
  31. package/config/discovery/unassigned-artefacts.yml +2 -0
  32. package/config/gitignore-block.txt +19 -3
  33. package/dist/cli/commands/uiServe.js +13 -4
  34. package/dist/cli/commands/uiServe.js.map +1 -1
  35. package/dist/cli/registry.js +2 -0
  36. package/dist/cli/registry.js.map +1 -1
  37. package/dist/discovery/deprecation-report.md +7 -0
  38. package/dist/discovery/discovery-manifest.json +2107 -1409
  39. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  40. package/dist/discovery/discovery-manifest.summary.md +9 -9
  41. package/dist/discovery/orphan-report.md +10 -0
  42. package/dist/discovery/packs.json +1002 -0
  43. package/dist/discovery/trust-report.md +26 -0
  44. package/dist/discovery/workspaces.json +705 -0
  45. package/dist/mcp/registry-manifest.json +4 -4
  46. package/dist/router.json +1623 -0
  47. package/dist/server/app.js +11 -3
  48. package/dist/server/app.js.map +1 -1
  49. package/dist/server/io/atomicMultiWrite.js +3 -1
  50. package/dist/server/io/atomicMultiWrite.js.map +1 -1
  51. package/dist/server/io/yamlIO.js +22 -0
  52. package/dist/server/io/yamlIO.js.map +1 -1
  53. package/dist/server/routes/ping.js +8 -0
  54. package/dist/server/routes/ping.js.map +1 -1
  55. package/dist/server/routes/schema.js +2 -2
  56. package/dist/server/routes/schema.js.map +1 -1
  57. package/dist/server/routes/settings.js +104 -23
  58. package/dist/server/routes/settings.js.map +1 -1
  59. package/dist/server/routes/userMd.js +37 -27
  60. package/dist/server/routes/userMd.js.map +1 -1
  61. package/dist/server/routes/wizard.js +256 -20
  62. package/dist/server/routes/wizard.js.map +1 -1
  63. package/dist/server/schemas/settings.js +0 -1
  64. package/dist/server/schemas/settings.js.map +1 -1
  65. package/dist/server/token.js +10 -3
  66. package/dist/server/token.js.map +1 -1
  67. package/dist/server/writeRoot.js +28 -11
  68. package/dist/server/writeRoot.js.map +1 -1
  69. package/dist/server/writeRoot.test.js +22 -4
  70. package/dist/server/writeRoot.test.js.map +1 -1
  71. package/dist/shared/userMd/formAdapter.js +29 -51
  72. package/dist/shared/userMd/formAdapter.js.map +1 -1
  73. package/dist/shared/userMd/schema.js +32 -104
  74. package/dist/shared/userMd/schema.js.map +1 -1
  75. package/dist/shared/userMd/utils.js +64 -50
  76. package/dist/shared/userMd/utils.js.map +1 -1
  77. package/dist/ui/assets/index-D-DY1ywI.js +35 -0
  78. package/dist/ui/assets/index-D-DY1ywI.js.map +1 -0
  79. package/dist/ui/index.html +1 -1
  80. package/docs/adrs/router/0001-three-tier-routing.md +5 -5
  81. package/docs/adrs/smoke/0001-per-tier-smoke-scripts.md +1 -1
  82. package/docs/architecture.md +3 -3
  83. package/docs/archive/CHANGELOG-pre-3.1.0.md +167 -0
  84. package/docs/catalog.md +30 -26
  85. package/docs/contracts/CHANGELOG-conventions.md +1 -1
  86. package/docs/contracts/agent-user-schema.md +6 -9
  87. package/docs/contracts/consumer-bridge.md +79 -0
  88. package/docs/contracts/discovery-manifest.md +209 -0
  89. package/docs/contracts/discovery-manifest.schema.json +77 -4
  90. package/docs/contracts/explain-trace.schema.json +1 -1
  91. package/docs/contracts/file-ownership-matrix.json +197 -13
  92. package/docs/contracts/frontmatter-contract.md +140 -0
  93. package/docs/contracts/gui-wizard.md +223 -0
  94. package/docs/contracts/installer-agent-mode.md +137 -0
  95. package/docs/contracts/kernel-membership.md +1 -1
  96. package/docs/contracts/mcp-tool-inventory.md +9 -9
  97. package/docs/contracts/namespace.md +6 -6
  98. package/docs/contracts/provider-lifecycle.md +5 -5
  99. package/docs/contracts/rule-router.md +4 -4
  100. package/docs/contracts/settings-api.md +53 -6
  101. package/docs/contracts/smoke-contracts.md +3 -3
  102. package/docs/contracts/trust-and-safety.md +144 -0
  103. package/docs/customization.md +2 -2
  104. package/docs/decisions/ADR-007-agent-discovery-scopes.md +12 -0
  105. package/docs/decisions/ADR-013-discovery-frontmatter-contract.md +24 -0
  106. package/docs/decisions/ADR-015-discovery-manifest-contract.md +146 -0
  107. package/docs/decisions/ADR-016-installer-architecture.md +189 -0
  108. package/docs/decisions/ADR-017-monorepo-physical-layout.md +261 -0
  109. package/docs/decisions/ADR-018-trust-and-safety-layer.md +159 -0
  110. package/docs/decisions/ADR-019-router-json-dist-location.md +124 -0
  111. package/docs/decisions/ADR-020-global-only-consumer-scope.md +123 -0
  112. package/docs/decisions/ADR-021-deployment-shape.md +153 -0
  113. package/docs/decisions/INDEX.md +7 -0
  114. package/docs/deploy/connector-setup.md +129 -0
  115. package/docs/deploy/env-vars.md +70 -0
  116. package/docs/deploy/policy-cookbook.md +130 -0
  117. package/docs/deploy/quickstart.md +112 -0
  118. package/docs/distribution/public-install-smoke.md +68 -0
  119. package/docs/distribution/registries.md +55 -0
  120. package/docs/distribution/telemetry-privacy.md +128 -0
  121. package/docs/distribution/telemetry-schema.md +174 -0
  122. package/docs/featured-skills.md +95 -0
  123. package/docs/getting-started-by-role.md +19 -1
  124. package/docs/getting-started.md +2 -2
  125. package/docs/guidelines/agent-infra/installed-tools-manifest.md +11 -8
  126. package/docs/guidelines/docs/readme-size-and-splitting.md +53 -1
  127. package/docs/installation.md +27 -14
  128. package/docs/maintainers/dev-mode.md +105 -0
  129. package/docs/setup/per-ide/claude-desktop.md +3 -2
  130. package/docs/wizard.md +39 -4
  131. package/package.json +18 -1
  132. package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
  133. package/scripts/_cli/cmd_doctor.py +150 -2
  134. package/scripts/_cli/cmd_explain.py +2 -1
  135. package/scripts/_cli/cmd_migrate_to_global.py +415 -0
  136. package/scripts/_cli/cmd_settings_migrate.py +146 -0
  137. package/scripts/_cli/explain_last/route.py +2 -1
  138. package/scripts/_dispatch.bash +36 -3
  139. package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
  140. package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
  141. package/scripts/_lib/agent_settings.py +4 -1
  142. package/scripts/_lib/agent_src.py +157 -0
  143. package/scripts/agent-config +17 -6
  144. package/scripts/audit_skill_descriptions.py +18 -6
  145. package/scripts/build_discovery_manifest.py +373 -17
  146. package/scripts/check_artefact_checksums.py +104 -0
  147. package/scripts/check_cluster_patterns.py +20 -4
  148. package/scripts/check_command_count_messaging.py +33 -14
  149. package/scripts/check_council_references.py +43 -4
  150. package/scripts/check_overlay_cascade_subdirs.py +7 -3
  151. package/scripts/check_references.py +5 -2
  152. package/scripts/check_reply_consistency.py +32 -9
  153. package/scripts/check_template_pin_drift.py +24 -7
  154. package/scripts/check_token_optimizer_freshness.py +18 -3
  155. package/scripts/compile_router.py +34 -2
  156. package/scripts/compress.py +162 -44
  157. package/scripts/config/presets.py +19 -1
  158. package/scripts/config/profiles.py +16 -1
  159. package/scripts/discovery_stats.py +70 -0
  160. package/scripts/expected_perms.json +47 -0
  161. package/scripts/generate_index.py +78 -46
  162. package/scripts/generate_ownership_matrix.py +98 -43
  163. package/scripts/generate_pack_manifests.py +183 -0
  164. package/scripts/install +18 -1
  165. package/scripts/install.py +934 -59
  166. package/scripts/install.sh +27 -9
  167. package/scripts/lint_agents_layout.py +93 -13
  168. package/scripts/lint_agents_md.py +1 -1
  169. package/scripts/lint_archived_skills.py +32 -16
  170. package/scripts/lint_bench_corpus.py +14 -2
  171. package/scripts/lint_command_tiers.py +15 -2
  172. package/scripts/lint_featured_skills.py +139 -0
  173. package/scripts/lint_framework_leakage.py +33 -6
  174. package/scripts/lint_global_paths.py +147 -0
  175. package/scripts/lint_orchestration_dsl.py +6 -3
  176. package/scripts/lint_pack_boundaries.py +147 -0
  177. package/scripts/lint_pack_first_win.py +103 -0
  178. package/scripts/lint_readme_jargon.py +131 -0
  179. package/scripts/lint_readme_size.py +33 -0
  180. package/scripts/lint_rule_interactions.py +23 -5
  181. package/scripts/lint_rule_tiers.py +12 -3
  182. package/scripts/lint_trust_coherence.py +212 -0
  183. package/scripts/measure_rule_budget.py +22 -4
  184. package/scripts/move_artefact.py +143 -0
  185. package/scripts/new_skill.py +148 -0
  186. package/scripts/plan_physical_move.py +353 -0
  187. package/scripts/refine_ticket_detect.py +30 -7
  188. package/scripts/release.py +22 -2
  189. package/scripts/schemas/command.schema.json +4 -0
  190. package/scripts/skill_linter.py +248 -118
  191. package/scripts/skill_trigger_eval.py +28 -8
  192. package/scripts/smoke/kernel.sh +1 -1
  193. package/scripts/smoke/router.sh +24 -5
  194. package/scripts/smoke/skills.sh +15 -7
  195. package/scripts/smoke_quickstart.py +11 -2
  196. package/scripts/snapshot_agent_outputs.py +144 -0
  197. package/scripts/update_counts.py +45 -17
  198. package/scripts/validate_decision_engine.py +9 -1
  199. package/scripts/validate_discovery_manifest.py +94 -0
  200. package/scripts/validate_frontmatter.py +39 -20
  201. package/scripts/verify_physical_move.py +185 -0
  202. package/templates/agent-user.md +0 -1
  203. package/templates/agent-user.yml +21 -0
  204. package/templates/minimal/agents-overrides-readme.md +46 -0
  205. package/templates/minimal/overrides-gitkeep +2 -0
  206. package/dist/ui/assets/index-BTRcKDlB.js +0 -39
  207. package/dist/ui/assets/index-BTRcKDlB.js.map +0 -1
  208. package/templates/minimal/agents-gitkeep +0 -2
@@ -0,0 +1,157 @@
1
+ """Locate artefact source roots across the monorepo physical layout.
2
+
3
+ Phase 4 of the monorepo migration (ADR-017) physically moves source
4
+ artefacts out of the flat ``.agent-src.uncompressed/`` directory into
5
+ ``packages/core/.agent-src.uncompressed/`` and
6
+ ``packages/pack-*/.agent-src.uncompressed/`` trees. This helper hides
7
+ that decision from every scanner so they keep working pre-move and
8
+ post-move with the same call shape.
9
+
10
+ Contract:
11
+
12
+ - ``artefact_roots()`` returns every directory that contains source
13
+ ``.md`` artefacts. Pre-move that is ``.agent-src.uncompressed/`` at
14
+ the repo root. Post-move it is every ``packages/*/.agent-src.uncompressed/``.
15
+ Both can coexist during the migration window.
16
+ - ``iter_artefacts()`` yields every source ``.md`` path under those roots.
17
+ - ``logical_relpath(p)`` returns the artefact's stable identity path
18
+ (e.g. ``skills/laravel/SKILL.md``), independent of which physical
19
+ root contains it. This is what manifests, hash maps, and projections
20
+ use as the artefact key.
21
+ - ``strip_source_prefix(p)`` returns the same as ``logical_relpath``
22
+ but accepts repo-relative POSIX strings (used by the compressor's
23
+ output-path computation and the LEGACY_SRC_PREFIX logic).
24
+ """
25
+ from __future__ import annotations
26
+
27
+ from pathlib import Path
28
+ from typing import Iterator
29
+
30
+ ROOT = Path(__file__).resolve().parents[2]
31
+ LEGACY_SRC = ROOT / ".agent-src.uncompressed"
32
+ PACKAGES = ROOT / "packages"
33
+
34
+ # Repo-relative POSIX path prefixes that anchor an artefact source tree.
35
+ # Order: legacy first (kept until the move lands), then packages/*. Each
36
+ # entry is the prefix that gets stripped to obtain the logical path.
37
+ _LEGACY_PREFIX = ".agent-src.uncompressed/"
38
+ _PACKAGE_SUFFIX = "/.agent-src.uncompressed/"
39
+
40
+
41
+ def artefact_roots() -> list[Path]:
42
+ """Every existing directory that contains source ``.md`` artefacts.
43
+
44
+ Returns at most one ``.agent-src.uncompressed/`` root (legacy) plus
45
+ one root per ``packages/*/`` subdirectory that exposes its own
46
+ ``.agent-src.uncompressed/`` tree. Order is stable: legacy first,
47
+ then ``packages/`` entries sorted alphabetically.
48
+ """
49
+ roots: list[Path] = []
50
+ if LEGACY_SRC.exists():
51
+ roots.append(LEGACY_SRC)
52
+ if PACKAGES.exists():
53
+ for pkg in sorted(PACKAGES.iterdir()):
54
+ sub = pkg / ".agent-src.uncompressed"
55
+ if sub.is_dir():
56
+ roots.append(sub)
57
+ return roots
58
+
59
+
60
+ def iter_artefacts(suffix: str = ".md") -> Iterator[Path]:
61
+ """Yield every artefact file under every active source root.
62
+
63
+ Files are returned in deterministic order: roots in the order from
64
+ ``artefact_roots()``, files within each root sorted by path. Symlinks
65
+ and non-files are skipped.
66
+ """
67
+ for root in artefact_roots():
68
+ for p in sorted(root.rglob(f"*{suffix}")):
69
+ if p.is_file():
70
+ yield p
71
+
72
+
73
+ def iter_all_sources() -> Iterator[tuple[Path, str]]:
74
+ """Yield ``(physical_path, logical_relpath)`` for every file under every root.
75
+
76
+ Same deterministic order as :func:`iter_artefacts` but covers *all*
77
+ files (md and non-md) and pre-computes the logical relative path.
78
+ If a logical path appears in multiple roots (legacy + packages/ during
79
+ the move window) the first wins — caller is responsible for ensuring
80
+ that does not happen post-move.
81
+ """
82
+ seen: set[str] = set()
83
+ for root in artefact_roots():
84
+ for p in sorted(root.rglob("*")):
85
+ if not p.is_file():
86
+ continue
87
+ try:
88
+ rel = p.relative_to(root).as_posix()
89
+ except ValueError:
90
+ continue
91
+ if rel in seen:
92
+ continue
93
+ seen.add(rel)
94
+ yield p, rel
95
+
96
+
97
+ def resolve_logical(logical_rel: str) -> Path | None:
98
+ """Return the physical path that backs ``logical_rel``, or ``None``.
99
+
100
+ Walks :func:`artefact_roots` in order and returns the first hit.
101
+ """
102
+ rel = logical_rel.replace("\\", "/").lstrip("/")
103
+ for root in artefact_roots():
104
+ p = root / rel
105
+ if p.exists():
106
+ return p
107
+ return None
108
+
109
+
110
+ def logical_relpath(path: Path) -> str:
111
+ """Return the artefact's logical identity path (POSIX, no prefix).
112
+
113
+ Examples:
114
+ ``.agent-src.uncompressed/skills/laravel/SKILL.md``
115
+ → ``skills/laravel/SKILL.md``
116
+ ``packages/pack-laravel/.agent-src.uncompressed/skills/laravel/SKILL.md``
117
+ → ``skills/laravel/SKILL.md``
118
+ ``packages/core/.agent-src.uncompressed/rules/scope-control.md``
119
+ → ``rules/scope-control.md``
120
+
121
+ Raises ``ValueError`` if ``path`` is not under any known source root.
122
+ """
123
+ p = path.resolve() if path.is_absolute() else (ROOT / path).resolve()
124
+ for root in artefact_roots():
125
+ try:
126
+ return p.relative_to(root.resolve()).as_posix()
127
+ except ValueError:
128
+ continue
129
+ raise ValueError(f"path is not under any artefact root: {path}")
130
+
131
+
132
+ def strip_source_prefix(rel: str) -> str | None:
133
+ """Strip the ``.agent-src.uncompressed/`` anchor from a repo-relative path.
134
+
135
+ Accepts both the legacy flat layout and the monorepo packages layout.
136
+ Returns ``None`` if the path is not under any source root.
137
+
138
+ Examples:
139
+ ``".agent-src.uncompressed/rules/foo.md"`` → ``"rules/foo.md"``
140
+ ``"packages/core/.agent-src.uncompressed/rules/foo.md"`` → ``"rules/foo.md"``
141
+ ``"packages/pack-laravel/.agent-src.uncompressed/skills/x/SKILL.md"``
142
+ → ``"skills/x/SKILL.md"``
143
+ ``"docs/architecture.md"`` → ``None``
144
+ """
145
+ posix = rel.replace("\\", "/")
146
+ if posix.startswith(_LEGACY_PREFIX):
147
+ return posix[len(_LEGACY_PREFIX):]
148
+ if posix.startswith("packages/"):
149
+ idx = posix.find(_PACKAGE_SUFFIX)
150
+ if idx != -1:
151
+ return posix[idx + len(_PACKAGE_SUFFIX):]
152
+ return None
153
+
154
+
155
+ def is_artefact_path(rel: str) -> bool:
156
+ """``True`` if a repo-relative POSIX path sits under any source root."""
157
+ return strip_source_prefix(rel) is not None
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env bash
2
- # Deprecated direct invocation.
2
+ # Maintainer / source-checkout entry point.
3
3
  #
4
- # Public entry point is now `dist/cli/agent-config.js` via
5
- # `npx @event4u/agent-config`. This shim survives one minor cycle
6
- # then is removed (see ADR-012 + roadmap typescript-cli-and-local-gui-foundation).
4
+ # Public consumer entry point is `dist/cli/agent-config.js` via
5
+ # `npx @event4u/agent-config` that's the path documented in README.
6
+ # This shim is the canonical local invocation for maintainers working
7
+ # in a source checkout, where `npx` can't resolve the bin without a
8
+ # prior `npm link` (no `node_modules/.bin/agent-config` symlink).
7
9
  #
8
10
  # Behaviour:
9
11
  # - If dist/cli/agent-config.js exists → forward to the TS binary
@@ -12,8 +14,14 @@
12
14
  # tests, work_engine harness — that pre-date the TS shell).
13
15
  # - Else → error with a one-line build hint
14
16
  #
17
+ # Deprecation warning is only emitted when this script is invoked from
18
+ # an installed npm package (i.e. not a source-repo checkout). Source-
19
+ # checkout detection: `$here/../src/cli/registry.ts` exists. Maintainers
20
+ # running `./agent-config` from the repo root see no warning.
21
+ #
15
22
  # Set AGENT_CONFIG_QUIET_DEPRECATION=1 to suppress the deprecation
16
- # warning (used by test harnesses that pin the stderr surface).
23
+ # warning unconditionally (used by test harnesses that pin the stderr
24
+ # surface).
17
25
  set -euo pipefail
18
26
 
19
27
  # Resolve $0 through symlinks (maintainer repo root has a ./agent-config
@@ -28,8 +36,11 @@ done
28
36
  here="$(cd -- "$(dirname -- "$src")" && pwd)"
29
37
  ts_entry="$here/../dist/cli/agent-config.js"
30
38
  legacy_entry="$here/_dispatch.bash"
39
+ source_marker="$here/../src/cli/registry.ts"
31
40
 
32
- if [[ "${AGENT_CONFIG_QUIET_DEPRECATION:-0}" != "1" ]]; then
41
+ # Suppress the deprecation warning when invoked from a source checkout
42
+ # (maintainer path) — `src/` is not shipped in the published tarball.
43
+ if [[ "${AGENT_CONFIG_QUIET_DEPRECATION:-0}" != "1" && ! -f "$source_marker" ]]; then
33
44
  echo "warning: scripts/agent-config is deprecated; use 'npx @event4u/agent-config' instead" >&2
34
45
  fi
35
46
 
@@ -27,7 +27,10 @@ from dataclasses import asdict, dataclass, field
27
27
  from pathlib import Path
28
28
  from typing import List
29
29
 
30
- DEFAULT_ROOT = Path(".agent-src.uncompressed/skills")
30
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
31
+ from _lib.agent_src import artefact_roots # noqa: E402
32
+
33
+ DEFAULT_ROOT: Path | None = None
31
34
  MIN_LENGTH = 150
32
35
  # Mirrors scripts/skill_linter.py `description_too_long` threshold.
33
36
  MAX_LENGTH = 200
@@ -149,14 +152,23 @@ def render_text(findings: List[Finding], worst_only: bool) -> str:
149
152
 
150
153
  def main() -> int:
151
154
  parser = argparse.ArgumentParser(description=__doc__)
152
- parser.add_argument("--root", type=Path, default=DEFAULT_ROOT)
155
+ parser.add_argument("--root", type=Path, default=None)
153
156
  parser.add_argument("--json", action="store_true", help="emit JSON")
154
157
  parser.add_argument("--full", action="store_true", help="show all flagged, not just top 15")
155
158
  args = parser.parse_args()
156
- if not args.root.exists():
157
- print(f"error: {args.root} does not exist", file=sys.stderr)
158
- return 2
159
- findings = collect_findings(args.root)
159
+ if args.root is not None:
160
+ if not args.root.exists():
161
+ print(f"error: {args.root} does not exist", file=sys.stderr)
162
+ return 2
163
+ roots = [args.root]
164
+ else:
165
+ roots = [r / "skills" for r in artefact_roots() if (r / "skills").is_dir()]
166
+ if not roots:
167
+ print("error: no skills/ directories found across artefact roots", file=sys.stderr)
168
+ return 2
169
+ findings: List[Finding] = []
170
+ for r in roots:
171
+ findings.extend(collect_findings(r))
160
172
  if args.json:
161
173
  print(json.dumps([asdict(f) for f in findings], indent=2))
162
174
  else: