@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
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env python3
2
+ """Coverage forcing-function — WARN-only (R3 of road-to-test-and-gate-integrity).
3
+
4
+ A lightweight coverage-governance nudge: warn (never block) when a PR adds a
5
+ new gate script without a matching test, so the +0-tests pattern (three
6
+ releases shipped behaviour with no coverage) becomes visible at PR time. This
7
+ is partly a *social* problem — R3 is a nudge, not a cure.
8
+
9
+ Honest framing (per the roadmap critique): R3 IS a lightweight coverage-
10
+ governance layer, NOT pure hardening. It ships WARN-only, is calibrated from
11
+ the REAL Phase 2 backfill experience (not guessed up front), and carries a
12
+ sunset clause. The hard-fail flip is a SEPARATE, later decision gated on
13
+ warn-phase data.
14
+
15
+ ----------------------------------------------------------------------------
16
+ TRIGGER SURFACE — calibrated from Phase 2 evidence + AI council
17
+ (claude-sonnet-4-5 + gpt-4o, analysis, 2026-06-02):
18
+
19
+ WARN when a NEW `scripts/check_*.py` or `scripts/lint_*.py` gate is added
20
+ with no matching new/changed test in the same diff (`tests/**/test_*.py`
21
+ or `tests/**/*_test.py` whose stem matches the gate's stem).
22
+
23
+ What is DELIBERATELY excluded (the council killed these to avoid
24
+ pragma-spam / signal-to-noise collapse — "over-broad triggers train people
25
+ to bypass it"):
26
+ - Edits to EXISTING gate scripts. Phase 2 migrated 4 gates with one-line
27
+ path-constant swaps that legitimately needed NO test; a trigger that
28
+ fired on edits would be pure noise. KNOWN RECALL LIMIT: a behaviour
29
+ change to an existing gate ships untracked in this warn-only phase —
30
+ accepted on purpose; revisit only if the warn-phase data justifies it.
31
+ - New skills with `requires_skills`. Skills are markdown capability docs,
32
+ usually test-less by design → would fire constantly. Dropped for Phase 1
33
+ (council: "no Phase 2 evidence; immediate pragma-spam").
34
+ - Taskfile wiring, docs/`*.md`, `agents/**`, `config/**`, logging/path-
35
+ constant edits.
36
+
37
+ PRAGMA — `# coverage-diff-ignore: <reason>` (reason mandatory) in the new
38
+ gate file suppresses its warning. Because the trigger is NEW files only, the
39
+ pragma lives in the added lines (it IS the diff), so it is naturally
40
+ commit-scoped — it cannot silently silence FUTURE edits to the file (those
41
+ do not trigger). Mirrors the `check-refs` `<!-- ref-ignore -->` allowlist.
42
+
43
+ SUCCESS METRIC + SUNSET (the fail-mode flip is a separate later decision):
44
+ - Track warn-rate and pragma-rate over the next release cycle (this script
45
+ emits `coverage-diff: warned=N suppressed=M` so the data is in CI logs —
46
+ no new telemetry surface).
47
+ - Consider fail-mode only at >= 3 legitimate catches per cycle AND
48
+ pragma-rate < 10%.
49
+ - If precision is poor after one cycle, REMOVE the check rather than
50
+ escalate it. A noisy nudge people route around is worse than none.
51
+ ----------------------------------------------------------------------------
52
+
53
+ Usage:
54
+ python3 scripts/check_test_coverage_diff.py [--base-ref REF] [--files-status "A\tpath" ...]
55
+ Exit code: always 0 (warn-only by contract this phase).
56
+ """
57
+ from __future__ import annotations
58
+
59
+ import argparse
60
+ import re
61
+ import subprocess
62
+ import sys
63
+ from pathlib import Path
64
+
65
+ REPO_ROOT = Path(__file__).resolve().parent.parent
66
+
67
+ GATE_RE = re.compile(r"^scripts/(?:check_|lint_)[A-Za-z0-9_]+\.py$")
68
+ PRAGMA_RE = re.compile(r"#\s*coverage-diff-ignore:\s*(\S.*?)\s*$")
69
+ _PRAGMA_SCAN_LINES = 60
70
+
71
+
72
+ def _is_test_file(path: str) -> bool:
73
+ if not (path.startswith("tests/") and path.endswith(".py")):
74
+ return False
75
+ stem = Path(path).stem
76
+ return stem.startswith("test_") or stem.endswith("_test")
77
+
78
+
79
+ def _test_matches_gate(gate_path: str, test_paths: list[str]) -> bool:
80
+ """A test covers a gate when its stem shares the gate's stem.
81
+
82
+ Accepts naming variance: `tests/test_check_foo.py`, `tests/foo_test.py`,
83
+ `tests/sub/test_foo.py` all match gate `scripts/check_foo.py`.
84
+ """
85
+ gate_stem = Path(gate_path).stem # e.g. check_foo
86
+ short = gate_stem.removeprefix("check_").removeprefix("lint_") # foo
87
+ for t in test_paths:
88
+ tstem = Path(t).stem.removeprefix("test_").removesuffix("_test")
89
+ if gate_stem in Path(t).stem or short and short in tstem:
90
+ return True
91
+ return False
92
+
93
+
94
+ def evaluate(changed, pragma_reason):
95
+ """Pure core (no git, no I/O): classify the changed-file set.
96
+
97
+ `changed` is a list of (status, path) where status is the git
98
+ name-status code ('A' added, 'M' modified, …). `pragma_reason(path)`
99
+ returns the in-file opt-out reason or None. Returns
100
+ `(warnings, suppressed)` where warnings is a list of new gate paths
101
+ with no matching test and no pragma, suppressed is [(path, reason)].
102
+ """
103
+ new_gates = [p for (s, p) in changed if s == "A" and GATE_RE.match(p)]
104
+ test_changes = [p for (_s, p) in changed if _is_test_file(p)]
105
+ warnings: list[str] = []
106
+ suppressed: list[tuple[str, str]] = []
107
+ for gate in new_gates:
108
+ if _test_matches_gate(gate, test_changes):
109
+ continue
110
+ reason = pragma_reason(gate)
111
+ if reason:
112
+ suppressed.append((gate, reason))
113
+ else:
114
+ warnings.append(gate)
115
+ return warnings, suppressed
116
+
117
+
118
+ def _pragma_reason_from_tree(path: str) -> str | None:
119
+ f = REPO_ROOT / path
120
+ try:
121
+ head = f.read_text(encoding="utf-8", errors="ignore").splitlines()[:_PRAGMA_SCAN_LINES]
122
+ except OSError:
123
+ return None
124
+ for line in head:
125
+ m = PRAGMA_RE.search(line)
126
+ if m:
127
+ return m.group(1)
128
+ return None
129
+
130
+
131
+ def _resolve_base_ref(explicit: str | None) -> str:
132
+ if explicit:
133
+ return explicit
134
+ for candidate in ("origin/main", "origin/master", "main", "master"):
135
+ try:
136
+ subprocess.check_output(
137
+ ["git", "rev-parse", "--verify", candidate], stderr=subprocess.DEVNULL,
138
+ )
139
+ return candidate
140
+ except subprocess.CalledProcessError:
141
+ continue
142
+ return "HEAD~1"
143
+
144
+
145
+ def _git_name_status(base_ref: str) -> list[tuple[str, str]]:
146
+ try:
147
+ out = subprocess.check_output(
148
+ ["git", "diff", "--name-status", f"{base_ref}...HEAD"],
149
+ stderr=subprocess.STDOUT, text=True,
150
+ )
151
+ except subprocess.CalledProcessError as exc:
152
+ print(f"⚠️ coverage-diff: git diff failed ({exc.output.strip()}); skipping.", file=sys.stderr)
153
+ return []
154
+ rows: list[tuple[str, str]] = []
155
+ for line in out.splitlines():
156
+ parts = line.split("\t")
157
+ if len(parts) >= 2:
158
+ rows.append((parts[0][:1], parts[-1]))
159
+ return rows
160
+
161
+
162
+ def main(argv: list[str] | None = None) -> int:
163
+ ap = argparse.ArgumentParser()
164
+ ap.add_argument("--base-ref", default=None)
165
+ opts = ap.parse_args(argv)
166
+ changed = _git_name_status(_resolve_base_ref(opts.base_ref))
167
+ warnings, suppressed = evaluate(changed, _pragma_reason_from_tree)
168
+ if warnings:
169
+ print("⚠️ coverage-diff: new gate(s) added with no matching test (warn-only):")
170
+ for g in warnings:
171
+ print(f" {g} — add tests/test_{Path(g).stem}.py, or a "
172
+ f"`# coverage-diff-ignore: <reason>` line if no test is warranted.")
173
+ for g, reason in suppressed:
174
+ print(f" (suppressed: {g} — {reason})")
175
+ print(f"coverage-diff: warned={len(warnings)} suppressed={len(suppressed)}")
176
+ return 0 # warn-only by contract this phase
177
+
178
+
179
+ if __name__ == "__main__":
180
+ raise SystemExit(main())
@@ -13,6 +13,10 @@ from __future__ import annotations
13
13
  import json
