bmad-module-skill-forge 2.0.0 → 2.0.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 (235) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.nvmrc +1 -1
  3. package/docs/_data/pinned.yaml +1 -1
  4. package/package.json +2 -2
  5. package/src/knowledge/skill-lifecycle.md +1 -1
  6. package/src/module-help.csv +2 -2
  7. package/src/shared/scripts/skf-comention-pairs.py +350 -0
  8. package/src/shared/scripts/skf-count-tokens.py +222 -0
  9. package/src/shared/scripts/skf-detect-docs.py +232 -1
  10. package/src/shared/scripts/skf-detect-language.py +77 -0
  11. package/src/shared/scripts/skf-detect-tools.py +105 -2
  12. package/src/shared/scripts/skf-enumerate-stack-skills.py +111 -2
  13. package/src/shared/scripts/skf-find-cycles.py +246 -0
  14. package/src/shared/scripts/skf-hash-content.py +487 -2
  15. package/src/shared/scripts/skf-manifest-ops.py +67 -1
  16. package/src/shared/scripts/skf-rebuild-managed-sections.py +180 -0
  17. package/src/shared/scripts/skf-recommend-scope-type.py +1 -2
  18. package/src/shared/scripts/skf-render-metadata-stats.py +499 -0
  19. package/src/shared/scripts/skf-rewrite-skill-name.py +308 -0
  20. package/src/shared/scripts/skf-shard-body.py +461 -0
  21. package/src/shared/scripts/skf-skill-inventory.py +139 -3
  22. package/src/shared/scripts/skf-structural-diff.py +307 -67
  23. package/src/shared/scripts/skf-validate-brief-schema.py +39 -1
  24. package/src/shared/scripts/skf-validate-feasibility-report.py +234 -0
  25. package/src/shared/scripts/skf-validate-frontmatter.py +17 -12
  26. package/src/shared/scripts/skf-validate-output.py +443 -10
  27. package/src/shared/scripts/skf-validate-pins.py +1 -1
  28. package/src/shared/scripts/skf-verify-no-trace.py +214 -0
  29. package/src/shared/scripts/skf-verify-provenance-completeness.py +439 -0
  30. package/src/skf-analyze-source/SKILL.md +17 -32
  31. package/src/skf-analyze-source/customize.toml +2 -0
  32. package/src/skf-analyze-source/references/auto-docs-only.md +116 -0
  33. package/src/skf-analyze-source/references/continue.md +8 -13
  34. package/src/skf-analyze-source/references/generate-briefs.md +28 -12
  35. package/src/skf-analyze-source/references/headless-contract.md +33 -0
  36. package/src/skf-analyze-source/references/health-check.md +1 -2
  37. package/src/skf-analyze-source/references/identify-units.md +25 -36
  38. package/src/skf-analyze-source/references/init.md +12 -25
  39. package/src/skf-analyze-source/references/map-and-detect.md +8 -20
  40. package/src/skf-analyze-source/references/recommend.md +4 -14
  41. package/src/skf-analyze-source/references/scan-project.md +3 -11
  42. package/src/skf-analyze-source/references/step-auto-scope.md +42 -180
  43. package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -2
  44. package/src/skf-audit-skill/SKILL.md +9 -6
  45. package/src/skf-audit-skill/customize.toml +2 -0
  46. package/src/skf-audit-skill/references/init.md +58 -32
  47. package/src/skf-audit-skill/references/re-index.md +15 -34
  48. package/src/skf-audit-skill/references/report.md +11 -58
  49. package/src/skf-audit-skill/references/semantic-diff.md +4 -22
  50. package/src/skf-audit-skill/references/severity-classify.md +53 -87
  51. package/src/skf-audit-skill/references/step-doc-drift.md +49 -19
  52. package/src/skf-audit-skill/references/structural-diff.md +43 -68
  53. package/src/skf-brief-skill/SKILL.md +7 -36
  54. package/src/skf-brief-skill/assets/scope-templates.md +1 -1
  55. package/src/skf-brief-skill/customize.toml +14 -0
  56. package/src/skf-brief-skill/references/analyze-target.md +6 -14
  57. package/src/skf-brief-skill/references/confirm-brief.md +2 -14
  58. package/src/skf-brief-skill/references/draft-checkpoint.md +8 -8
  59. package/src/skf-brief-skill/references/gather-intent.md +19 -22
  60. package/src/skf-brief-skill/references/headless-args.md +1 -1
  61. package/src/skf-brief-skill/references/health-check.md +1 -22
  62. package/src/skf-brief-skill/references/invocation-contract.md +37 -0
  63. package/src/skf-brief-skill/references/scope-definition.md +4 -14
  64. package/src/skf-brief-skill/references/step-auto-brief.md +1 -5
  65. package/src/skf-brief-skill/references/step-auto-validate.md +1 -5
  66. package/src/skf-brief-skill/references/version-resolution.md +1 -1
  67. package/src/skf-brief-skill/references/write-brief.md +17 -10
  68. package/src/skf-campaign/SKILL.md +15 -84
  69. package/src/skf-campaign/customize.toml +2 -0
  70. package/src/skf-campaign/manifest.yaml +4 -0
  71. package/src/skf-campaign/references/campaign-contracts.md +67 -0
  72. package/src/skf-campaign/references/campaign-directive-spec.md +14 -103
  73. package/src/skf-campaign/references/step-01-setup.md +4 -5
  74. package/src/skf-campaign/references/step-02-strategy.md +8 -3
  75. package/src/skf-campaign/references/step-03-pins.md +1 -1
  76. package/src/skf-campaign/references/step-04-provenance.md +1 -1
  77. package/src/skf-campaign/references/step-05-skill-loop.md +1 -1
  78. package/src/skf-campaign/references/step-06-batch.md +7 -6
  79. package/src/skf-campaign/references/step-11-maintenance.md +2 -2
  80. package/src/skf-campaign/references/step-resume.md +9 -8
  81. package/src/skf-campaign/scripts/campaign-deps.py +10 -1
  82. package/src/skf-campaign/scripts/campaign-render-batch.py +199 -0
  83. package/src/skf-campaign/scripts/campaign-status.py +162 -0
  84. package/src/skf-campaign/templates/campaign-brief-template.yaml +1 -1
  85. package/src/skf-create-skill/SKILL.md +14 -3
  86. package/src/skf-create-skill/assets/compile-assembly-rules.md +17 -38
  87. package/src/skf-create-skill/assets/skill-sections.md +9 -100
  88. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +4 -13
  89. package/src/skf-create-skill/customize.toml +13 -0
  90. package/src/skf-create-skill/references/authoritative-files-protocol.md +7 -7
  91. package/src/skf-create-skill/references/compile.md +34 -47
  92. package/src/skf-create-skill/references/component-extraction.md +8 -35
  93. package/src/skf-create-skill/references/ecosystem-check.md +9 -25
  94. package/src/skf-create-skill/references/enrich.md +4 -17
  95. package/src/skf-create-skill/references/extract.md +14 -29
  96. package/src/skf-create-skill/references/extraction-patterns-tracing.md +1 -1
  97. package/src/skf-create-skill/references/extraction-patterns.md +7 -7
  98. package/src/skf-create-skill/references/generate-artifacts.md +9 -23
  99. package/src/skf-create-skill/references/health-check.md +1 -1
  100. package/src/skf-create-skill/references/load-brief.md +16 -18
  101. package/src/skf-create-skill/references/report.md +12 -15
  102. package/src/skf-create-skill/references/source-resolution-protocols.md +3 -3
  103. package/src/skf-create-skill/references/step-auto-shard.md +52 -25
  104. package/src/skf-create-skill/references/step-doc-rot.md +30 -12
  105. package/src/skf-create-skill/references/step-doc-sources.md +2 -2
  106. package/src/skf-create-skill/references/sub/ccc-discover.md +2 -13
  107. package/src/skf-create-skill/references/sub/fetch-docs.md +8 -50
  108. package/src/skf-create-skill/references/sub/fetch-temporal.md +5 -20
  109. package/src/skf-create-skill/references/tier-degradation-rules.md +6 -8
  110. package/src/skf-create-skill/references/validate.md +59 -28
  111. package/src/skf-create-skill/scripts/scan-doc-rot.py +245 -0
  112. package/src/skf-create-stack-skill/SKILL.md +15 -9
  113. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +1 -1
  114. package/src/skf-create-stack-skill/customize.toml +10 -0
  115. package/src/skf-create-stack-skill/references/compile-stack.md +7 -8
  116. package/src/skf-create-stack-skill/references/compose-mode-rules.md +2 -13
  117. package/src/skf-create-stack-skill/references/detect-integrations.md +49 -54
  118. package/src/skf-create-stack-skill/references/detect-manifests.md +37 -30
  119. package/src/skf-create-stack-skill/references/generate-output.md +24 -75
  120. package/src/skf-create-stack-skill/references/init.md +9 -81
  121. package/src/skf-create-stack-skill/references/manifest-patterns.md +5 -35
  122. package/src/skf-create-stack-skill/references/parallel-extract.md +12 -27
  123. package/src/skf-create-stack-skill/references/rank-and-confirm.md +6 -6
  124. package/src/skf-create-stack-skill/references/report.md +27 -62
  125. package/src/skf-create-stack-skill/references/validate.md +35 -31
  126. package/src/skf-drop-skill/SKILL.md +20 -23
  127. package/src/skf-drop-skill/customize.toml +10 -0
  128. package/src/skf-drop-skill/references/execute.md +51 -31
  129. package/src/skf-drop-skill/references/headless-contract.md +18 -0
  130. package/src/skf-drop-skill/references/health-check.md +1 -13
  131. package/src/skf-drop-skill/references/report.md +42 -10
  132. package/src/skf-drop-skill/references/select.md +88 -48
  133. package/src/skf-drop-skill/scripts/dir-sizes.py +116 -0
  134. package/src/skf-export-skill/SKILL.md +17 -14
  135. package/src/skf-export-skill/assets/managed-section-format.md +2 -5
  136. package/src/skf-export-skill/customize.toml +10 -6
  137. package/src/skf-export-skill/references/generate-snippet.md +21 -35
  138. package/src/skf-export-skill/references/load-skill.md +44 -42
  139. package/src/skf-export-skill/references/multi-skill-mode.md +1 -1
  140. package/src/skf-export-skill/references/orphan-context-detection.md +11 -3
  141. package/src/skf-export-skill/references/package.md +27 -33
  142. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +24 -5
  143. package/src/skf-export-skill/references/result-envelope.md +28 -0
  144. package/src/skf-export-skill/references/summary.md +11 -5
  145. package/src/skf-export-skill/references/token-report.md +42 -24
  146. package/src/skf-export-skill/references/update-context.md +32 -33
  147. package/src/skf-forger/SKILL.md +27 -39
  148. package/src/skf-forger/references/pipeline-mode.md +34 -0
  149. package/src/skf-forger/scripts/parse-pipeline.py +270 -0
  150. package/src/skf-quick-skill/SKILL.md +10 -60
  151. package/src/skf-quick-skill/customize.toml +9 -0
  152. package/src/skf-quick-skill/references/batch-mode.md +6 -4
  153. package/src/skf-quick-skill/references/compile.md +15 -52
  154. package/src/skf-quick-skill/references/ecosystem-check.md +5 -20
  155. package/src/skf-quick-skill/references/finalize.md +12 -6
  156. package/src/skf-quick-skill/references/halt-contract.md +54 -0
  157. package/src/skf-quick-skill/references/health-check.md +3 -4
  158. package/src/skf-quick-skill/references/quick-extract.md +7 -18
  159. package/src/skf-quick-skill/references/registry-resolution.md +1 -23
  160. package/src/skf-quick-skill/references/resolve-target.md +27 -34
  161. package/src/skf-quick-skill/references/write-and-validate.md +10 -25
  162. package/src/skf-refine-architecture/SKILL.md +14 -7
  163. package/src/skf-refine-architecture/customize.toml +9 -0
  164. package/src/skf-refine-architecture/references/compile.md +9 -7
  165. package/src/skf-refine-architecture/references/gap-analysis.md +21 -50
  166. package/src/skf-refine-architecture/references/health-check.md +1 -1
  167. package/src/skf-refine-architecture/references/improvements.md +9 -25
  168. package/src/skf-refine-architecture/references/init.md +9 -16
  169. package/src/skf-refine-architecture/references/issue-detection.md +10 -25
  170. package/src/skf-refine-architecture/references/refinement-rules.md +4 -16
  171. package/src/skf-refine-architecture/references/report.md +13 -10
  172. package/src/skf-rename-skill/SKILL.md +11 -21
  173. package/src/skf-rename-skill/customize.toml +10 -0
  174. package/src/skf-rename-skill/references/execute.md +107 -78
  175. package/src/skf-rename-skill/references/exit-codes.md +12 -0
  176. package/src/skf-rename-skill/references/report.md +16 -8
  177. package/src/skf-rename-skill/references/select.md +69 -42
  178. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -0
  179. package/src/skf-setup/SKILL.md +20 -12
  180. package/src/skf-setup/customize.toml +10 -0
  181. package/src/skf-setup/references/auto-index.md +9 -10
  182. package/src/skf-setup/references/ccc-index.md +14 -15
  183. package/src/skf-setup/references/detect-and-tier.md +6 -8
  184. package/src/skf-setup/references/report.md +22 -31
  185. package/src/skf-setup/references/tier-rules.md +2 -32
  186. package/src/skf-setup/references/write-config.md +9 -10
  187. package/src/skf-test-skill/SKILL.md +10 -8
  188. package/src/skf-test-skill/customize.toml +2 -0
  189. package/src/skf-test-skill/references/coherence-check.md +19 -31
  190. package/src/skf-test-skill/references/coverage-check.md +109 -75
  191. package/src/skf-test-skill/references/detect-mode.md +10 -41
  192. package/src/skf-test-skill/references/external-validators.md +11 -19
  193. package/src/skf-test-skill/references/init.md +38 -20
  194. package/src/skf-test-skill/references/migration-section-rules.md +4 -4
  195. package/src/skf-test-skill/references/report.md +60 -40
  196. package/src/skf-test-skill/references/score.md +32 -64
  197. package/src/skf-test-skill/references/scoring-rules.md +6 -62
  198. package/src/skf-test-skill/references/source-access-protocol.md +6 -6
  199. package/src/skf-test-skill/references/step-hard-gate.md +1 -3
  200. package/src/skf-test-skill/scripts/aggregate-coherence.py +276 -0
  201. package/src/skf-test-skill/scripts/check-metadata-coherence.py +359 -0
  202. package/src/skf-test-skill/scripts/combine-external-scores.py +190 -0
  203. package/src/skf-test-skill/scripts/compute-score.py +131 -1
  204. package/src/skf-test-skill/scripts/reconcile-coverage.py +327 -0
  205. package/src/skf-test-skill/scripts/validate-inventory.py +251 -0
  206. package/src/skf-test-skill/scripts/verify-declared-numerator.py +192 -0
  207. package/src/skf-update-skill/SKILL.md +17 -5
  208. package/src/skf-update-skill/customize.toml +14 -0
  209. package/src/skf-update-skill/references/detect-changes.md +19 -37
  210. package/src/skf-update-skill/references/health-check.md +3 -9
  211. package/src/skf-update-skill/references/init.md +39 -32
  212. package/src/skf-update-skill/references/manual-section-rules.md +1 -2
  213. package/src/skf-update-skill/references/merge-conflict-rules.md +1 -52
  214. package/src/skf-update-skill/references/merge.md +13 -57
  215. package/src/skf-update-skill/references/re-extract.md +12 -24
  216. package/src/skf-update-skill/references/report.md +14 -7
  217. package/src/skf-update-skill/references/validate.md +32 -30
  218. package/src/skf-update-skill/references/write.md +86 -51
  219. package/src/skf-update-skill/scripts/skf-new-file-diff.py +167 -0
  220. package/src/skf-verify-stack/SKILL.md +11 -21
  221. package/src/skf-verify-stack/customize.toml +12 -3
  222. package/src/skf-verify-stack/references/coverage-patterns.md +1 -29
  223. package/src/skf-verify-stack/references/coverage.md +21 -10
  224. package/src/skf-verify-stack/references/exit-codes.md +14 -0
  225. package/src/skf-verify-stack/references/health-check.md +1 -1
  226. package/src/skf-verify-stack/references/init.md +20 -25
  227. package/src/skf-verify-stack/references/integration-verification-rules.md +10 -19
  228. package/src/skf-verify-stack/references/integrations.md +44 -49
  229. package/src/skf-verify-stack/references/report.md +28 -34
  230. package/src/skf-verify-stack/references/requirements.md +3 -3
  231. package/src/skf-verify-stack/references/synthesize.md +36 -39
  232. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +196 -0
  233. package/src/skf-verify-stack/scripts/skf-report-delta.py +256 -0
  234. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +258 -0
  235. package/src/skf-rename-skill/references/rebuild-context.md +0 -110
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.10"
4
+ # ///
5
+ """Deterministic schema validation of the §1 subagent inventory JSON.
6
+
7
+ Structural validator for the SKF test-skill coverage step (coverage-check.md
8
+ §1a "Schema validation" block). test-skill is a quality gate, so it must not
9
+ trust subagent output blindly: before any downstream step consumes the
10
+ documented inventory, the shape is validated. That validation is pure
11
+ structural checking — strip wrapping fences, parse JSON, assert required keys
12
+ and types, assert each export entry is a dict with a non-empty `name` and a
13
+ `kind` drawn from a fixed enum, and assert each cross-check mismatch carries its
14
+ five fields — with exactly one correct pass/fail per input and no
15
+ interpretation of meaning. It is therefore script work, not prompt work; the
16
+ prompt reads this verdict and owns the HALT decision plus the separate
17
+ grep ground-truth spot-check.
18
+
19
+ Distinct from reconcile-coverage.py (coverage numerator arithmetic) and
20
+ compute-score.py (weight tables): this script only validates the *shape* of the
21
+ subagent inventory and, on success, echoes the fence-stripped parsed inventory
22
+ back so the prompt consumes it directly instead of re-parsing the raw response.
23
+
24
+ CLI usage (mirrors reconcile-coverage.py):
25
+ uv run validate-inventory.py '<raw response>' # positional
26
+ uv run validate-inventory.py --json-input '<raw response>' # explicit flag
27
+ echo '<raw response>' | uv run validate-inventory.py --stdin # piped input
28
+
29
+ Input: the subagent's RAW response text (JSON, optionally wrapped in a markdown
30
+ code fence — a leading line of three backticks with an optional language tag and
31
+ a trailing line of three backticks are stripped before parsing).
32
+
33
+ Output (stdout, one object):
34
+ {
35
+ "valid": <bool>, # true only when parse + every schema check pass
36
+ "violations": [<str>, ...], # human-readable failures; empty when valid
37
+ "rejectedCount": <int>, # count of malformed exports[] entries
38
+ "exportsCount": <int>, # len(exports) when parsed; 0 otherwise
39
+ "inventory": {...} | null # fence-stripped parsed inventory when valid; null otherwise
40
+ }
41
+
42
+ Exit codes:
43
+ 0 — inventory valid
44
+ 1 — no input provided (usage error)
45
+ 2 — inventory invalid (parse failure or schema violation; result JSON emitted)
46
+ """
47
+
48
+ from __future__ import annotations
49
+
50
+ import argparse
51
+ import json
52
+ import re
53
+ import sys
54
+
55
+ # Fixed enum for exports[].kind — the constructs SKF documents across languages
56
+ # and skill types: JS/TS (function/class/type/constant/hook/interface/method),
57
+ # Rust public-API items (struct/enum/trait/macro, alongside the shared
58
+ # type/constant/function), and stack-composition scaffolds (adapter). Mirrors the
59
+ # enum documented in coverage-check.md §1a.
60
+ VALID_KINDS = (
61
+ "function",
62
+ "class",
63
+ "type",
64
+ "constant",
65
+ "hook",
66
+ "interface",
67
+ "method",
68
+ "struct",
69
+ "enum",
70
+ "trait",
71
+ "macro",
72
+ "adapter",
73
+ )
74
+
75
+ REQUIRED_MISMATCH_FIELDS = (
76
+ "export",
77
+ "skill_md_line",
78
+ "reference_file",
79
+ "reference_line",
80
+ "issue",
81
+ )
82
+
83
+ _FENCE_OPEN = re.compile(r"^```[A-Za-z0-9_-]*$")
84
+ _FENCE_CLOSE = re.compile(r"^```$")
85
+
86
+
87
+ def strip_fences(text):
88
+ """Remove a wrapping markdown code fence, matching coverage-check.md §1a step 1.
89
+
90
+ When the first non-empty line is three backticks (optionally with a language
91
+ tag) and the last non-empty line is three backticks, drop those two lines.
92
+ Otherwise return the text unchanged.
93
+ """
94
+ lines = text.split("\n")
95
+ non_empty_idx = [i for i, ln in enumerate(lines) if ln.strip()]
96
+ if len(non_empty_idx) < 2:
97
+ return text
98
+ first, last = non_empty_idx[0], non_empty_idx[-1]
99
+ if _FENCE_OPEN.match(lines[first].strip()) and _FENCE_CLOSE.match(lines[last].strip()):
100
+ kept = [ln for i, ln in enumerate(lines) if i != first and i != last]
101
+ return "\n".join(kept)
102
+ return text
103
+
104
+
105
+ def validate_inventory(raw):
106
+ """Pure structural validation of the subagent inventory response.
107
+
108
+ `raw` is the raw response text. Returns the result dict documented in the
109
+ module docstring. Deterministic: identical input yields an identical verdict.
110
+ """
111
+ violations = []
112
+ inner = strip_fences(raw)
113
+ try:
114
+ data = json.loads(inner)
115
+ except json.JSONDecodeError as exc:
116
+ return {
117
+ "valid": False,
118
+ "violations": [f"subagent response not valid JSON: {exc.msg}"],
119
+ "rejectedCount": 0,
120
+ "exportsCount": 0,
121
+ "inventory": None,
122
+ }
123
+
124
+ if not isinstance(data, dict):
125
+ return {
126
+ "valid": False,
127
+ "violations": ["subagent response is not a JSON object"],
128
+ "rejectedCount": 0,
129
+ "exportsCount": 0,
130
+ "inventory": None,
131
+ }
132
+
133
+ exports = data.get("exports")
134
+ if not isinstance(exports, list):
135
+ violations.append(
136
+ "missing/typo: `exports` must be present and a list"
137
+ )
138
+ exports = []
139
+
140
+ cross = data.get("cross_check_mismatches")
141
+ if not isinstance(cross, list):
142
+ violations.append(
143
+ "missing/typo: `cross_check_mismatches` must be present and a list (may be empty)"
144
+ )
145
+ cross = []
146
+
147
+ # Per-entry export validation — count rejections.
148
+ rejected = 0
149
+ for idx, entry in enumerate(exports):
150
+ if not isinstance(entry, dict):
151
+ rejected += 1
152
+ continue
153
+ name = entry.get("name")
154
+ kind = entry.get("kind")
155
+ if not (isinstance(name, str) and name):
156
+ rejected += 1
157
+ continue
158
+ if kind not in VALID_KINDS:
159
+ rejected += 1
160
+ continue
161
+ if rejected > 0:
162
+ subject = "entry does" if rejected == 1 else "entries do"
163
+ violations.append(
164
+ f"{rejected} exports[] {subject} not match schema "
165
+ f"(each needs a non-empty string `name` and a `kind` in "
166
+ f"{{{', '.join(VALID_KINDS)}}})"
167
+ )
168
+
169
+ # Non-empty cross-check mismatch entries must carry all five fields.
170
+ for idx, entry in enumerate(cross):
171
+ if not isinstance(entry, dict):
172
+ violations.append(f"cross_check_mismatches[{idx}] is not an object")
173
+ continue
174
+ missing = [f for f in REQUIRED_MISMATCH_FIELDS if f not in entry]
175
+ if missing:
176
+ violations.append(
177
+ f"cross_check_mismatches[{idx}] missing field(s): {', '.join(missing)}"
178
+ )
179
+
180
+ valid = not violations
181
+ return {
182
+ "valid": valid,
183
+ "violations": violations,
184
+ "rejectedCount": rejected,
185
+ "exportsCount": len(exports),
186
+ "inventory": data if valid else None,
187
+ }
188
+
189
+
190
+ # --- CLI --------------------------------------------------------------------
191
+
192
+
193
+ def _build_parser():
194
+ parser = argparse.ArgumentParser(
195
+ prog="validate-inventory",
196
+ description=(
197
+ "Deterministic schema validation of the §1 subagent inventory JSON "
198
+ "(coverage-check.md §1a). Strips wrapping fences, parses, and asserts "
199
+ "the required-keys/types/enum/mismatch-field contract, returning "
200
+ "{valid, violations, rejectedCount, exportsCount, inventory}."
201
+ ),
202
+ formatter_class=argparse.RawDescriptionHelpFormatter,
203
+ )
204
+ src = parser.add_mutually_exclusive_group()
205
+ src.add_argument(
206
+ "raw_input",
207
+ nargs="?",
208
+ help="Raw subagent response as a positional argument.",
209
+ )
210
+ src.add_argument(
211
+ "--json-input",
212
+ dest="raw_input_flag",
213
+ help="Raw subagent response passed via flag (overrides positional).",
214
+ )
215
+ src.add_argument(
216
+ "--stdin",
217
+ action="store_true",
218
+ help="Read the raw subagent response from stdin.",
219
+ )
220
+ return parser
221
+
222
+
223
+ def _resolve_input(args):
224
+ if args.stdin:
225
+ return sys.stdin.read()
226
+ if args.raw_input_flag is not None:
227
+ return args.raw_input_flag
228
+ if args.raw_input is not None:
229
+ return args.raw_input
230
+ return ""
231
+
232
+
233
+ def main(argv=None):
234
+ parser = _build_parser()
235
+ args = parser.parse_args(argv)
236
+ raw = _resolve_input(args)
237
+ if not raw.strip():
238
+ parser.print_usage(file=sys.stderr)
239
+ print(
240
+ "error: no input provided (positional arg, --json-input, or --stdin)",
241
+ file=sys.stderr,
242
+ )
243
+ return 1
244
+
245
+ result = validate_inventory(raw)
246
+ print(json.dumps(result, indent=2))
247
+ return 0 if result["valid"] else 2
248
+
249
+
250
+ if __name__ == "__main__":
251
+ raise SystemExit(main())
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.10"
4
+ # dependencies = []
5
+ # ///
6
+ """Deterministic numerator ground-truth verifier.
7
+
8
+ Grep-and-count helper for the SKF test-skill workflow (coverage-check.md §4b
9
+ "Numerator ground-truth"). It fires only on the inflation signature
10
+ (`stats.exports_documented == effective_denominator` exactly): the numerator
11
+ equals the denominator, the tell of a documented count padded to force 100%
12
+ coverage. On that signature the declared count is not trusted — every declared
13
+ export name is greped against the skill's documentation surface and only the
14
+ names that actually appear count toward coverage.
15
+
16
+ Distinct from reconcile-coverage.py (whose scalar/stack branches grep the §1
17
+ inventory names and return only a residual missing *count*): this script greps
18
+ the full declared metadata/provenance set and enumerates the *absent* names so
19
+ §4b can list them in a High-severity gap. Both scripts share the same grep
20
+ semantics (SKILL.md ∪ sorted references/*.md, UTF-8, case-sensitive substring
21
+ containment — mirroring `grep "{name}"`) so their numerators agree.
22
+
23
+ Input schema (one JSON object):
24
+ {
25
+ "declaredNames": ["foo", "Bar::baz", ...], # metadata.exports[] / provenance declared set
26
+ "skillPackagePath": "/path/to/skill" # SKILL.md ∪ references/*.md
27
+ }
28
+
29
+ Output (stdout, one object):
30
+ {
31
+ "declared": <int>, # de-duplicated declared name count
32
+ "verified": <int>, # declared names present in the doc surface
33
+ "present": [names], # sorted
34
+ "absent": [names], # sorted (declared − verified) — the gap list
35
+ "inflated": <bool> # verified < declared (numerator was padded)
36
+ }
37
+ or {"error": ..., "code": "INVALID_INPUT"} on a schema violation.
38
+
39
+ `verified` is the numerator §4b uses for Export Coverage (it overrides the
40
+ declared count when `inflated` is true).
41
+
42
+ CLI usage (mirrors reconcile-coverage.py):
43
+ uv run verify-declared-numerator.py '<JSON>' # positional
44
+ uv run verify-declared-numerator.py --json-input '<JSON>' # explicit flag
45
+ cat input.json | uv run verify-declared-numerator.py --stdin # piped input
46
+
47
+ Exit codes:
48
+ 0 — verification emitted successfully
49
+ 1 — no input / input could not be parsed as JSON
50
+ 2 — input parsed but schema/semantics invalid (error object emitted as JSON)
51
+ """
52
+
53
+ from __future__ import annotations
54
+
55
+ import argparse
56
+ import json
57
+ import sys
58
+ from pathlib import Path
59
+
60
+
61
+ def make_error(message):
62
+ return {"error": message, "code": "INVALID_INPUT"}
63
+
64
+
65
+ def load_doc_text(skill_package_path):
66
+ """Concatenate SKILL.md ∪ references/*.md text for name-presence grepping.
67
+
68
+ Deterministic: references are read in sorted order. Reads as UTF-8 so
69
+ non-ASCII exports do not mojibake on Windows (cp1252 default). Mirrors
70
+ reconcile-coverage.py.load_doc_text so the two numerators agree.
71
+ """
72
+ root = Path(skill_package_path)
73
+ parts = []
74
+ skill_md = root / "SKILL.md"
75
+ if skill_md.is_file():
76
+ parts.append(skill_md.read_text(encoding="utf-8"))
77
+ refs_dir = root / "references"
78
+ if refs_dir.is_dir():
79
+ for ref in sorted(refs_dir.glob("*.md")):
80
+ parts.append(ref.read_text(encoding="utf-8"))
81
+ return "\n".join(parts)
82
+
83
+
84
+ def _validate(inp):
85
+ if inp is None or not isinstance(inp, dict):
86
+ return "Input must be a JSON object"
87
+ names = inp.get("declaredNames")
88
+ if not isinstance(names, list) or not names:
89
+ return "declaredNames must be a non-empty list of strings"
90
+ if not inp.get("skillPackagePath"):
91
+ return "skillPackagePath is required to grep SKILL.md ∪ references"
92
+ return None
93
+
94
+
95
+ def verify(inp, doc_text=None):
96
+ """Pure verification. `doc_text` may be injected (tests); else loaded from disk."""
97
+ err = _validate(inp)
98
+ if err:
99
+ return make_error(err)
100
+
101
+ declared_set = sorted({n for n in inp["declaredNames"] if isinstance(n, str) and n})
102
+ if doc_text is None:
103
+ doc_text = load_doc_text(inp["skillPackagePath"])
104
+
105
+ present = sorted(n for n in declared_set if n in doc_text)
106
+ absent = sorted(n for n in declared_set if n not in doc_text)
107
+
108
+ return {
109
+ "declared": len(declared_set),
110
+ "verified": len(present),
111
+ "present": present,
112
+ "absent": absent,
113
+ "inflated": len(present) < len(declared_set),
114
+ }
115
+
116
+
117
+ # --- CLI --------------------------------------------------------------------
118
+
119
+
120
+ def _build_parser():
121
+ parser = argparse.ArgumentParser(
122
+ prog="verify-declared-numerator",
123
+ description=(
124
+ "Deterministic numerator ground-truth verifier (coverage-check.md "
125
+ "§4b). Greps the full declared export set against SKILL.md ∪ "
126
+ "references/*.md and enumerates the present/absent names so a padded "
127
+ "numerator is caught and the verified count replaces it."
128
+ ),
129
+ formatter_class=argparse.RawDescriptionHelpFormatter,
130
+ epilog=(
131
+ "Example:\n"
132
+ " uv run verify-declared-numerator.py "
133
+ "'{\"declaredNames\":[\"foo\",\"bar\"],"
134
+ "\"skillPackagePath\":\"/path/to/skill\"}'"
135
+ ),
136
+ )
137
+ src = parser.add_mutually_exclusive_group()
138
+ src.add_argument(
139
+ "json_input",
140
+ nargs="?",
141
+ help="JSON object as a positional argument (single-quote it on the shell).",
142
+ )
143
+ src.add_argument(
144
+ "--json-input",
145
+ dest="json_input_flag",
146
+ help="JSON object passed via flag (overrides positional).",
147
+ )
148
+ src.add_argument(
149
+ "--stdin",
150
+ action="store_true",
151
+ help="Read the JSON object from stdin.",
152
+ )
153
+ return parser
154
+
155
+
156
+ def _resolve_input(args):
157
+ if args.stdin:
158
+ return sys.stdin.read()
159
+ if args.json_input_flag is not None:
160
+ return args.json_input_flag
161
+ if args.json_input is not None:
162
+ return args.json_input
163
+ return ""
164
+
165
+
166
+ def main(argv=None):
167
+ parser = _build_parser()
168
+ args = parser.parse_args(argv)
169
+ raw = _resolve_input(args)
170
+ if not raw.strip():
171
+ parser.print_usage(file=sys.stderr)
172
+ print(
173
+ "error: no input provided (positional arg, --json-input, or --stdin)",
174
+ file=sys.stderr,
175
+ )
176
+ return 1
177
+
178
+ try:
179
+ data = json.loads(raw)
180
+ except json.JSONDecodeError as exc:
181
+ print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
182
+ return 1
183
+
184
+ result = verify(data)
185
+ print(json.dumps(result, indent=2))
186
+ if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
187
+ return 2
188
+ return 0
189
+
190
+
191
+ if __name__ == "__main__":
192
+ raise SystemExit(main())
@@ -12,6 +12,7 @@ Surgically updates existing skills when source code changes, preserving all [MAN
12
12
  ## Conventions
13
13
 
14
14
  - Bare paths (e.g. `references/<name>.md`) resolve from the skill root.
15
+ - **Module-level path exception:** bare paths beginning with `knowledge/` or `shared/` resolve from the SKF module root (`{project-root}/_bmad/skf/` installed, `src/` in dev), not the skill root — stage files reference `knowledge/version-paths.md` and `knowledge/tool-resolution.md`, and the terminal step chains to `shared/health-check.md`.
15
16
  - `references/` holds prompt content carved out of SKILL.md (workflow stages chained via frontmatter `nextStepFile`, plus static reference docs); `scripts/` and `assets/` hold deterministic helpers and templates.
16
17
  - `{skill-root}` resolves to this skill's installed directory (where `customize.toml` lives, if present).
17
18
  - `{project-root}`-prefixed paths resolve from the project working directory.
@@ -50,11 +51,11 @@ These rules apply to every step in this workflow:
50
51
  | Aspect | Detail |
51
52
  |--------|--------|
52
53
  | **Inputs** | skill_name [required] |
53
- | **Flags** | `--headless` / `-H` (auto-resolve all gates); `--from-test-report` (gap-driven mode); `--allow-workspace-drift` (gap-driven only — bypass §0.a pinning guard); `--detect-only` (run detect-changes only, exit before re-extract; envelope `status="detect-only"`); `--dry-run` (run detect-changes + re-extract, exit before merge/write; envelope `status="dry-run"` describes what would change). If both `--detect-only` and `--dry-run` are passed, `--detect-only` wins. |
54
+ | **Flags** | `--headless` / `-H` (auto-resolve all gates); `--from-test-report` (gap-driven mode); `--allow-workspace-drift` (gap-driven only — bypass §0.a pinning guard); `--allow-degraded` (headless only — pre-authorize the lossy degraded full re-extraction when the provenance map is missing, instead of halting `blocked`; init.md §4); `--detect-only` (run detect-changes only, exit before re-extract; envelope `status="detect-only"`); `--dry-run` (run detect-changes + re-extract, exit before merge/write; envelope `status="dry-run"` describes what would change). If both `--detect-only` and `--dry-run` are passed, `--detect-only` wins. |
54
55
  | **Gates** | step 1: Confirm Gate [C] | step 4: Confirm Gate [C if clean merge, HALT if conflicts] |
55
- | **Outputs** | Updated SKILL.md, updated provenance-map.json, evidence-report.md (none when `--detect-only` or `--dry-run` is set — those modes are read-only inspection paths) |
56
- | **Concurrency** | Two simultaneous real-update runs against the same skill would corrupt provenance. init.md §1b acquires a PID-file lock at `{forge_data_folder}/{skill_name}/.skf-update.lock` before any artifact read; live-PID collisions halt with `status: "halted-for-concurrent-run"`. Stale locks (dead PID) are cleared silently with a warning. The lock is released by the terminal health-check step (step 8) and by every documented halt path. Read-only modes (`--detect-only`, `--dry-run`) skip the lock entirely — they're safe alongside a concurrent real update. |
57
- | **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true. Each auto-resolved gate appends a `{gate, default_action, taken_action, reason, evidence?}` entry to `headless_decisions[]`, surfaced in step 7's `SKF_UPDATE_RESULT_JSON` envelope so non-interactive runs can be audited post-hoc. Pipeline branches on the envelope's top-level `status` field (`success`, `no-changes`, `detect-only`, `dry-run`, or one of the documented `halted-for-*` codes). The first four are successful exits — pipelines treating non-success as failure must include them in the success set. Schema: `src/shared/scripts/schemas/skf-update-result-envelope.v1.json`. |
56
+ | **Outputs** | Updated SKILL.md, metadata.json, provenance-map.json, evidence-report.md (none when `--detect-only` or `--dry-run` is set — those modes are read-only inspection paths) |
57
+ | **Concurrency** | Two simultaneous real-update runs against the same skill would corrupt provenance. init.md §1b acquires a PID-file lock at `{forge_data_folder}/{skill_name}/.skf-update.lock` before any artifact read; live-PID collisions halt with `status: "halted-for-concurrent-run"`. Stale locks (dead PID) are cleared silently with a warning. The lock is released by the terminal health-check step (step 8) on the normal path and by the two init-stage headless halts (§4/§6); mid-workflow halts leave it for the next run's stale-lock self-heal (see init.md §1b Release contract). Read-only modes (`--detect-only`, `--dry-run`) skip the lock entirely — they're safe alongside a concurrent real update. |
58
+ | **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true. Each auto-resolved gate appends a `{gate, default_action, taken_action, reason, evidence?}` entry to `headless_decisions[]`, surfaced in step 7's `SKF_UPDATE_RESULT_JSON` envelope so non-interactive runs can be audited post-hoc. A HALT reached in headless mode emits its own `SKF_UPDATE_RESULT_JSON` at the halt site (the site's `status` code plus an `error: {phase, path?, reason}` object) and exits — halts do not fall through to step 7. Pipeline branches on the envelope's top-level `status` field (`success`, `no-changes`, `detect-only`, `dry-run`, or one of the documented `halted-for-*`/`blocked` codes). The first four are successful exits — pipelines treating non-success as failure must include them in the success set. The status enum is defined once in `src/shared/scripts/schemas/skf-update-result-envelope.v1.json`. |
58
59
 
