bmad-module-skill-forge 2.0.0 → 2.0.2

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 (237) 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/docs/workflows.md +1 -1
  5. package/package.json +2 -2
  6. package/src/knowledge/skill-lifecycle.md +1 -1
  7. package/src/module-help.csv +2 -2
  8. package/src/shared/health-check.md +40 -3
  9. package/src/shared/scripts/skf-comention-pairs.py +350 -0
  10. package/src/shared/scripts/skf-count-tokens.py +222 -0
  11. package/src/shared/scripts/skf-detect-docs.py +232 -1
  12. package/src/shared/scripts/skf-detect-language.py +77 -0
  13. package/src/shared/scripts/skf-detect-tools.py +105 -2
  14. package/src/shared/scripts/skf-enumerate-stack-skills.py +111 -2
  15. package/src/shared/scripts/skf-find-cycles.py +246 -0
  16. package/src/shared/scripts/skf-hash-content.py +487 -2
  17. package/src/shared/scripts/skf-manifest-ops.py +67 -1
  18. package/src/shared/scripts/skf-rebuild-managed-sections.py +180 -0
  19. package/src/shared/scripts/skf-recommend-scope-type.py +1 -2
  20. package/src/shared/scripts/skf-render-metadata-stats.py +499 -0
  21. package/src/shared/scripts/skf-rewrite-skill-name.py +308 -0
  22. package/src/shared/scripts/skf-shard-body.py +461 -0
  23. package/src/shared/scripts/skf-skill-inventory.py +139 -3
  24. package/src/shared/scripts/skf-structural-diff.py +307 -67
  25. package/src/shared/scripts/skf-validate-brief-schema.py +39 -1
  26. package/src/shared/scripts/skf-validate-feasibility-report.py +234 -0
  27. package/src/shared/scripts/skf-validate-frontmatter.py +17 -12
  28. package/src/shared/scripts/skf-validate-output.py +443 -10
  29. package/src/shared/scripts/skf-validate-pins.py +1 -1
  30. package/src/shared/scripts/skf-verify-no-trace.py +214 -0
  31. package/src/shared/scripts/skf-verify-provenance-completeness.py +439 -0
  32. package/src/skf-analyze-source/SKILL.md +17 -32
  33. package/src/skf-analyze-source/customize.toml +2 -0
  34. package/src/skf-analyze-source/references/auto-docs-only.md +116 -0
  35. package/src/skf-analyze-source/references/continue.md +8 -13
  36. package/src/skf-analyze-source/references/generate-briefs.md +28 -12
  37. package/src/skf-analyze-source/references/headless-contract.md +33 -0
  38. package/src/skf-analyze-source/references/health-check.md +1 -2
  39. package/src/skf-analyze-source/references/identify-units.md +25 -36
  40. package/src/skf-analyze-source/references/init.md +12 -25
  41. package/src/skf-analyze-source/references/map-and-detect.md +8 -20
  42. package/src/skf-analyze-source/references/recommend.md +4 -14
  43. package/src/skf-analyze-source/references/scan-project.md +3 -11
  44. package/src/skf-analyze-source/references/step-auto-scope.md +42 -180
  45. package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -2
  46. package/src/skf-audit-skill/SKILL.md +9 -6
  47. package/src/skf-audit-skill/customize.toml +2 -0
  48. package/src/skf-audit-skill/references/init.md +58 -32
  49. package/src/skf-audit-skill/references/re-index.md +15 -34
  50. package/src/skf-audit-skill/references/report.md +11 -58
  51. package/src/skf-audit-skill/references/semantic-diff.md +4 -22
  52. package/src/skf-audit-skill/references/severity-classify.md +53 -87
  53. package/src/skf-audit-skill/references/step-doc-drift.md +49 -19
  54. package/src/skf-audit-skill/references/structural-diff.md +43 -68
  55. package/src/skf-brief-skill/SKILL.md +7 -36
  56. package/src/skf-brief-skill/assets/scope-templates.md +1 -1
  57. package/src/skf-brief-skill/customize.toml +14 -0
  58. package/src/skf-brief-skill/references/analyze-target.md +6 -14
  59. package/src/skf-brief-skill/references/confirm-brief.md +2 -14
  60. package/src/skf-brief-skill/references/draft-checkpoint.md +8 -8
  61. package/src/skf-brief-skill/references/gather-intent.md +19 -22
  62. package/src/skf-brief-skill/references/headless-args.md +1 -1
  63. package/src/skf-brief-skill/references/health-check.md +1 -22
  64. package/src/skf-brief-skill/references/invocation-contract.md +37 -0
  65. package/src/skf-brief-skill/references/scope-definition.md +4 -14
  66. package/src/skf-brief-skill/references/step-auto-brief.md +1 -5
  67. package/src/skf-brief-skill/references/step-auto-validate.md +1 -5
  68. package/src/skf-brief-skill/references/version-resolution.md +1 -1
  69. package/src/skf-brief-skill/references/write-brief.md +17 -10
  70. package/src/skf-campaign/SKILL.md +15 -84
  71. package/src/skf-campaign/customize.toml +2 -0
  72. package/src/skf-campaign/manifest.yaml +4 -0
  73. package/src/skf-campaign/references/campaign-contracts.md +67 -0
  74. package/src/skf-campaign/references/campaign-directive-spec.md +14 -103
  75. package/src/skf-campaign/references/step-01-setup.md +4 -5
  76. package/src/skf-campaign/references/step-02-strategy.md +8 -3
  77. package/src/skf-campaign/references/step-03-pins.md +1 -1
  78. package/src/skf-campaign/references/step-04-provenance.md +1 -1
  79. package/src/skf-campaign/references/step-05-skill-loop.md +1 -1
  80. package/src/skf-campaign/references/step-06-batch.md +7 -6
  81. package/src/skf-campaign/references/step-11-maintenance.md +2 -2
  82. package/src/skf-campaign/references/step-resume.md +9 -8
  83. package/src/skf-campaign/scripts/campaign-deps.py +10 -1
  84. package/src/skf-campaign/scripts/campaign-render-batch.py +199 -0
  85. package/src/skf-campaign/scripts/campaign-status.py +162 -0
  86. package/src/skf-campaign/templates/campaign-brief-template.yaml +1 -1
  87. package/src/skf-create-skill/SKILL.md +14 -3
  88. package/src/skf-create-skill/assets/compile-assembly-rules.md +17 -38
  89. package/src/skf-create-skill/assets/skill-sections.md +9 -100
  90. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +4 -13
  91. package/src/skf-create-skill/customize.toml +13 -0
  92. package/src/skf-create-skill/references/authoritative-files-protocol.md +7 -7
  93. package/src/skf-create-skill/references/compile.md +34 -47
  94. package/src/skf-create-skill/references/component-extraction.md +8 -35
  95. package/src/skf-create-skill/references/ecosystem-check.md +9 -25
  96. package/src/skf-create-skill/references/enrich.md +4 -17
  97. package/src/skf-create-skill/references/extract.md +14 -29
  98. package/src/skf-create-skill/references/extraction-patterns-tracing.md +1 -1
  99. package/src/skf-create-skill/references/extraction-patterns.md +7 -7
  100. package/src/skf-create-skill/references/generate-artifacts.md +9 -23
  101. package/src/skf-create-skill/references/health-check.md +1 -1
  102. package/src/skf-create-skill/references/load-brief.md +16 -18
  103. package/src/skf-create-skill/references/report.md +12 -15
  104. package/src/skf-create-skill/references/source-resolution-protocols.md +3 -3
  105. package/src/skf-create-skill/references/step-auto-shard.md +52 -25
  106. package/src/skf-create-skill/references/step-doc-rot.md +30 -12
  107. package/src/skf-create-skill/references/step-doc-sources.md +2 -2
  108. package/src/skf-create-skill/references/sub/ccc-discover.md +2 -13
  109. package/src/skf-create-skill/references/sub/fetch-docs.md +8 -50
  110. package/src/skf-create-skill/references/sub/fetch-temporal.md +5 -20
  111. package/src/skf-create-skill/references/tier-degradation-rules.md +6 -8
  112. package/src/skf-create-skill/references/validate.md +59 -28
  113. package/src/skf-create-skill/scripts/scan-doc-rot.py +245 -0
  114. package/src/skf-create-stack-skill/SKILL.md +15 -9
  115. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +1 -1
  116. package/src/skf-create-stack-skill/customize.toml +10 -0
  117. package/src/skf-create-stack-skill/references/compile-stack.md +7 -8
  118. package/src/skf-create-stack-skill/references/compose-mode-rules.md +2 -13
  119. package/src/skf-create-stack-skill/references/detect-integrations.md +49 -54
  120. package/src/skf-create-stack-skill/references/detect-manifests.md +37 -30
  121. package/src/skf-create-stack-skill/references/generate-output.md +24 -75
  122. package/src/skf-create-stack-skill/references/init.md +9 -81
  123. package/src/skf-create-stack-skill/references/manifest-patterns.md +5 -35
  124. package/src/skf-create-stack-skill/references/parallel-extract.md +12 -27
  125. package/src/skf-create-stack-skill/references/rank-and-confirm.md +6 -6
  126. package/src/skf-create-stack-skill/references/report.md +27 -62
  127. package/src/skf-create-stack-skill/references/validate.md +35 -31
  128. package/src/skf-drop-skill/SKILL.md +20 -23
  129. package/src/skf-drop-skill/customize.toml +10 -0
  130. package/src/skf-drop-skill/references/execute.md +51 -31
  131. package/src/skf-drop-skill/references/headless-contract.md +18 -0
  132. package/src/skf-drop-skill/references/health-check.md +1 -13
  133. package/src/skf-drop-skill/references/report.md +42 -10
  134. package/src/skf-drop-skill/references/select.md +88 -48
  135. package/src/skf-drop-skill/scripts/dir-sizes.py +116 -0
  136. package/src/skf-export-skill/SKILL.md +17 -14
  137. package/src/skf-export-skill/assets/managed-section-format.md +2 -5
  138. package/src/skf-export-skill/customize.toml +10 -6
  139. package/src/skf-export-skill/references/generate-snippet.md +21 -35
  140. package/src/skf-export-skill/references/load-skill.md +44 -42
  141. package/src/skf-export-skill/references/multi-skill-mode.md +1 -1
  142. package/src/skf-export-skill/references/orphan-context-detection.md +11 -3
  143. package/src/skf-export-skill/references/package.md +27 -33
  144. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +24 -5
  145. package/src/skf-export-skill/references/result-envelope.md +28 -0
  146. package/src/skf-export-skill/references/summary.md +11 -5
  147. package/src/skf-export-skill/references/token-report.md +42 -24
  148. package/src/skf-export-skill/references/update-context.md +32 -33
  149. package/src/skf-forger/SKILL.md +27 -39
  150. package/src/skf-forger/references/pipeline-mode.md +34 -0
  151. package/src/skf-forger/scripts/parse-pipeline.py +270 -0
  152. package/src/skf-quick-skill/SKILL.md +10 -60
  153. package/src/skf-quick-skill/customize.toml +9 -0
  154. package/src/skf-quick-skill/references/batch-mode.md +6 -4
  155. package/src/skf-quick-skill/references/compile.md +15 -52
  156. package/src/skf-quick-skill/references/ecosystem-check.md +5 -20
  157. package/src/skf-quick-skill/references/finalize.md +12 -6
  158. package/src/skf-quick-skill/references/halt-contract.md +54 -0
  159. package/src/skf-quick-skill/references/health-check.md +3 -4
  160. package/src/skf-quick-skill/references/quick-extract.md +7 -18
  161. package/src/skf-quick-skill/references/registry-resolution.md +1 -23
  162. package/src/skf-quick-skill/references/resolve-target.md +27 -34
  163. package/src/skf-quick-skill/references/write-and-validate.md +10 -25
  164. package/src/skf-refine-architecture/SKILL.md +14 -7
  165. package/src/skf-refine-architecture/customize.toml +9 -0
  166. package/src/skf-refine-architecture/references/compile.md +9 -7
  167. package/src/skf-refine-architecture/references/gap-analysis.md +21 -50
  168. package/src/skf-refine-architecture/references/health-check.md +1 -1
  169. package/src/skf-refine-architecture/references/improvements.md +9 -25
  170. package/src/skf-refine-architecture/references/init.md +9 -16
  171. package/src/skf-refine-architecture/references/issue-detection.md +10 -25
  172. package/src/skf-refine-architecture/references/refinement-rules.md +4 -16
  173. package/src/skf-refine-architecture/references/report.md +13 -10
  174. package/src/skf-rename-skill/SKILL.md +11 -21
  175. package/src/skf-rename-skill/customize.toml +10 -0
  176. package/src/skf-rename-skill/references/execute.md +107 -78
  177. package/src/skf-rename-skill/references/exit-codes.md +12 -0
  178. package/src/skf-rename-skill/references/report.md +16 -8
  179. package/src/skf-rename-skill/references/select.md +69 -42
  180. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -0
  181. package/src/skf-setup/SKILL.md +20 -12
  182. package/src/skf-setup/customize.toml +10 -0
  183. package/src/skf-setup/references/auto-index.md +9 -10
  184. package/src/skf-setup/references/ccc-index.md +14 -15
  185. package/src/skf-setup/references/detect-and-tier.md +6 -8
  186. package/src/skf-setup/references/report.md +22 -31
  187. package/src/skf-setup/references/tier-rules.md +2 -32
  188. package/src/skf-setup/references/write-config.md +9 -10
  189. package/src/skf-test-skill/SKILL.md +10 -8
  190. package/src/skf-test-skill/customize.toml +2 -0
  191. package/src/skf-test-skill/references/coherence-check.md +19 -31
  192. package/src/skf-test-skill/references/coverage-check.md +109 -75
  193. package/src/skf-test-skill/references/detect-mode.md +10 -41
  194. package/src/skf-test-skill/references/external-validators.md +11 -19
  195. package/src/skf-test-skill/references/init.md +38 -20
  196. package/src/skf-test-skill/references/migration-section-rules.md +4 -4
  197. package/src/skf-test-skill/references/report.md +60 -40
  198. package/src/skf-test-skill/references/score.md +32 -64
  199. package/src/skf-test-skill/references/scoring-rules.md +6 -62
  200. package/src/skf-test-skill/references/source-access-protocol.md +6 -6
  201. package/src/skf-test-skill/references/step-hard-gate.md +1 -3
  202. package/src/skf-test-skill/scripts/aggregate-coherence.py +276 -0
  203. package/src/skf-test-skill/scripts/check-metadata-coherence.py +359 -0
  204. package/src/skf-test-skill/scripts/combine-external-scores.py +190 -0
  205. package/src/skf-test-skill/scripts/compute-score.py +131 -1
  206. package/src/skf-test-skill/scripts/reconcile-coverage.py +327 -0
  207. package/src/skf-test-skill/scripts/validate-inventory.py +251 -0
  208. package/src/skf-test-skill/scripts/verify-declared-numerator.py +192 -0
  209. package/src/skf-update-skill/SKILL.md +17 -5
  210. package/src/skf-update-skill/customize.toml +14 -0
  211. package/src/skf-update-skill/references/detect-changes.md +19 -37
  212. package/src/skf-update-skill/references/health-check.md +3 -9
  213. package/src/skf-update-skill/references/init.md +39 -32
  214. package/src/skf-update-skill/references/manual-section-rules.md +1 -2
  215. package/src/skf-update-skill/references/merge-conflict-rules.md +1 -52
  216. package/src/skf-update-skill/references/merge.md +13 -57
  217. package/src/skf-update-skill/references/re-extract.md +12 -24
  218. package/src/skf-update-skill/references/report.md +14 -7
  219. package/src/skf-update-skill/references/validate.md +32 -30
  220. package/src/skf-update-skill/references/write.md +86 -51
  221. package/src/skf-update-skill/scripts/skf-new-file-diff.py +167 -0
  222. package/src/skf-verify-stack/SKILL.md +11 -21
  223. package/src/skf-verify-stack/customize.toml +12 -3
  224. package/src/skf-verify-stack/references/coverage-patterns.md +1 -29
  225. package/src/skf-verify-stack/references/coverage.md +21 -10
  226. package/src/skf-verify-stack/references/exit-codes.md +14 -0
  227. package/src/skf-verify-stack/references/health-check.md +1 -1
  228. package/src/skf-verify-stack/references/init.md +20 -25
  229. package/src/skf-verify-stack/references/integration-verification-rules.md +10 -19
  230. package/src/skf-verify-stack/references/integrations.md +44 -49
  231. package/src/skf-verify-stack/references/report.md +28 -34
  232. package/src/skf-verify-stack/references/requirements.md +3 -3
  233. package/src/skf-verify-stack/references/synthesize.md +36 -39
  234. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +196 -0
  235. package/src/skf-verify-stack/scripts/skf-report-delta.py +256 -0
  236. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +258 -0
  237. package/src/skf-rename-skill/references/rebuild-context.md +0 -110