14
14
  import sys
15
15
  from pathlib import Path
16
+ try: # invocation-agnostic import (repo-root-on-path vs scripts-on-path)
17
+ from scripts._lib.agent_settings import project_settings_path
18
+ except ModuleNotFoundError: # pragma: no cover
19
+ from _lib.agent_settings import project_settings_path
16
20
 
17
21
  ROOT = Path(__file__).resolve().parent.parent
18
22
  # ADR-017: rules now live across multiple source roots. Legacy
@@ -20,7 +24,7 @@ ROOT = Path(__file__).resolve().parent.parent
20
24
  # pure-condensed consumer projection.
21
25
  RULES_DIR = ROOT / ".agent-src.uncondensed" / "rules"
22
26
  OUT_PATH = ROOT / "dist" / "router.json"
23
- SETTINGS_PATH = ROOT / ".agent-settings.yml"
27
+ SETTINGS_PATH = project_settings_path(ROOT)
24
28
  SCHEMA_VERSION = 1
25
29
 
26
30
  # Compile-time rule toggles. Maps rule-id → settings predicate.
@@ -27,6 +27,10 @@ import re
27
27
  import shutil
28
28
  import sys
29
29
  from pathlib import Path
30
+ try: # invocation-agnostic import (repo-root-on-path vs scripts-on-path)
31
+ from scripts._lib.agent_settings import project_settings_path
32
+ except ModuleNotFoundError: # pragma: no cover
33
+ from _lib.agent_settings import project_settings_path
30
34
 