59
60
  ## On Activation
60
61
 
@@ -64,4 +65,15 @@ These rules apply to every step in this workflow:
64
65
 
65
66
  2. **Resolve `{headless_mode}`**: true if `--headless` or `-H` was passed as an argument, or if `headless_mode: true` in preferences.yaml. Default: false.
66
67
 
67
- 3. Load, read the full file, and then execute `references/init.md` to begin the workflow.
68
+ 3. **Resolve workflow customization.** Run:
69
+
70
+ ```bash
71
+ python3 {project-root}/_bmad/scripts/resolve_customization.py \
72
+ --skill {skill-root} --key workflow
73
+ ```
74
+
75
+ This merges the three layers per `bmad-customize` rules (scalars override, arrays append): `{skill-root}/customize.toml` (bundled defaults), `_bmad/custom/<skill-name>.toml` under `{project-root}` (team overrides), and `_bmad/custom/<skill-name>.user.toml` under `{project-root}` (personal overrides). If the script is missing or fails, read `{skill-root}/customize.toml` directly.
76
+
77
+ Apply the resolved values so no surface is a silent no-op: execute each entry in `workflow.activation_steps_prepend` in order now; treat every entry in `workflow.persistent_facts` as standing context for the whole run (entries prefixed `file:` are paths or globs whose contents load as facts — the bundled default loads any `project-context.md` under `{project-root}`); resolve `{onCompleteCommand}` ← `workflow.on_complete` if non-empty, else empty string, and stash it in workflow context (`references/report.md` §5b invokes it after the result contract is written; empty string = the hook is a no-op). After activation completes, execute each entry in `workflow.activation_steps_append` in order before `init.md` runs.
78
+
79
+ 4. Load, read the full file, and then execute `references/init.md` to begin the workflow.
@@ -1,6 +1,8 @@
1
1
  # DO NOT EDIT -- overwritten on every update.