@@ -30,7 +30,7 @@
30
30
  "name": "skill-forge",
31
31
  "source": "./",
32
32
  "description": "Evidence-based agent skills compiler with progressive capability tiers (Quick/Forge/Forge+/Deep).",
33
- "version": "2.0.0",
33
+ "version": "2.0.2",
34
34
  "author": {
35
35
  "name": "Armel"
36
36
  },
package/.nvmrc CHANGED
@@ -1 +1 @@
1
- 22
1
+ 24
@@ -26,7 +26,7 @@
26
26
  # Enforced two ways: (1) release.yaml bumps this line in the same commit
27
27
  # as package.json + marketplace.json on every release; (2) validate-docs-drift.js
28
28
  # cross-checks this value against package.json.version and fails on mismatch.
29
- skf_version: "2.0.0"
29
+ skf_version: "2.0.2"
30
30
 
31
31
  # Path to the oh-my-skills repo, resolved relative to this repo's root.
32
32
  # Override at runtime with the OMS environment variable if oh-my-skills
package/docs/workflows.md CHANGED
@@ -474,7 +474,7 @@ If the answer to all of these is "no", the health check exits in one line (`Clea
474
474
 
475
475
  - **Severity gate.** Only `bug` findings submit live as GitHub issues by default. `friction` and `gap` findings — the most subjective categories — go to a **local queue** at `{forge_data_folder}/improvement-queue/` unless you explicitly opt in to submit them live during the review gate. This keeps the high-signal reports (real defects) flowing to maintainers while the softer observations sit safely on your disk for you to batch or revisit.
476
476
  - **Fingerprint dedup.** Every finding gets a deterministic 7-hex fingerprint computed from `sha1(severity|workflow|step_file|section)` — no LLM similarity judgment, just a tuple hash. Before Ferris opens a new issue, it searches the repo for an existing open issue with the same `fp-*` label. If one exists, you're offered a choice: add a 👍 reaction (silent upvote), react + post a one-sentence environment delta, open a new issue anyway (if you're certain it's distinct), or skip. Re-reporting the same fingerprint is safe — it just adds to the signal-count on the canonical issue.
477
- - **Global seen-cache.** Once you've submitted or reacted for a given fingerprint, it's recorded at `~/.skf/health-check-seen.json` so the same user never re-reports the same defect across sessions or across different projects on the same machine.
477
+ - **Global seen-cache.** Once you've submitted or reacted for a given fingerprint, it's recorded at `~/.skf/health-check-seen.json` so the same user doesn't re-report the same defect across sessions or across different projects on the same machine, for as long as the issue it points at stays open. Once that issue is closed as completed, the suppression lifts on purpose: a fresh sighting of a fixed defect is a regression, and it gets reported and linked back to the original.
478
478
  - **Server-side safety net.** If two users race past the client-side search and both open issues with the same fingerprint, a GitHub Action on this repo catches it: the later issue is auto-closed as a duplicate, linked to the canonical (lowest-numbered) issue, and a 👍 is added there to preserve the signal-count. Manual filers using the [issue template](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/new/choose) feed the same pipeline.
479
479
 
480
480
  **Net effect:** 1,000 users hitting the same bug produce **one canonical issue** with a reaction-count of roughly 1,000 — not 1,000 duplicate issues or a 1,000-comment thread. The maintainer sees population impact at a glance, and your report is never lost.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-module-skill-forge",
4
- "version": "2.0.0",
4
+ "version": "2.0.2",
5
5
  "description": "BMAD module — Turn code and docs into instructions AI agents can actually follow. Progressive capability tiers (Quick/Forge/Forge+/Deep).",
6
6
  "keywords": [
7
7
  "bmad",
@@ -53,7 +53,7 @@
53
53
  "test:cli": "node test/test-cli-integration.js",
54
54
  "test:install": "node test/test-installation-components.js",
55
55
  "test:knowledge": "node test/test-knowledge-base.js",
56
- "test:python": "uv run --with pytest --with pyyaml --with jsonschema pytest test/test-compute-score-contract.py test/test-skf-preflight.py test/test-skf-skill-inventory.py test/test-skf-validate-output.py test/test-skf-validate-frontmatter.py test/test-skf-manifest-ops.py test/test-skf-rebuild-managed-sections.py test/test-skf-atomic-write.py test/test-skf-severity-classify.py test/test-skf-structural-diff.py test/test-skf-detect-tools.py test/test-skf-forge-tier-rw.py test/test-skf-emit-result-envelope.py test/test-skf-qmd-classify-collections.py test/test-skf-merge-ccc-exclusions.py test/test-skf-resolve-package.py test/test-skf-extract-public-api.py test/test-skf-render-quick-metadata.py test/test-skf-validate-brief-inputs.py test/test-skf-emit-brief-result-envelope.py test/test-skf-write-skill-brief.py test/test-skf-detect-workspaces.py test/test-skf-recommend-scope-type.py test/test-skf-detect-language.py test/test-skf-language-corpora.py test/test-skf-merge-doc-urls.py test/test-skf-derive-assembly-shape.py test/test-skf-description-guard.py test/test-skf-detect-scripts-assets.py test/test-skf-hash-content.py test/test-skf-validate-brief-schema.py test/test-skf-check-workspace-drift.py test/test-skf-update-active-symlink.py test/test-skf-build-change-manifest.py test/test-skf-provenance-gap-dispatch.py test/test-skf-resolve-authoritative-files.py test/test-skf-scan-manifests.py test/test-skf-pair-intersect.py test/test-skf-enumerate-stack-skills.py test/test-skf-compare-file-hashes.py test/test-skf-load-provenance.py test/test-skf-scan-skill-md-structure.py test/test-skf-disqualify-candidates.py test/test-skf-chain-reachability.py test/test-skf-shape-detect.py test/test-skf-detect-docs.py test/test-skf-step-doc-sources.py test/test-skf-step-doc-drift.py test/test-skf-step-hard-gate.py test/test-skf-per-pipeline-thresholds.py test/test-skf-evidence-report-fallback.py test/test-skf-validate-pins.py test/test-skf-auto-shard.py test/test-skf-preapply.py test/test-skf-step-doc-rot.py test/test-skf-campaign-state.py test/test-skf-campaign-pins.py test/test-skf-campaign-deps.py test/test-skf-campaign-report.py test/test-skf-campaign-stepfiles.py test/test-skf-campaign-validate-state.py test/test-skf-campaign-provenance.py test/test-skf-campaign-parse-manifest.py test/test-skf-campaign-render-kickoff.py -v",
56
+ "test:python": "uv run --with pytest --with pyyaml --with jsonschema pytest test/test-compute-score-contract.py test/test-skf-preflight.py test/test-skf-skill-inventory.py test/test-skf-validate-output.py test/test-skf-validate-frontmatter.py test/test-skf-manifest-ops.py test/test-skf-rebuild-managed-sections.py test/test-skf-atomic-write.py test/test-skf-severity-classify.py test/test-skf-structural-diff.py test/test-skf-detect-tools.py test/test-skf-forge-tier-rw.py test/test-skf-emit-result-envelope.py test/test-skf-qmd-classify-collections.py test/test-skf-merge-ccc-exclusions.py test/test-skf-resolve-package.py test/test-skf-extract-public-api.py test/test-skf-render-quick-metadata.py test/test-skf-validate-brief-inputs.py test/test-skf-emit-brief-result-envelope.py test/test-skf-write-skill-brief.py test/test-skf-detect-workspaces.py test/test-skf-recommend-scope-type.py test/test-skf-detect-language.py test/test-skf-language-corpora.py test/test-skf-merge-doc-urls.py test/test-skf-derive-assembly-shape.py test/test-skf-description-guard.py test/test-skf-detect-scripts-assets.py test/test-skf-hash-content.py test/test-skf-validate-brief-schema.py test/test-skf-check-workspace-drift.py test/test-skf-update-active-symlink.py test/test-skf-build-change-manifest.py test/test-skf-provenance-gap-dispatch.py test/test-skf-resolve-authoritative-files.py test/test-skf-scan-manifests.py test/test-skf-pair-intersect.py test/test-skf-enumerate-stack-skills.py test/test-skf-compare-file-hashes.py test/test-skf-load-provenance.py test/test-skf-scan-skill-md-structure.py test/test-skf-disqualify-candidates.py test/test-skf-chain-reachability.py test/test-skf-shape-detect.py test/test-skf-detect-docs.py test/test-skf-step-doc-sources.py test/test-skf-step-doc-drift.py test/test-skf-step-hard-gate.py test/test-skf-per-pipeline-thresholds.py test/test-skf-evidence-report-fallback.py test/test-skf-validate-pins.py test/test-skf-auto-shard.py test/test-skf-preapply.py test/test-skf-step-doc-rot.py test/test-skf-campaign-state.py test/test-skf-campaign-pins.py test/test-skf-campaign-deps.py test/test-skf-campaign-report.py test/test-skf-campaign-stepfiles.py test/test-skf-campaign-validate-state.py test/test-skf-campaign-provenance.py test/test-skf-campaign-parse-manifest.py test/test-skf-campaign-render-kickoff.py test/test-skf-campaign-render-batch.py test/test-skf-shard-body.py test/test-skf-render-metadata-stats.py test/test-skf-comention-pairs.py test/test-skf-count-tokens.py test/test-skf-reconcile-coverage.py test/test-skf-verify-provenance-completeness.py test/test-skf-find-cycles.py test/test-skf-validate-feasibility-report.py test/test-skf-rewrite-skill-name.py test/test-skf-verify-no-trace.py test/test-skf-validate-inventory.py test/test-skf-check-metadata-coherence.py test/test-skf-scan-doc-rot.py test/test-skf-aggregate-coherence.py test/test-skf-dir-sizes.py test/test-skf-parse-pipeline.py test/test-skf-validate-rename-name.py test/test-skf-verify-declared-numerator.py test/test-skf-combine-external-scores.py test/test-skf-new-file-diff.py test/test-skf-coverage-tally.py test/test-skf-report-delta.py test/test-skf-verdict-rollup.py -v",
57
57
  "test:schemas": "node test/test-agent-schema.js",
58
58
  "test:workflow": "node test/test-workflow-state.js",
59
59
  "validate:refs": "node tools/validate-file-refs.js --strict",
@@ -143,7 +143,7 @@ EX → agentskills.io bundle + context snippets (flat platform paths)
143
143
 
144
144
  VS → feasibility-report-{project_name}.md (verdict + integration verdicts)
145
145
  ↓ (report consumed by RA)
146
- RA → refined-architecture-{project_name}.md (gaps filled, issues flagged, improvements suggested)
146
+ RA → refined-architecture-{arch_project_name}.md (gaps filled, issues flagged, improvements suggested)
147
147
  ↓ (refined doc consumed by SS compose-mode)
148
148
  SS (compose) → SKILL.md (stack skill synthesized from individual skills + architecture)
149
149
  ```
@@ -1,4 +1,4 @@
1
- module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs
1
+ module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs
2
2
  skf,skf-forger,Ferris — Skill Forge Agent,FF,"Skill compilation specialist — the forge master. Invoke to access all SKF workflows via guided menu.",,,anytime,,,false,,
3
3
  skf,skf-setup,Setup Forge,SF,"Initialize forge environment — detect tools and set capability tier (Quick/Forge/Forge+/Deep)",,,anytime,,"skf-analyze-source,skf-brief-skill,skf-quick-skill,skf-verify-stack,skf-campaign",false,,forge-tier.yaml
4
4
  skf,skf-campaign,Campaign,CA,"Orchestrate multi-library skill production across sessions — dependency tracking, file-based state, and resume",,resume [--from=<skill>],anytime,skf-setup,,false,forge_data_folder,_campaign-state.yaml
@@ -10,7 +10,7 @@ skf,skf-create-stack-skill,Stack Skill,SS,"Consolidated project stack skill with
10
10
  skf,skf-update-skill,Update Skill,US,"Smart regeneration preserving [MANUAL] sections after source changes",,,anytime,skf-export-skill,,false,skills_output_folder,SKILL.md
11
11
  skf,skf-audit-skill,Audit Skill,AS,"Drift detection between skill and current source code",,,anytime,skf-export-skill,,false,,drift report
12
12
  skf,skf-verify-stack,Verify Stack,VS,"Pre-code stack feasibility verification against architecture and PRD documents",,,anytime,skf-setup,skf-refine-architecture,false,forge_data_folder,feasibility-report-{project_name}.md
13
- skf,skf-refine-architecture,Refine Architecture,RA,"Improve architecture doc using verified skill data and VS feasibility findings",,,anytime,skf-verify-stack,,false,output_folder,refined-architecture-{project_name}.md
13
+ skf,skf-refine-architecture,Refine Architecture,RA,"Improve architecture doc using verified skill data and VS feasibility findings",,,anytime,skf-verify-stack,,false,output_folder,refined-architecture-{arch_project_name}.md
14
14
  skf,skf-test-skill,Test Skill,TS,"Cognitive completeness verification — quality gate before export",,,anytime,"skf-create-skill,skf-create-stack-skill,skf-quick-skill",skf-export-skill,false,,completeness score
15
15
  skf,skf-export-skill,Export Skill,EX,"Package for distribution and inject context into CLAUDE.md/AGENTS.md/.cursorrules",,,anytime,skf-test-skill,"skf-update-skill,skf-audit-skill,skf-rename-skill,skf-drop-skill",false,skills_output_folder,agentskills.io package
16
16
  skf,skf-rename-skill,Rename Skill,RS,"Rename a skill across all its versions — transactional copy-verify-delete with platform context rebuild",,,anytime,skf-export-skill,,false,skills_output_folder,renamed skill group
@@ -151,7 +151,36 @@ The `section-slug` is a kebab-case normalized section heading (e.g. `missing-sta
151
151
 
152
152
  **2. Check the local seen-cache** at `{seenCachePath}`:
153
153
 
154
- If the cache exists and already contains this fingerprint for this user, skip submission silently and log: `"fp-{hash}: already submitted from this machine on {date}, issue {url} — skipping"`. This prevents the same user from re-reporting the same defect across sessions.
154
+ Read `{seenCachePath}`. A missing file, an empty file, or one that does not parse as JSON is an empty cache — never an error, never a reason to halt.
155
+
156
+ **Presence in the cache is not the verdict.** A fingerprint whose defect was already fixed must NOT suppress: a fresh sighting of a fixed defect is a *regression*, and it is the single most valuable report this loop can produce. Look up the fingerprint and work these branches in order, stopping at the first that matches:
157
+
158
+ 1. **No record for this fingerprint** → `unseen`. Go to sub-step 3.
159
+ 2. **Record with `"action": "resolved"`** → `regression`. Recorded by hand when the defect was fixed. Go to sub-step 3 and apply the regression treatment below.
160
+ 3. **Record with `"action"` of `created`, `reacted`, or `commented`, and a non-empty `issue_url`** → you must decide whether that issue is still live before you may suppress. The cache is machine-local and records only what *this* user did; the issue's own state is shared, so it is the better authority. Run exactly this, and give it a hard time bound so a hung network can never strand the run:
161
+
162
+ ```
163
+ timeout 10 gh issue view {issue_url} --json state,stateReason --jq '.state + "/" + (.stateReason // "")'
164
+ ```
165
+
166
+ - `CLOSED/COMPLETED`, or `MERGED` (the record points at a resolving PR) → `regression`. It was fixed and closed out.
167
+ - `CLOSED/NOT_PLANNED` → `handled`. **Not** a fix: this is how the dedup Action closes a duplicate, and how a maintainer closes a wontfix. Treating it as a regression would file a REGRESSION issue against a defect nobody ever fixed, every session, forever.
168
+ - `OPEN` → `handled`. Already filed and still being tracked.
169
+ - Any failure at all (offline, timeout, no auth, deleted issue, unparseable output) → `handled`. This refinement never blocks a run and never halts on error.
170
+ 4. **Record with one of those actions but an empty `issue_url`** → `handled`. There is no issue to consult, so the local record stands.
171
+ 5. **Record with no `action`, or any other value** → `unrecognized`. Hand-edited, or written by a newer version of this file, so it cannot be confirmed handled. Go to sub-step 3 as an ordinary submission.
172
+
173
+ **`handled` is the only verdict that suppresses.** Skip submission silently and log: `"fp-{hash}: already handled on {date}, issue {url} — skipping"`. Every other verdict proceeds to sub-step 3.
174
+
175
+ **Why anything uncertain reports instead of suppressing.** A duplicate report is self-healing: the dedup Action closes it and moves the upvote to the canonical issue. A silenced regression is caught by nothing at all. When the verdict is in doubt, report.
176
+
177
+ **Regression treatment** (verdicts `regression` only). Carry the prior record forward so the recurrence can never be filed as a first sighting:
178
+
179
+ - Append ` — REGRESSION` to the issue title, after `{short description}`.
180
+ - Add a `## Regression` section immediately above `## Finding`, containing one line: `Regression of {issue_url}, resolved {date}.` If the prior record has no `issue_url`, write `Regression of a previously resolved local finding, resolved {date}.` The `{fp}` label still ties the two together server-side.
181
+ - Leave `## Finding` and the other one-sentence sections exactly as specified below. The backlink lives in its own section precisely so those budgets are untouched.
182
+
183
+ **What this does and does not catch.** The fingerprint is `sha1` over the section slug, so a fix that renames or relocates the section it touches changes the fingerprint. Such a regression arrives as `unseen` and is reported as a first sighting, which is safe but unlinked. These branches catch the regressions that recur under the same heading in the same step file, which is the case where the old behaviour was silent.
155
184
 
156
185
  **3. Check GitHub CLI availability** with `gh auth status`. If `gh` is unavailable, fall through to step 5c (local fallback).
157
186
 
@@ -205,9 +234,11 @@ gh issue create \
205
234
  --body "{formatted body using issue template structure}"
206
235
  ```
207
236
 
237
+ When sub-step 2 returned `regression`, append ` — REGRESSION` to that `--title` value, after `{short description}`, and include the `## Regression` backlink section in the body. The command above is the literal one you run, so the marker has to be in it: an instruction that the title should say REGRESSION loses to a copied command that does not.
238
+
208
239
  The fingerprint `{fp}` appears in both title (human-readable) and label (server-side filterable). Maintainers can query all reports for a defect via the `fp-*` label without relying on title text.
209
240
 
210
- After the issue is created, write the fingerprint → issue-url mapping to the seen-cache at `{seenCachePath}` so this user never re-reports the same fingerprint.
241
+ After the issue is created, write the fingerprint → issue-url mapping to the seen-cache at `{seenCachePath}` so this user does not re-report it while that issue stays open.
211
242
 
212
243
  **Writing rules — non-negotiable:**
213
244
 
@@ -300,7 +331,13 @@ After each successful `gh issue create`, append to `{seenCachePath}`:
300
331
  }
301
332
  ```
302
333
 
303
- Ensure the parent directory exists. This file is global across the user's machine — not per-project — so the same defect is never re-reported across different repos the user works in.
334
+ Ensure the parent directory exists. This file is global across the user's machine — not per-project — so the same defect is not re-reported across different repos the user works in while its issue stays open.
335
+
336
+ **Merge, never replace.** Read the existing file, set this one key, write the whole object back. A write that emits only the new key silently discards every other fingerprint the user has ever recorded.
337
+
338
+ **Read before you write, and never downgrade a `resolved` record.** Before setting the key, check whether one already exists with `"action": "resolved"`. If it does, and you are here because that resolved defect just recurred, you are recording the regression's new issue: overwrite it with `created` and the new URL, which is correct because a live issue now tracks the recurrence. In every other case leave a `resolved` record alone. The record must never move from `resolved` back to a suppressing action without a new open issue to justify it, or the next run silently re-arms the suppression the fix removed.
339
+
340
+ **The `action` vocabulary is `created`, `reacted`, `commented`, `resolved`.** The first three are written by this workflow. `resolved` is written by hand when the defect is fixed, and it is the only one that does not suppress. See the health-check section of `CONTRIBUTING.md` for when to record it.
304
341
 
305
342
  ### 5c. Local-Queue Path (gh unavailable OR friction/gap default)
306
343
 
@@ -0,0 +1,350 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.10"
4
+ # dependencies = []
5
+ # ///
6
+ """SKF Co-mention Pairs — compute compose-mode co-mention pairs from an
7
+ architecture document with the same precision guards the prompt used to run
8
+ by hand.
9
+
10
+ `detect-integrations.md` §2 (compose branch) asks the LLM to scan an
11
+ architecture markdown document for skill-name co-mentions using three
12
+ deterministic guards:
13
+
14
+ 1. Word-boundary matching (`\\b{skill_name}\\b`, case-insensitive) — no
15
+ substring false positives (`react` must not match inside `reactive`).
16
+ 2. Section filtering — paragraphs governed by an H1/H2 header that
17
+ normalises to one of {introduction, overview, glossary, table of
18
+ contents, references, appendix, index} are excluded, and heading text
19
+ itself is never a co-mention source.
20
+ 3. Two-paragraph minimum — a pair (A, B) qualifies only when at least two
21
+ distinct body paragraphs each contain word-boundary matches for both A
22
+ and B.
23
+
24
+ Given a fixed architecture doc and skill-name set the qualifying-pair set is
25
+ fully determined, so this belongs in a deterministic helper rather than
26
+ in-prompt regex/counting. It mirrors the co-import subprocess the code-mode
27
+ branch already delegates and the `skf-pair-intersect.py` pre-pass. The LLM
28
+ still does the semantic composition (integration-section authoring, tier
29
+ inheritance, VS-verdict parsing) — the script only emits the qualifying pairs
30
+ and their evidence paragraphs.
31
+
32
+ Subcommand:
33
+ comention --doc <md-file-or-'-'> --skills <json-file-or-'-'>
34
+ --doc path to the architecture markdown document, or '-' for stdin.
35
+ --skills path to a JSON array of loaded skill names
36
+ (e.g. `["react", "express"]`), or '-' for stdin.
37
+ (--doc and --skills cannot both read from stdin.)
38
+
39
+ Emit JSON:
40
+ {
41
+ "pairs": [
42
+ {
43
+ "a": "<skill>", "b": "<skill>",
44
+ "paragraph_count": N,
45
+ "evidence": [{"header": "<governing-header-or-null>",
46
+ "excerpt": "<collapsed paragraph text>"}, ...]
47
+ },
48
+ ...
49
+ ],
50
+ "excluded_section_count": M
51
+ }
52
+
53
+ Only unordered pairs (a < b lexicographically) with paragraph_count >= 2
54
+ are emitted. `pairs[]` is sorted by paragraph_count DESC, then (a, b)
55
+ ASC — stable, mirroring skf-pair-intersect.py. `evidence[]` is in
56
+ document order. `excluded_section_count` is the number of DISTINCT
57
+ excluded H1/H2 sections (by normalised header) that governed at least
58
+ one body paragraph — a diagnostic for the caller, not a gate.
59
+
60
+ CLI examples:
61
+ uv run skf-comention-pairs.py comention --doc arch.md --skills skills.json
62
+ echo '["react","express"]' | uv run skf-comention-pairs.py comention \\
63
+ --doc arch.md --skills -
64
+
65
+ Exit codes:
66
+ 0 — operation succeeded (including: no qualifying pairs → empty list)
67
+ 1 — user error (bad JSON, missing/unreadable file, both inputs on stdin,
68
+ malformed skills array)
69
+ 2 — unexpected internal error
70
+ """
71
+
72
+ from __future__ import annotations
73
+
74
+ import argparse
75
+ import json
76
+ import re
77
+ import sys
78
+ from pathlib import Path
79
+
80
+
81
+ # Closed exclusion set — governing H1/H2 headers that normalise to one of
82
+ # these drop their paragraphs from co-mention analysis (they typically
83
+ # enumerate all libraries without describing an integration).
84
+ EXCLUDED_HEADERS = frozenset({
85
+ "introduction",
86
+ "overview",
87
+ "glossary",
88
+ "table of contents",
89
+ "references",
90
+ "appendix",
91
+ "index",
92
+ })
93
+
94
+ # ATX header line: 1-6 leading hashes followed by at least one space/tab.
95
+ # A line like `#foo` (no space) is NOT a header per CommonMark and is treated
96
+ # as body text. Capture group 1 = hashes (level), group 2 = header text.
97
+ _HEADER_RE = re.compile(r"^(#{1,6})[ \t]+(.*)$")
98
+
99
+ _EXCERPT_LIMIT = 240
100
+
101
+
102
+ class UserError(Exception):
103
+ """A caller-facing input error → exit code 1."""
104
+
105
+
106
+ # --------------------------------------------------------------------------
107
+ # Markdown parsing
108
+ # --------------------------------------------------------------------------
109
+
110
+
111
+ def _strip_closing_hashes(text: str) -> str:
112
+ """Remove a closed-ATX trailing run of hashes (e.g. `Overview ##`)."""
113
+ return re.sub(r"[ \t]*#+[ \t]*$", "", text)
114
+
115
+
116
+ def normalize_header(text: str) -> str:
117
+ """Normalise a header for exclusion-set comparison: lowercase, collapse
118
+ internal whitespace, strip trailing punctuation."""
119
+ t = text.strip().lower()
120
+ t = re.sub(r"\s+", " ", t).strip()
121
+ t = t.rstrip(".,:;!?").strip()
122
+ return t
123
+
124
+
125
+ def parse_body_paragraphs(doc_text: str) -> list[tuple[str | None, str]]:
126
+ """Split the document into body paragraphs, each tagged with the raw text
127
+ of its governing (nearest-preceding) H1/H2 header (or None).
128
+
129
+ Paragraphs are maximal runs of consecutive non-blank, non-header lines,
130
+ separated by blank lines. Header lines of ANY level are never emitted as
131
+ paragraphs (headings are not a co-mention source). Only H1/H2 headers
132
+ update the governing header; H3-H6 leave it unchanged.
133
+ """
134
+ paragraphs: list[tuple[str | None, str]] = []
135
+ governing: str | None = None
136
+ buf: list[str] = []
137
+
138
+ def _flush() -> None:
139
+ if buf:
140
+ text = "\n".join(buf).strip()
141
+ if text:
142
+ paragraphs.append((governing, text))
143
+ buf.clear()
144
+
145
+ for raw_line in doc_text.splitlines():
146
+ m = _HEADER_RE.match(raw_line)
147
+ if m is not None:
148
+ # A header terminates the current paragraph; the header line
149
+ # itself is not body content.
150
+ _flush()
151
+ level = len(m.group(1))
152
+ header_text = _strip_closing_hashes(m.group(2)).strip()
153
+ if level <= 2:
154
+ governing = header_text if header_text else None
155
+ continue
156
+ if raw_line.strip() == "":
157
+ _flush()
158
+ continue
159
+ buf.append(raw_line)
160
+ _flush()
161
+ return paragraphs
162
+
163
+
164
+ # --------------------------------------------------------------------------
165
+ # Co-mention analysis
166
+ # --------------------------------------------------------------------------
167
+
168
+
169
+ def _make_excerpt(text: str, limit: int = _EXCERPT_LIMIT) -> str:
170
+ """Collapse whitespace and truncate for a stable, compact evidence quote."""
171
+ collapsed = re.sub(r"\s+", " ", text).strip()
172
+ if len(collapsed) > limit:
173
+ return collapsed[: limit - 3].rstrip() + "..."
174
+ return collapsed
175
+
176
+
177
+ def analyze(doc_text: str, skill_names: list[str]) -> dict:
178
+ """Compute qualifying co-mention pairs and their evidence.
179
+
180
+ Deterministic: same (doc_text, skill_names) → identical output.
181
+ """
182
+ names = sorted(set(skill_names))
183
+ patterns = {
184
+ n: re.compile(r"\b" + re.escape(n) + r"\b", re.IGNORECASE)
185
+ for n in names
186
+ }
187
+ paragraphs = parse_body_paragraphs(doc_text)
188
+
189
+ excluded_sections: set[str] = set()
190
+ # (a, b) -> list of evidence dicts, one per qualifying paragraph
191
+ pair_evidence: dict[tuple[str, str], list[dict]] = {}
192
+
193
+ for governing, text in paragraphs:
194
+ if governing is not None:
195
+ norm = normalize_header(governing)
196
+ if norm in EXCLUDED_HEADERS:
197
+ excluded_sections.add(norm)
198
+ continue
199
+ # names is sorted, so `present` is sorted → pairs are (a < b)
200
+ present = [n for n in names if patterns[n].search(text)]
201
+ if len(present) < 2:
202
+ continue
203
+ excerpt = _make_excerpt(text)
204
+ evidence_entry = {"header": governing, "excerpt": excerpt}
205
+ for i in range(len(present)):
206
+ for j in range(i + 1, len(present)):
207
+ key = (present[i], present[j])
208
+ pair_evidence.setdefault(key, []).append(evidence_entry)
209
+
210
+ pairs: list[dict] = []
211
+ for (a, b), evidence in pair_evidence.items():
212
+ if len(evidence) >= 2:
213
+ pairs.append({
214
+ "a": a,
215
+ "b": b,
216
+ "paragraph_count": len(evidence),
217
+ "evidence": evidence,
218
+ })
219
+ pairs.sort(key=lambda p: (-p["paragraph_count"], p["a"], p["b"]))
220
+
221
+ return {
222
+ "pairs": pairs,
223
+ "excluded_section_count": len(excluded_sections),
224
+ }
225
+
226
+
227
+ # --------------------------------------------------------------------------
228
+ # CLI
229
+ # --------------------------------------------------------------------------
230
+
231
+
232
+ def _read_source(source: str, label: str) -> str:
233
+ """Read text from a file path or stdin (if source == '-')."""
234
+ if source == "-":
235
+ try:
236
+ return sys.stdin.read()
237
+ except OSError as exc:
238
+ raise UserError(f"failed to read {label} from stdin: {exc}") from exc
239
+ path = Path(source)
240
+ if not path.is_file():
241
+ raise UserError(f"{label} file not found: {path}")
242
+ try:
243
+ return path.read_text(encoding="utf-8")
244
+ except OSError as exc:
245
+ raise UserError(f"failed to read {label} file {path}: {exc}") from exc
246
+
247
+
248
+ def parse_skills(raw_text: str) -> list[str]:
249
+ """Parse and validate the --skills JSON array of non-empty strings."""
250
+ try:
251
+ data = json.loads(raw_text)
252
+ except json.JSONDecodeError as exc:
253
+ raise UserError(f"malformed JSON in --skills input: {exc}") from exc
254
+ if not isinstance(data, list):
255
+ raise UserError(
256
+ "--skills input must be a JSON array of strings; "
257
+ f"got {type(data).__name__}"
258
+ )
259
+ names: list[str] = []
260
+ for idx, item in enumerate(data):
261
+ if not isinstance(item, str) or not item:
262
+ raise UserError(
263
+ f"--skills[{idx}] must be a non-empty string; got {item!r}"
264
+ )
265
+ names.append(item)
266
+ return names
267
+
268
+
269
+ def _cmd_comention(args: argparse.Namespace) -> int:
270
+ if args.doc == "-" and args.skills == "-":
271
+ raise UserError(
272
+ "--doc and --skills cannot both read from stdin ('-')"
273
+ )
274
+ # Read the file-backed input first so a single stdin source stays intact.
275
+ if args.doc == "-":
276
+ skills_text = _read_source(args.skills, "--skills")
277
+ doc_text = _read_source(args.doc, "--doc")
278
+ else:
279
+ doc_text = _read_source(args.doc, "--doc")
280
+ skills_text = _read_source(args.skills, "--skills")
281
+ skill_names = parse_skills(skills_text)
282
+
283
+ result = analyze(doc_text, skill_names)
284
+ if args.verbose:
285
+ print(
286
+ f"analyzed {len(set(skill_names))} distinct skill names; "
287
+ f"{len(result['pairs'])} qualifying pairs; "
288
+ f"{result['excluded_section_count']} excluded sections",
289
+ file=sys.stderr,
290
+ )
291
+ json.dump(result, sys.stdout, indent=2)
292
+ sys.stdout.write("\n")
293
+ return 0
294
+
295
+
296
+ def _build_parser() -> argparse.ArgumentParser:
297
+ parser = argparse.ArgumentParser(
298
+ prog="skf-comention-pairs",
299
+ description=(
300
+ "Compute compose-mode co-mention pairs from an architecture "
301
+ "document (detect-integrations.md §2 compose branch)."
302
+ ),
303
+ )
304
+ sub = parser.add_subparsers(dest="cmd", required=True)
305
+
306
+ p_cm = sub.add_parser(
307
+ "comention",
308
+ help=(
309
+ "emit qualifying co-mention pairs (>=2 body paragraphs, "
310
+ "word-boundary, section-filtered) as JSON"
311
+ ),
312
+ )
313
+ p_cm.add_argument(
314
+ "--doc",
315
+ required=True,
316
+ help="path to the architecture markdown document, or '-' for stdin",
317
+ )
318
+ p_cm.add_argument(
319
+ "--skills",
320
+ required=True,
321
+ help=(
322
+ "path to a JSON array of loaded skill names, or '-' for stdin. "
323
+ "Shape: [\"<skill>\", ...]"
324
+ ),
325
+ )
326
+ p_cm.add_argument(
327
+ "--verbose",
328
+ action="store_true",
329
+ help="print a one-line summary to stderr",
330
+ )
331
+ p_cm.set_defaults(func=_cmd_comention)
332
+
333
+ return parser
334
+
335
+
336
+ def main(argv: list[str] | None = None) -> int:
337
+ parser = _build_parser()
338
+ args = parser.parse_args(argv)
339
+ try:
340
+ return args.func(args)
341
+ except UserError as exc:
342
+ print(f"error: {exc}", file=sys.stderr)
343
+ return 1
344
+ except Exception as exc: # noqa: BLE001 — last-resort guard → exit 2
345
+ print(f"internal error: {exc}", file=sys.stderr)
346
+ return 2
347
+
348
+
349
+ if __name__ == "__main__":
350
+ raise SystemExit(main())