31
35
  import yaml
32
36
 
@@ -46,7 +50,7 @@ SOURCE_DIR = PROJECT_ROOT / ".agent-src.uncondensed"
46
50
  TARGET_DIR = PROJECT_ROOT / ".agent-src"
47
51
  AUGMENT_DIR = PROJECT_ROOT / ".augment"
48
52
  HASH_FILE = PROJECT_ROOT / "internal" / ".condensation-hashes.json"
49
- SETTINGS_FILE = PROJECT_ROOT / ".agent-settings.yml"
53
+ SETTINGS_FILE = project_settings_path(PROJECT_ROOT)
50
54
 
51
55
 
52
56
  def _iter_sources():
@@ -415,6 +419,13 @@ COMMANDS_SOURCE = PROJECT_ROOT / ".agent-src" / "commands"
415
419
  PERSONAS_SOURCE = PROJECT_ROOT / ".agent-src" / "personas"
416
420
  USER_TYPES_SOURCE = PROJECT_ROOT / ".agent-src" / "user-types"
417
421
  CLAUDE_SKILLS_DIR = PROJECT_ROOT / ".claude" / "skills"
422
+ # Committed plugin-marketplace projection for command-as-skill entries.
423
+ # The marketplace is consumed as a git repo, so every skills[] path must be
424
+ # committed. Real skills resolve to .agent-src/skills/<name> (already
425
+ # committed); commands have no <slug>/SKILL.md shape in source, so their
426
+ # committed projection lives here. .claude/skills/ stays a gitignored,
427
+ # generate-tools-rebuilt local auto-discovery channel.
428
+ PLUGIN_SKILLS_DIR = PROJECT_ROOT / ".claude-plugin" / "skills"
418
429
 