2
2
  #
3
3
  # Workflow customization surface for skf-update-skill.
4
+ # Team overrides: _bmad/custom/skf-update-skill.toml (under {project-root})
5
+ # Personal overrides: _bmad/custom/skf-update-skill.user.toml (under {project-root})
4
6
 
5
7
  [workflow]
6
8
 
@@ -33,3 +35,15 @@ activation_steps_append = []
33
35
  persistent_facts = [
34
36
  "file:{project-root}/**/project-context.md",
35
37
  ]
38
+
39
+ # Pipeline-integration hook invoked after report.md §5b writes the result
40
+ # contract (the per-run record plus update-skill-result-latest.json). The
41
+ # command is called as:
42
+ # <on_complete> --result-path=<{forge_version}/update-skill-result-latest.json>
43
+ # Useful for Slack notifications, dashboard ingest, CI hooks, or chaining a
44
+ # downstream skill (audit, export, test). Failures are logged to the run's
45
+ # warnings[] but never fail the workflow.
46
+ #
47
+ # Empty string = no-op (default).
48
+
49
+ on_complete = ""
@@ -36,6 +36,10 @@ provenanceGapDispatchProbeOrder:
36
36
  detectScriptsAssetsProbeOrder:
37
37
  - '{project-root}/_bmad/skf/shared/scripts/skf-detect-scripts-assets.py'
38
38
  - '{project-root}/src/shared/scripts/skf-detect-scripts-assets.py'
39
+ # `{newFileDiffHelper}` derives NEW_FILE (inventory source_files not in the
40
+ # provenance map, minus [MANUAL] paths) — the set-difference §Category D would
41
+ # otherwise ask the model to compute by hand. Per-skill helper, always bundled.
42
+ newFileDiffHelper: 'scripts/skf-new-file-diff.py'
39
43
  ---
40
44
 
41
45
  <!-- Config: communicate in {communication_language}. -->
@@ -51,7 +55,7 @@ Compare current source code state against the provenance map to produce a comple
51
55
  - Focus only on detecting and classifying changes — do not extract or merge
52
56
  - Use subprocess Pattern 4 (parallel) when available; if unavailable, compare sequentially
53
57
 
54
- ## MANDATORY SEQUENCE
58
+ ## Steps
55
59
 
56
60
  ### 0. Check for Test Report Input (Gap-Driven Mode)
57
61
 
@@ -105,7 +109,7 @@ Read the source directory at `{source_root}` and build a current file inventory:
105
109
 
106
110
  ### 1b. Discovered Authoritative Files Protocol (Mirror)
107
111
 
108
- **Purpose:** mirror `skf-create-skill` §2a into update-skill. `skf-create-skill` §2a catches authoritative AI documentation files (`llms.txt`, `AGENTS.md`, `.cursorrules`, etc.) during **creation**. But a project may add these files *after* the skill was created — for example, an upstream project adopts an `llms.txt` convention six months into development. Without this mirror, update-skill would either miss the new file entirely (if it doesn't match the provenance map's file patterns) or classify it as a generic ADDED file in §2 Category A with no authoritative-file treatment. The mirror surfaces the discovery with the same P/S/U prompt create-skill uses, honoring any prior amendments.
112
+ **Purpose:** mirror `skf-create-skill` §2a into update-skill. `skf-create-skill` §2a catches authoritative AI documentation files (`llms.txt`, `AGENTS.md`, `.cursorrules`, etc.) during **creation**, but a project may add these files *after* the skill was created. Without this mirror, update-skill would either miss the new file entirely (if it doesn't match the provenance map's file patterns) or classify it as a generic ADDED file in §2 Category A with no authoritative-file treatment. The mirror surfaces the discovery with the same P/S/U prompt create-skill uses, honoring any prior amendments.
109
113
 