419
430
  PERSONA_TOOL_DIRS = {
420
431
  ".claude/personas": "../../.agent-src/personas",
@@ -770,6 +781,17 @@ def _parse_frontmatter(content: str) -> tuple[dict, str]:
770
781
  return meta if isinstance(meta, dict) else {}, body
771
782
 
772
783
 
784
+ def _yaml_scalar(value: str) -> str:
785
+ """Return a YAML-safe single-line scalar for a frontmatter value.
786
+
787
+ Descriptions can contain ``:``, ``#``, or quotes — characters that break
788
+ an unquoted YAML scalar (e.g. ``description: Hard Floor: ...`` is invalid
789
+ YAML). A JSON string is itself a valid YAML double-quoted scalar, so
790
+ ``json.dumps`` gives correct escaping for free.
791
+ """
792
+ return json.dumps(value, ensure_ascii=False)
793
+
794
+
773
795
  def _emit_cursor_mdc(source: Path, target: Path) -> None:
774
796
  """Write a Cursor `.mdc` file with Cursor-shaped frontmatter."""
775
797
  meta, body = _parse_frontmatter(source.read_text())
@@ -777,7 +799,7 @@ def _emit_cursor_mdc(source: Path, target: Path) -> None:
777
799
  always_apply = bool(meta.get("alwaysApply") or meta.get("type") == "always")
778
800
  lines = [
779
801
  "---",
780
- f"description: {description}",
802
+ f"description: {_yaml_scalar(description)}",
781
803
  "globs: ",
782
804
  f"alwaysApply: {'true' if always_apply else 'false'}",
783
805
  "---",
@@ -797,7 +819,7 @@ def _emit_windsurf_rule(source: Path, target: Path) -> None:
797
819
  lines = [
798
820
  "---",
799
821
  f"trigger: {trigger}",
800
- f"description: {description}",
822
+ f"description: {_yaml_scalar(description)}",
801
823
  "globs: ",
802
824
  "---",
803
825
  "",
@@ -1145,6 +1167,70 @@ def generate_claude_commands() -> int:
1145
1167
  return count
1146
1168
 
1147
1169
 
1170
+ def generate_plugin_command_skills() -> int:
1171
+ """Mirror command-as-skill entries into the committed .claude-plugin/skills/.
1172
+
1173
+ The plugin marketplace references each command entry as a <slug>/SKILL.md
1174
+ path that must be committed (git-consumed marketplace). Commands have no
1175
+ such shape in source, so this projects them as symlinks:
1176
+ .claude-plugin/skills/<slug>/SKILL.md → ../../../.agent-src/commands/<rel>.
1177
+
1178
+ Symlink-only by design: the committed .claude/skills/ shape was always
1179
+ symlinks (ADR-034 model-rendered copies are local-only, never committed),
1180
+ so the distributed marketplace behaviour is preserved exactly. The local
1181
+ auto-discovery channel (.claude/skills/, gitignored) keeps model rendering
1182
+ for the dev loop.
1183
+ """
1184
+ if not COMMANDS_SOURCE.exists():
1185
+ return 0
1186
+
1187
+ PLUGIN_SKILLS_DIR.mkdir(parents=True, exist_ok=True)
1188
+
1189
+ skill_names: set[str] = set()
1190
+ if SKILLS_SOURCE.exists():
1191
+ skill_names = {d.name for d in SKILLS_SOURCE.iterdir() if d.is_dir()}
1192
+
1193
+ current_slugs: set[str] = set()
1194
+ count = 0
1195
+ for source_file, slug in _iter_commands():
1196
+ # A real skill of the same name takes priority — skip the command.
1197
+ if slug in skill_names:
1198
+ continue
1199
+ current_slugs.add(slug)
1200
+
1201
+ skill_dir = PLUGIN_SKILLS_DIR / slug
1202
+ skill_dir.mkdir(parents=True, exist_ok=True)
1203
+ skill_file = skill_dir / "SKILL.md"
1204
+ if skill_file.exists() or skill_file.is_symlink():
1205
+ skill_file.unlink()
1206
+
1207
+ rel_path = source_file.relative_to(COMMANDS_SOURCE)
1208
+ rel_target = Path("../../../.agent-src/commands") / rel_path
1209
+ skill_file.symlink_to(rel_target)
1210
+ count += 1
1211
+
1212
+ # Reap stale command dirs from removed commands (exactly one SKILL.md).
1213
+ removed_dirs = 0
1214
+ for item in PLUGIN_SKILLS_DIR.iterdir():
1215
+ if not item.is_dir() or item.is_symlink():
1216
+ continue
1217
+ if item.name in current_slugs:
1218
+ continue
1219
+ skill_md = item / "SKILL.md"
1220
+ if skill_md.is_symlink() or skill_md.is_file():
1221
+ entries = list(item.iterdir())
1222
+ if len(entries) == 1 and entries[0].name == "SKILL.md":
1223
+ skill_md.unlink()
1224
+ item.rmdir()
1225
+ removed_dirs += 1
1226
+
1227
+ msg = f" ✅ Created {count} command entries in .claude-plugin/skills/"
1228
+ if removed_dirs:
1229
+ msg += f" ({removed_dirs} stale dirs removed)"
1230
+ info(msg)
1231
+ return count
1232
+
1233
+
1148
1234
  def generate_persona_symlinks() -> int:
1149
1235
  """Create symlink directories for personas (.claude/personas/, .cursor/personas/).
1150
1236
 
@@ -1298,6 +1384,7 @@ def generate_tools() -> None:
1298
1384
  generate_gemini_md()
1299
1385
  skills = generate_claude_skills() if _tool_active("claude-code") else 0
1300
1386
  commands = generate_claude_commands() if _tool_active("claude-code") else 0
1387
+ plugin_cmd_skills = generate_plugin_command_skills() if _tool_active("claude-code") else 0
1301
1388
  plugin_hooks = generate_plugin_hooks() if _tool_active("claude-code") else 0
1302
1389
  personas = generate_persona_symlinks()
1303
1390
  user_types = generate_user_type_symlinks()
@@ -1307,7 +1394,8 @@ def generate_tools() -> None:
1307
1394
  windsurf_wf = generate_windsurf_workflows() if _tool_active("windsurf") else 0
1308
1395
  summary = (
1309
1396
  f"✅ generate-tools — rules={rules} skills={skills} "
1310
- f"commands={commands} plugin_hooks={plugin_hooks} "
1397
+ f"commands={commands} plugin_cmd_skills={plugin_cmd_skills} "
1398
+ f"plugin_hooks={plugin_hooks} "
1311
1399
  f"personas={personas} user_types={user_types} "
1312
1400
  f"cursor_mdc={cursor_mdc} windsurf_rules={windsurf_modern} "
1313
1401
  f"cursor_commands={cursor_cmds} windsurf_workflows={windsurf_wf} "