110
114
  **Skip this section entirely if:**
111
115
 
@@ -177,11 +181,11 @@ Read the source directory at `{source_root}` and build a current file inventory:
177
181
 
178
182
  **Record for evidence report:** the update-skill evidence report appends `authoritative_files_mirror: {candidates: N, promoted: P, skipped: S, pre_decided: A, already_tracked: T, decisions: [{path, action, heuristic, reason}]}`.
179
183
 
180
- **Interaction with §2 change detection:** promoted docs live in `promoted_docs_new[]`, NOT in the change manifest. But §2 Category A ("files in source but not in provenance map → ADDED") would still find the promoted doc files on disk and classify them as ADDED if nothing prevents it. The coordination mechanism is an explicit pre-filter exclusion set built in §2.0 (below) that every Category A subprocess worker receives as an input before it starts scanning. See §2.0 for the exact contract. The exclusion set is the only mechanism guaranteeing that parallel subprocesses cannot double-count `promoted_docs_new[]` paths — prose-level "skip any path" instructions cannot cross subprocess boundaries.
184
+ **Interaction with §2 change detection:** promoted docs live in `promoted_docs_new[]`, not in the change manifest. §2.0's `change_detection_excludes` set (built below) is what stops §2 Category A from re-classifying them as ADDED it is the only coordination that survives across the parallel Category A subprocesses.
181
185
 
182
186
  ### 1c. Major-Version Scope Reconciliation (Pre-Detection)
183
187
 
184
- **Purpose:** When upstream undergoes a paradigm shift (rebrand, package restructure, major-version rewrite), the brief's `scope.include` no longer reflects the real public API. §1b handles new authoritative-doc files; §1c handles new **code globs** that fall outside the original scope. Without it, update-skill silently misses the new surface and pays the gap cost on every future update — the cocoindex `0.3.37 → 1.0.0` and cognee `0.5.8 → 1.0.0` runs are existence proofs that this case is real and recurring.
188
+ **Purpose:** §1b handles new authoritative-doc files; §1c handles new **code globs** that fall outside the original scope when upstream restructures (rebrand, package restructure, major-version rewrite) so the brief's `scope.include` no longer reflects the real public API. Without it, update-skill silently misses the new public surface and pays the gap cost on every future update.
185
189
 
186
190
  **Skip this section entirely if:**
187
191
 
@@ -233,7 +237,7 @@ Read the source directory at `{source_root}` and build a current file inventory:
233
237
  - `status: "pre-decided-demoted"` → record as `pre_decided`; do not re-prompt (`prior_action` ∈ `demoted-include`, `demoted-exclude`).
234
238
  - `status: "unresolved"` → continue to step 4 (user prompt).
235
239
 
236
- **Note:** the Out-of-Scope section is an optional audit-skill output. `skf-audit-skill` does not currently discover new files (per `src/skf-audit-skill/references/re-index.md` — new-file detection is the responsibility of update-skill). The section is a forward-looking integration point: manual additions or a future audit-skill enhancement populate it.
240
+ **Note:** the Out-of-Scope section is an optional audit-skill output and is often absent or empty — new-file detection is update-skill's job (§1b/§2.2), not audit-skill's so the `no-report` / `no-candidates` paths above are the common case.
237
241
 
238
242
  3. **Prompt for each unresolved candidate.** Present the same menu shape as §1b:
239
243
 
@@ -358,15 +362,14 @@ Translate the helper's output into the change manifest:
358
362
  - `DELETED_FILE` rows → add to manifest as DELETED_FILE
359
363
  - `UNCHANGED` rows → omit from the manifest (no action needed)
360
364
 
361
- The helper does NOT detect NEW_FILE its job is provenance comparison only. Detect new files by running the same deterministic detector create-skill step 3 §4c uses (resolved via `detectScriptsAssetsProbeOrder`) against the current source tree:
365
+ The compare helper reports only tracked files; NEW_FILE detection (a file present in source but absent from the provenance map) is a set-difference, so it runs through a script rather than the prompt. Pipe the same deterministic detector create-skill step 3 §4c uses (resolved via `detectScriptsAssetsProbeOrder`) into `{newFileDiffHelper}`, which subtracts the provenance map's `file_entries[].source_file` and sets aside user-authored `[MANUAL]` paths:
362
366
 
363
367
  ```bash
364
- uv run {detectScriptsAssetsHelper} detect <source-root>
368
+ uv run {detectScriptsAssetsHelper} detect <source-root> \
369
+ | uv run {newFileDiffHelper} {forge_version}/provenance-map.json
365
370
  ```
366
371
 
367
- It emits `scripts_inventory[]` and `assets_inventory[]` (each entry carries `source_file`). Take every `source_file` across both inventories, then subtract the paths already present in `provenance.file_entries[].source_file`. Of the remaining files, those matching detection patterns (`scripts/`, `bin/`, `assets/`, `templates/`) and NOT under `scripts/[MANUAL]/` or `assets/[MANUAL]/` are NEW_FILE.
368
-
369
- Files in `scripts/[MANUAL]/` or `assets/[MANUAL]/` → SKIP (user-authored, preserved).
372
+ It emits `{"new_files":[{source_file, kind}], "skipped_manual":[...], "already_tracked":[...], "stats":{...}}`. Add each `new_files[]` entry to the manifest as NEW_FILE `kind` (`script`/`asset`) selects the target array. `skipped_manual[]` are user-authored files under `scripts/[MANUAL]/` or `assets/[MANUAL]/`, preserved and not touched; `already_tracked[]` were handled by the compare above.
370
373
 
371
374
  Aggregate all subprocess results into a unified change manifest.
372
375
 
@@ -429,8 +432,6 @@ The upstream surface appears to have been substantially replaced. The brief's
429
432
  - **[B] Brief:** halt with status `halted-for-brief-refinement`. Display: `"Halting update-skill. Re-run skf-brief-skill to refine scope for {skill_name}, then re-run skf-update-skill."` Change manifest discarded — no partial writes.
430
433
  - **[A] Audit:** halt with status `halted-for-audit`. Display: `"Halting update-skill. Run skf-audit-skill against {skill_name} to map the new surface — its drift report will feed §1c on the next update-skill run."` Change manifest discarded.
431
434
 
432
- **Why both §1c and §2.2:** §1c is precise (per-path P/S/U) but requires upstream signal from audit-skill. §2.2 is coarse (single halt/continue) but self-contained — it fires even when the user runs update-skill directly without audit. Together they cover the major-version case across the two real workflows.
433
-
434
435
  ### 3. Build Change Manifest
435
436
 
436
437
  Hand the assembled Category A/B/C/D JSON to the helper:
@@ -490,7 +491,7 @@ The skill `{skill_name}` is current — no update needed.
490
491
 
491
492
  → Skip steps 03-06, immediately load {noChangeReportFile} with "no changes" status.
492
493
 
493
- ### 5. Display Change Summary and Auto-Proceed
494
+ ### 5. Display Change Summary and Route
494
495
 
495
496
  "**Change Detection Complete:**
496
497
 
@@ -500,30 +501,11 @@ The skill `{skill_name}` is current — no update needed.
500
501
  | Files added | {count} |
501
502
  | Files deleted | {count} |
502
503
  | Files moved/renamed | {count} |
503
- | Exports affected | {total_export_changes} |
504
-
505
- **Proceeding to re-extraction of {affected_file_count if normal mode, or gap_count if gap-driven mode} changes...**"
506
-
507
- ### 6. Present MENU OPTIONS
508
-
509
- Display: "**Proceeding to re-extraction...**"
510
-
511
- #### Menu Handling Logic:
512
-
513
- - **If `detect_only_mode` is true:** display "**Detect-only mode — skipping re-extract/merge/validate/write.** Loading report..." and load `{noChangeReportFile}` (report.md). The report handles the detect-only envelope. Do NOT load `{nextStepFile}`.
514
- - Else, after change manifest is built, immediately load, read entire file, then execute `{nextStepFile}`.
515
- - **EXCEPTION:** If no changes detected (section 4), load `{noChangeReportFile}` instead.
516
-
517
- #### EXECUTION RULES:
518
-
519
- - This is an auto-proceed step with no user choices
520
- - Proceed directly to next step after change detection completes (or to report when `detect_only_mode` is true)
521
-
522
- ## CRITICAL STEP COMPLETION NOTE
504
+ | Exports affected | {total_export_changes} |"
523
505
 
524
- ONLY WHEN the change manifest is fully built will you load the next file:
506
+ This step auto-proceeds — no user choices. Once the change manifest is fully built, load and fully read the next file, then execute it, per the branch that applies:
525
507
 
526
- - `detect_only_mode == true` → load `{noChangeReportFile}` (report.md). Report emits status `detect-only`.
527
- - No changes detected → load `{noChangeReportFile}` (report.md). Report emits status `no-changes`.
528
- - Otherwise → load `{nextStepFile}` (re-extract.md) to begin re-extraction.
508
+ - **`detect_only_mode == true`**display "**Detect-only mode — skipping re-extract/merge/validate/write.** Loading report..." and load `{noChangeReportFile}` (report.md), which emits status `detect-only`. Do not load `{nextStepFile}`.
509
+ - **No changes detected** (section 4) → load `{noChangeReportFile}` (report.md), which emits status `no-changes`.
510
+ - **Otherwise**display "**Proceeding to re-extraction of {affected_file_count if normal mode, or gap_count if gap-driven mode} changes...**" and load `{nextStepFile}` (re-extract.md) to begin re-extraction.
529
511