bmad-module-skill-forge 1.4.1 → 1.5.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 (231) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +0 -8
  3. package/docs/_data/pinned.yaml +1 -1
  4. package/docs/_internal/STABILITY.md +1 -1
  5. package/docs/architecture.md +2 -2
  6. package/docs/examples.md +1 -1
  7. package/docs/workflows.md +5 -5
  8. package/package.json +2 -2
  9. package/src/README.md +1 -1
  10. package/src/knowledge/ccc-bridge.md +12 -12
  11. package/src/knowledge/qmd-registry.md +5 -5
  12. package/src/shared/health-check.md +5 -5
  13. package/src/shared/references/description-guard-protocol.md +100 -0
  14. package/src/shared/references/output-contract-schema.md +1 -1
  15. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +6 -6
  16. package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +9 -3
  17. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +149 -0
  18. package/src/shared/scripts/schemas/skill-brief.v1.json +22 -2
  19. package/src/shared/scripts/skf-build-change-manifest.py +420 -0
  20. package/src/shared/scripts/skf-check-workspace-drift.py +321 -0
  21. package/src/shared/scripts/skf-compare-file-hashes.py +357 -0
  22. package/src/shared/scripts/skf-description-guard.py +359 -0
  23. package/src/shared/scripts/skf-detect-language.py +2 -2
  24. package/src/shared/scripts/skf-detect-scripts-assets.py +613 -0
  25. package/src/shared/scripts/skf-detect-tools.py +147 -16
  26. package/src/shared/scripts/skf-detect-workspaces.py +1 -1
  27. package/src/shared/scripts/skf-disqualify-candidates.py +576 -0
  28. package/src/shared/scripts/skf-emit-brief-result-envelope.py +3 -3
  29. package/src/shared/scripts/skf-emit-result-envelope.py +93 -9
  30. package/src/shared/scripts/skf-enumerate-stack-skills.py +514 -0
  31. package/src/shared/scripts/skf-forge-tier-rw.py +11 -11
  32. package/src/shared/scripts/skf-hash-content.py +284 -0
  33. package/src/shared/scripts/skf-load-provenance.py +295 -0
  34. package/src/shared/scripts/skf-manifest-ops.py +22 -6
  35. package/src/shared/scripts/skf-merge-ccc-exclusions.py +34 -4
  36. package/src/shared/scripts/skf-pair-intersect.py +250 -0
  37. package/src/shared/scripts/skf-provenance-gap-dispatch.py +433 -0
  38. package/src/shared/scripts/skf-qmd-classify-collections.py +42 -8
  39. package/src/shared/scripts/skf-recommend-scope-type.py +2 -2
  40. package/src/shared/scripts/skf-render-quick-metadata.py +2 -2
  41. package/src/shared/scripts/skf-resolve-authoritative-files.py +504 -0
  42. package/src/shared/scripts/skf-scan-manifests.py +738 -0
  43. package/src/shared/scripts/skf-scan-skill-md-structure.py +375 -0
  44. package/src/shared/scripts/skf-update-active-symlink.py +315 -0
  45. package/src/shared/scripts/skf-validate-brief-inputs.py +5 -5
  46. package/src/shared/scripts/skf-validate-brief-schema.py +348 -0
  47. package/src/shared/scripts/skf-write-skill-brief.py +61 -5
  48. package/src/skf-analyze-source/SKILL.md +69 -16
  49. package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
  50. package/src/skf-analyze-source/customize.toml +55 -0
  51. package/src/skf-analyze-source/{steps-c/step-01b-continue.md → references/continue.md} +12 -12
  52. package/src/skf-analyze-source/{steps-c/step-06-generate-briefs.md → references/generate-briefs.md} +29 -10
  53. package/src/skf-analyze-source/{steps-c/step-07-health-check.md → references/health-check.md} +5 -3
  54. package/src/skf-analyze-source/{steps-c/step-03-identify-units.md → references/identify-units.md} +48 -14
  55. package/src/skf-analyze-source/{steps-c/step-01-init.md → references/init.md} +32 -9
  56. package/src/skf-analyze-source/{steps-c/step-04-map-and-detect.md → references/map-and-detect.md} +52 -25
  57. package/src/skf-analyze-source/{steps-c/step-05-recommend.md → references/recommend.md} +15 -8
  58. package/src/skf-analyze-source/{steps-c/step-02-scan-project.md → references/scan-project.md} +16 -10
  59. package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -0
  60. package/src/skf-analyze-source/templates/analysis-report-template.md +6 -6
  61. package/src/skf-audit-skill/SKILL.md +66 -15
  62. package/src/skf-audit-skill/assets/drift-report-template.md +6 -5
  63. package/src/skf-audit-skill/customize.toml +49 -0
  64. package/src/skf-audit-skill/{steps-c/step-07-health-check.md → references/health-check.md} +5 -3
  65. package/src/skf-audit-skill/{steps-c/step-01-init.md → references/init.md} +63 -19
  66. package/src/skf-audit-skill/{steps-c/step-02-re-index.md → references/re-index.md} +11 -11
  67. package/src/skf-audit-skill/{steps-c/step-06-report.md → references/report.md} +25 -7
  68. package/src/skf-audit-skill/{steps-c/step-04-semantic-diff.md → references/semantic-diff.md} +6 -6
  69. package/src/skf-audit-skill/{steps-c/step-05-severity-classify.md → references/severity-classify.md} +5 -5
  70. package/src/skf-audit-skill/references/severity-rules.md +2 -0
  71. package/src/skf-audit-skill/{steps-c/step-03-structural-diff.md → references/structural-diff.md} +35 -17
  72. package/src/skf-brief-skill/SKILL.md +49 -19
  73. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  74. package/src/skf-brief-skill/assets/skill-brief-schema.md +32 -2
  75. package/src/skf-brief-skill/customize.toml +44 -0
  76. package/src/skf-brief-skill/{steps-c/step-02-analyze-target.md → references/analyze-target.md} +27 -15
  77. package/src/skf-brief-skill/{steps-c/step-04-confirm-brief.md → references/confirm-brief.md} +13 -10
  78. package/src/skf-brief-skill/references/draft-checkpoint.md +4 -4
  79. package/src/skf-brief-skill/{steps-c/step-01-gather-intent.md → references/gather-intent.md} +35 -29
  80. package/src/skf-brief-skill/references/headless-args.md +9 -9
  81. package/src/skf-brief-skill/references/headless-source-authority-detection.md +1 -1
  82. package/src/skf-brief-skill/{steps-c/step-06-health-check.md → references/health-check.md} +4 -2
  83. package/src/skf-brief-skill/references/portfolio-similarity-check.md +2 -2
  84. package/src/skf-brief-skill/references/qmd-collection-registration.md +2 -2
  85. package/src/skf-brief-skill/{steps-c/step-03-scope-definition.md → references/scope-definition.md} +27 -20
  86. package/src/skf-brief-skill/references/version-resolution.md +2 -2
  87. package/src/skf-brief-skill/{steps-c/step-05-write-brief.md → references/write-brief.md} +29 -16
  88. package/src/skf-create-skill/SKILL.md +25 -19
  89. package/src/skf-create-skill/assets/compile-assembly-rules.md +3 -3
  90. package/src/skf-create-skill/assets/skill-sections.md +5 -5
  91. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +11 -11
  92. package/src/skf-create-skill/customize.toml +35 -0
  93. package/src/skf-create-skill/references/authoritative-files-protocol.md +142 -0
  94. package/src/skf-create-skill/{steps-c/step-05-compile.md → references/compile.md} +16 -16
  95. package/src/skf-create-skill/{steps-c/step-03d-component-extraction.md → references/component-extraction.md} +16 -16
  96. package/src/skf-create-skill/{steps-c/step-02-ecosystem-check.md → references/ecosystem-check.md} +4 -4
  97. package/src/skf-create-skill/{steps-c/step-04-enrich.md → references/enrich.md} +3 -3
  98. package/src/skf-create-skill/{steps-c/step-03-extract.md → references/extract.md} +52 -120
  99. package/src/skf-create-skill/references/extraction-patterns.md +10 -8
  100. package/src/skf-create-skill/{steps-c/step-07-generate-artifacts.md → references/generate-artifacts.md} +11 -11
  101. package/src/skf-create-skill/{steps-c/step-09-health-check.md → references/health-check.md} +4 -2
  102. package/src/skf-create-skill/{steps-c/step-01-load-brief.md → references/load-brief.md} +40 -32
  103. package/src/skf-create-skill/{steps-c/step-08-report.md → references/report.md} +8 -8
  104. package/src/skf-create-skill/references/source-resolution-protocols.md +6 -6
  105. package/src/skf-create-skill/{steps-c/sub/step-02b-ccc-discover.md → references/sub/ccc-discover.md} +5 -5
  106. package/src/skf-create-skill/{steps-c/sub/step-03c-fetch-docs.md → references/sub/fetch-docs.md} +28 -12
  107. package/src/skf-create-skill/{steps-c/sub/step-03b-fetch-temporal.md → references/sub/fetch-temporal.md} +81 -50
  108. package/src/skf-create-skill/{steps-c/step-06-validate.md → references/validate.md} +48 -28
  109. package/src/skf-create-stack-skill/SKILL.md +76 -17
  110. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +102 -0
  111. package/src/skf-create-stack-skill/assets/stack-skill-template.md +1 -1
  112. package/src/skf-create-stack-skill/customize.toml +46 -0
  113. package/src/skf-create-stack-skill/{steps-c/step-06-compile-stack.md → references/compile-stack.md} +6 -5
  114. package/src/skf-create-stack-skill/references/compose-mode-rules.md +3 -1
  115. package/src/skf-create-stack-skill/{steps-c/step-05-detect-integrations.md → references/detect-integrations.md} +37 -11
  116. package/src/skf-create-stack-skill/{steps-c/step-02-detect-manifests.md → references/detect-manifests.md} +32 -28
  117. package/src/skf-create-stack-skill/{steps-c/step-07-generate-output.md → references/generate-output.md} +11 -85
  118. package/src/skf-create-stack-skill/{steps-c/step-10-health-check.md → references/health-check.md} +4 -2
  119. package/src/skf-create-stack-skill/{steps-c/step-01-init.md → references/init.md} +70 -4
  120. package/src/skf-create-stack-skill/references/integration-patterns.md +2 -0
  121. package/src/skf-create-stack-skill/references/manifest-patterns.md +2 -0
  122. package/src/skf-create-stack-skill/{steps-c/step-04-parallel-extract.md → references/parallel-extract.md} +44 -17
  123. package/src/skf-create-stack-skill/{steps-c/step-03-rank-and-confirm.md → references/rank-and-confirm.md} +5 -4
  124. package/src/skf-create-stack-skill/{steps-c/step-09-report.md → references/report.md} +3 -3
  125. package/src/skf-create-stack-skill/{steps-c/step-08-validate.md → references/validate.md} +6 -6
  126. package/src/skf-drop-skill/SKILL.md +84 -12
  127. package/src/skf-drop-skill/customize.toml +57 -0
  128. package/src/skf-drop-skill/{steps-c/step-02-execute.md → references/execute.md} +49 -34
  129. package/src/skf-drop-skill/{steps-c/step-04-health-check.md → references/health-check.md} +6 -4
  130. package/src/skf-drop-skill/{steps-c/step-03-report.md → references/report.md} +13 -5
  131. package/src/skf-drop-skill/{steps-c/step-01-select.md → references/select.md} +63 -15
  132. package/src/skf-export-skill/SKILL.md +85 -16
  133. package/src/skf-export-skill/assets/managed-section-format.md +3 -3
  134. package/src/skf-export-skill/assets/snippet-format.md +2 -2
  135. package/src/skf-export-skill/customize.toml +49 -0
  136. package/src/skf-export-skill/{steps-c/step-03-generate-snippet.md → references/generate-snippet.md} +11 -11
  137. package/src/skf-export-skill/{steps-c/step-07-health-check.md → references/health-check.md} +6 -4
  138. package/src/skf-export-skill/{steps-c/step-01-load-skill.md → references/load-skill.md} +11 -49
  139. package/src/skf-export-skill/references/manifest-rebuild.md +68 -0
  140. package/src/skf-export-skill/references/multi-skill-mode.md +38 -0
  141. package/src/skf-export-skill/references/orphan-context-detection.md +75 -0
  142. package/src/skf-export-skill/references/orphan-row-detection.md +102 -0
  143. package/src/skf-export-skill/{steps-c/step-02-package.md → references/package.md} +7 -7
  144. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +79 -0
  145. package/src/skf-export-skill/{steps-c/step-06-summary.md → references/summary.md} +23 -11
  146. package/src/skf-export-skill/{steps-c/step-05-token-report.md → references/token-report.md} +5 -5
  147. package/src/skf-export-skill/{steps-c/step-04-update-context.md → references/update-context.md} +109 -113
  148. package/src/skf-quick-skill/SKILL.md +63 -123
  149. package/src/skf-quick-skill/customize.toml +44 -0
  150. package/src/skf-quick-skill/references/batch-mode.md +102 -0
  151. package/src/skf-quick-skill/{steps-c/step-04-compile.md → references/compile.md} +10 -10
  152. package/src/skf-quick-skill/{steps-c/step-02-ecosystem-check.md → references/ecosystem-check.md} +5 -5
  153. package/src/skf-quick-skill/{steps-c/step-06-finalize.md → references/finalize.md} +7 -7
  154. package/src/skf-quick-skill/{steps-c/step-07-health-check.md → references/health-check.md} +3 -3
  155. package/src/skf-quick-skill/{steps-c/step-03-quick-extract.md → references/quick-extract.md} +5 -5
  156. package/src/skf-quick-skill/references/registry-resolution.md +2 -0
  157. package/src/skf-quick-skill/{steps-c/step-01-resolve-target.md → references/resolve-target.md} +14 -10
  158. package/src/skf-quick-skill/{steps-c/step-05-write-and-validate.md → references/write-and-validate.md} +4 -4
  159. package/src/skf-refine-architecture/SKILL.md +86 -16
  160. package/src/skf-refine-architecture/customize.toml +49 -0
  161. package/src/skf-refine-architecture/{steps-c/step-05-compile.md → references/compile.md} +4 -4
  162. package/src/skf-refine-architecture/{steps-c/step-02-gap-analysis.md → references/gap-analysis.md} +5 -5
  163. package/src/skf-refine-architecture/{steps-c/step-07-health-check.md → references/health-check.md} +6 -4
  164. package/src/skf-refine-architecture/{steps-c/step-04-improvements.md → references/improvements.md} +5 -5
  165. package/src/skf-refine-architecture/references/init.md +144 -0
  166. package/src/skf-refine-architecture/{steps-c/step-03-issue-detection.md → references/issue-detection.md} +5 -5
  167. package/src/skf-refine-architecture/references/refinement-rules.md +2 -0
  168. package/src/skf-refine-architecture/{steps-c/step-06-report.md → references/report.md} +14 -5
  169. package/src/skf-rename-skill/SKILL.md +82 -12
  170. package/src/skf-rename-skill/customize.toml +52 -0
  171. package/src/skf-rename-skill/{steps-c/step-02-execute.md → references/execute.md} +94 -99
  172. package/src/skf-rename-skill/references/health-check.md +30 -0
  173. package/src/skf-rename-skill/references/rebuild-context.md +110 -0
  174. package/src/skf-rename-skill/{steps-c/step-03-report.md → references/report.md} +13 -5
  175. package/src/skf-rename-skill/{steps-c/step-01-select.md → references/select.md} +59 -20
  176. package/src/skf-setup/SKILL.md +35 -35
  177. package/src/skf-setup/customize.toml +33 -0
  178. package/src/skf-setup/{steps-c/step-03-auto-index.md → references/auto-index.md} +10 -18
  179. package/src/skf-setup/{steps-c/step-01b-ccc-index.md → references/ccc-index.md} +16 -31
  180. package/src/skf-setup/{steps-c/step-01-detect-and-tier.md → references/detect-and-tier.md} +23 -10
  181. package/src/skf-setup/{steps-c/step-05-health-check.md → references/health-check.md} +1 -1
  182. package/src/skf-setup/{steps-c/step-04-report.md → references/report.md} +21 -19
  183. package/src/skf-setup/references/tier-rules.md +1 -1
  184. package/src/skf-setup/{steps-c/step-02-write-config.md → references/write-config.md} +14 -13
  185. package/src/skf-test-skill/SKILL.md +77 -15
  186. package/src/skf-test-skill/customize.toml +54 -0
  187. package/src/skf-test-skill/{steps-c/step-04-coherence-check.md → references/coherence-check.md} +37 -73
  188. package/src/skf-test-skill/{steps-c/step-03-coverage-check.md → references/coverage-check.md} +16 -39
  189. package/src/skf-test-skill/{steps-c/step-02-detect-mode.md → references/detect-mode.md} +4 -26
  190. package/src/skf-test-skill/{steps-c/step-04b-external-validators.md → references/external-validators.md} +6 -28
  191. package/src/skf-test-skill/references/health-check.md +14 -0
  192. package/src/skf-test-skill/{steps-c/step-01-init.md → references/init.md} +21 -37
  193. package/src/skf-test-skill/references/migration-section-rules.md +4 -2
  194. package/src/skf-test-skill/{steps-c/step-06-report.md → references/report.md} +41 -51
  195. package/src/skf-test-skill/{steps-c/step-05-score.md → references/score.md} +26 -43
  196. package/src/skf-test-skill/references/scoring-rules.md +8 -6
  197. package/src/skf-test-skill/references/source-access-protocol.md +7 -5
  198. package/src/skf-test-skill/scripts/compute-score.py +88 -17
  199. package/src/skf-test-skill/templates/test-report-template.md +13 -13
  200. package/src/skf-update-skill/SKILL.md +23 -14
  201. package/src/skf-update-skill/customize.toml +44 -0
  202. package/src/skf-update-skill/{steps-c/step-02-detect-changes.md → references/detect-changes.md} +176 -68
  203. package/src/skf-update-skill/references/health-check.md +32 -0
  204. package/src/skf-update-skill/{steps-c/step-01-init.md → references/init.md} +56 -8
  205. package/src/skf-update-skill/references/manual-section-rules.md +4 -0
  206. package/src/skf-update-skill/references/merge-conflict-rules.md +4 -0
  207. package/src/skf-update-skill/{steps-c/step-04-merge.md → references/merge.md} +8 -8
  208. package/src/skf-update-skill/{steps-c/step-03-re-extract.md → references/re-extract.md} +53 -44
  209. package/src/skf-update-skill/references/remote-source-resolution.md +5 -1
  210. package/src/skf-update-skill/references/report.md +214 -0
  211. package/src/skf-update-skill/{steps-c/step-05-validate.md → references/validate.md} +5 -5
  212. package/src/skf-update-skill/{steps-c/step-06-write.md → references/write.md} +73 -51
  213. package/src/skf-verify-stack/SKILL.md +89 -17
  214. package/src/skf-verify-stack/assets/feasibility-report-template.md +4 -4
  215. package/src/skf-verify-stack/customize.toml +50 -0
  216. package/src/skf-verify-stack/references/coverage-patterns.md +2 -2
  217. package/src/skf-verify-stack/{steps-c/step-02-coverage.md → references/coverage.md} +13 -9
  218. package/src/skf-verify-stack/{steps-c/step-07-health-check.md → references/health-check.md} +7 -5
  219. package/src/skf-verify-stack/references/init.md +170 -0
  220. package/src/skf-verify-stack/references/integration-verification-rules.md +1 -1
  221. package/src/skf-verify-stack/{steps-c/step-03-integrations.md → references/integrations.md} +18 -14
  222. package/src/skf-verify-stack/{steps-c/step-06-report.md → references/report.md} +27 -42
  223. package/src/skf-verify-stack/{steps-c/step-04-requirements.md → references/requirements.md} +13 -9
  224. package/src/skf-verify-stack/{steps-c/step-05-synthesize.md → references/synthesize.md} +14 -10
  225. package/tools/validate-docs-drift.js +1 -1
  226. package/src/skf-refine-architecture/steps-c/step-01-init.md +0 -136
  227. package/src/skf-rename-skill/steps-c/step-04-health-check.md +0 -22
  228. package/src/skf-test-skill/steps-c/step-07-health-check.md +0 -25
  229. package/src/skf-update-skill/steps-c/step-07-report.md +0 -148
  230. package/src/skf-update-skill/steps-c/step-08-health-check.md +0 -22
  231. package/src/skf-verify-stack/steps-c/step-01-init.md +0 -178
@@ -1,19 +1,19 @@
1
1
  # /// script
2
2
  # requires-python = ">=3.10"
3
- # dependencies = []
3
+ # dependencies = ["pyyaml"]
4
4
  # ///
5
5
  """SKF Detect Tools — Parallel tool detection + tier calculation for skf-setup.
6
6
 
7
- Replaces the prose-driven tool-detection sequence in `src/skf-setup/steps-c/
8
- step-01-detect-and-tier.md` §3-§8b with one Python invocation. Probes ast-grep,
7
+ Replaces the prose-driven tool-detection sequence in `src/skf-setup/references/
8
+ detect-and-tier.md` §3-§8b with one Python invocation. Probes ast-grep,
9
9
  gh, qmd, and ccc concurrently, applies the 4-rule tier decision table (see
10
10
  `src/skf-setup/references/tier-rules.md`), evaluates --tier-override (with
11
11
  sanity check) and --require-tier (with tool-prerequisite check independent of
12
12
  the tier name), and emits one JSON document on stdout.
13
13
 
14
14
  Schema documented in DETECT_OUTPUT_SCHEMA at the bottom of this docstring.
15
- The output is consumed by step-01 prose, step-02 (forge-tier.yaml writer),
16
- and step-04 (status report + envelope).
15
+ The output is consumed by step 1 prose, step 2 (forge-tier.yaml writer),
16
+ and step 4 (status report + envelope).
17
17
 
18
18
  Tier rules (first match wins):
19
19
  Deep = ast-grep + gh-cli + qmd (all healthy)
@@ -21,12 +21,12 @@ Tier rules (first match wins):
21
21
  Forge = ast-grep
22
22
  Quick = otherwise
23
23
 
24
- CCC verification is two-step (matches step-01 §7):
24
+ CCC verification is two-step (matches step 1 §7):
25
25
  Step A: `ccc --help` exits 0 AND output contains "CocoIndex Code" marker.
26
26
  Rejects code2prompt-aliased-as-ccc and similar PATH shadowing.
27
27
  Step B: `ccc doctor` succeeds (daemon healthy).
28
28
 
29
- QMD verification is two-step (matches step-01 §5 post-PR-#248):
29
+ QMD verification is two-step (matches step 1 §5 post-PR-#248):
30
30
  Step A: `qmd --version` exits 0 (binary identity, falls back to --help).
31
31
  Step B: `qmd status` succeeds (daemon healthy).
32
32
  qmd_status: "absent" | "daemon_stopped" | "healthy" — affects climb hint.
@@ -88,13 +88,26 @@ from __future__ import annotations
88
88
  import argparse
89
89
  import json
90
90
  import os
91
+ import shutil
91
92
  import subprocess
92
93
  import sys
94
+ import tempfile
93
95
  from concurrent.futures import ThreadPoolExecutor
94
96
 
95
97
 
96
98
  VALID_TIERS = ("Quick", "Forge", "Forge+", "Deep")
97
99
  PROBE_TIMEOUT_SEC = 8 # per-tool subprocess.run timeout
100
+ # Prefer the OS `timeout(1)` utility to bound each probe. A daemon-backed
101
+ # tool (e.g. `qmd status`) can block in an uninterruptible syscall against
102
+ # its daemon; `subprocess.run`'s own post-timeout `process.wait()` is
103
+ # unbounded and never returns in that case, hanging the whole detector.
104
+ # `timeout --kill-after` reaps the child (and its session) at the OS level,
105
+ # so Python's wait always returns. POSIX-only: Windows' `timeout.exe` is an
106
+ # unrelated builtin (waits for input; cannot run a command), and the
107
+ # uninterruptible-wait hang is itself POSIX-specific (Windows uses a
108
+ # forcible TerminateProcess), so on Windows we fall back to subprocess.run's
109
+ # own timeout. None when unavailable.
110
+ _TIMEOUT_BIN = shutil.which("timeout") if os.name == "posix" else None
98
111
  CCC_IDENTITY_MARKER = "cocoindex code" # case-insensitive substring
99
112
 
100
113
 
@@ -115,16 +128,45 @@ def _run(cmd: list[str], timeout: int = PROBE_TIMEOUT_SEC) -> tuple[int, str, st
115
128
  Treats every failure mode (FileNotFoundError, TimeoutExpired, OSError,
116
129
  CalledProcessError) as a failed probe — returns rc=127 and an empty
117
130
  stdout/stderr. Tool detection should never crash the workflow.
131
+
132
+ The child is wrapped in the OS `timeout(1)` utility (when available) so a
133
+ daemon-backed probe blocked in an uninterruptible syscall (e.g.
134
+ `qmd status` waiting on its daemon socket) is reaped at the OS level —
135
+ `subprocess.run`'s own post-timeout `process.wait()` is unbounded and
136
+ would otherwise never return, hanging the whole detector. Child
137
+ stdout/stderr are redirected to temp files (no pipe to drain), and
138
+ `start_new_session=True` isolates the child's process group; the
139
+ Python-level timeout is a secondary net set slightly above the OS one.
118
140
  """
141
+ if _TIMEOUT_BIN:
142
+ run_cmd = [_TIMEOUT_BIN, "--kill-after=2", str(timeout), *cmd]
143
+ py_timeout = timeout + 5
144
+ else:
145
+ run_cmd = cmd
146
+ py_timeout = timeout
119
147
  try:
120
- result = subprocess.run(
121
- cmd,
122
- capture_output=True,
123
- text=True,
124
- timeout=timeout,
125
- check=False,
126
- )
127
- return result.returncode, result.stdout or "", result.stderr or ""
148
+ with tempfile.TemporaryFile() as out_f, tempfile.TemporaryFile() as err_f:
149
+ result = subprocess.run(
150
+ run_cmd,
151
+ stdin=subprocess.DEVNULL,
152
+ stdout=out_f,
153
+ stderr=err_f,
154
+ timeout=py_timeout,
155
+ check=False,
156
+ start_new_session=True,
157
+ )
158
+ # Mocked unit tests patch `subprocess.run` to return a fake with
159
+ # `.stdout`/`.stderr` strings set; real runs redirect to the temp
160
+ # files (so `result.stdout` is None — read the files instead).
161
+ stdout = result.stdout
162
+ if stdout is None:
163
+ out_f.seek(0)
164
+ stdout = out_f.read().decode("utf-8", "replace")
165
+ stderr = result.stderr
166
+ if stderr is None:
167
+ err_f.seek(0)
168
+ stderr = err_f.read().decode("utf-8", "replace")
169
+ return result.returncode, stdout or "", stderr or ""
128
170
  except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
129
171
  return 127, "", ""
130
172
 
@@ -251,6 +293,50 @@ def tier_prerequisites_met(tier: str, tools: dict) -> tuple[bool, list[str]]:
251
293
  return (len(missing) == 0, missing)
252
294
 
253
295
 
296
+ def read_prior_state(prior_state_path) -> dict:
297
+ """Read forge-tier.yaml from a previous run, return prior tier / tools / detection_date.
298
+
299
+ Returns a flat dict (always present, keys always set) so callers don't
300
+ branch on missing-file vs empty-file vs malformed-file — the script owns
301
+ that classification. On any read failure or absent file, returns the
302
+ "first-run" shape (everything null/empty). The CCC freshness fields are
303
+ surfaced separately so the caller doesn't reparse YAML in prose.
304
+ """
305
+ empty = {
306
+ "previous_tier": None,
307
+ "previous_detection_date": None,
308
+ "previous_tools": {},
309
+ "previous_ccc_index_status": None,
310
+ "previous_ccc_indexed_path": None,
311
+ "previous_ccc_last_indexed": None,
312
+ "previous_ccc_staleness_threshold_hours": None,
313
+ }
314
+ if not prior_state_path:
315
+ return empty
316
+ try:
317
+ import yaml # local import — only needed when --prior-state-from is used
318
+ from pathlib import Path as _P
319
+ p = _P(prior_state_path)
320
+ if not p.exists():
321
+ return empty
322
+ data = yaml.safe_load(p.read_text(encoding="utf-8")) or {}
323
+ except Exception:
324
+ return empty
325
+
326
+ tools_map = data.get("tools") if isinstance(data.get("tools"), dict) else {}
327
+ ccc_index = data.get("ccc_index") if isinstance(data.get("ccc_index"), dict) else {}
328
+
329
+ return {
330
+ "previous_tier": data.get("tier") if data.get("tier") in VALID_TIERS else None,
331
+ "previous_detection_date": data.get("tier_detected_at"),
332
+ "previous_tools": tools_map,
333
+ "previous_ccc_index_status": ccc_index.get("status"),
334
+ "previous_ccc_indexed_path": ccc_index.get("indexed_path"),
335
+ "previous_ccc_last_indexed": ccc_index.get("last_indexed"),
336
+ "previous_ccc_staleness_threshold_hours": ccc_index.get("staleness_threshold_hours"),
337
+ }
338
+
339
+
254
340
  def detect(args: argparse.Namespace) -> dict:
255
341
  tools: dict = {}
256
342
  with ThreadPoolExecutor(max_workers=4) as ex:
@@ -302,6 +388,9 @@ def detect(args: argparse.Namespace) -> dict:
302
388
  else:
303
389
  require_satisfied = None
304
390
 
391
+ prior = read_prior_state(getattr(args, "prior_state_from", None))
392
+ deltas = compute_deltas(tools, prior, calculated)
393
+
305
394
  return {
306
395
  "tools": tools,
307
396
  "tier": {
@@ -320,6 +409,38 @@ def detect(args: argparse.Namespace) -> dict:
320
409
  "satisfied": require_satisfied,
321
410
  "missing_tools": require_missing,
322
411
  },
412
+ "prior": prior,
413
+ "deltas": deltas,
414
+ }
415
+
416
+
417
+ def compute_deltas(current_tools: dict, prior: dict, calculated_tier: str) -> dict:
418
+ """Compute re-run deltas (tools added/removed, tier_changed, ccc_index_is_fresh).
419
+
420
+ Removes ~80 tokens of LLM-side set arithmetic + string compare from the
421
+ interactive banner branch in references/report.md. First-run convention
422
+ (prior.previous_tier is null): tools_added = currently-available tools,
423
+ tools_removed = [], tier_changed = false.
424
+ """
425
+ tool_keys = ("ast_grep", "gh_cli", "qmd", "ccc")
426
+ cur_avail = {k: bool((current_tools.get(k) or {}).get("available")) for k in tool_keys}
427
+
428
+ prev_tools_raw = prior.get("previous_tools") or {}
429
+ if prev_tools_raw:
430
+ prev_avail = {k: bool(prev_tools_raw.get(k, False)) for k in tool_keys}
431
+ tools_added = sorted(k for k in tool_keys if cur_avail[k] and not prev_avail[k])
432
+ tools_removed = sorted(k for k in tool_keys if prev_avail[k] and not cur_avail[k])
433
+ else:
434
+ tools_added = sorted(k for k in tool_keys if cur_avail[k])
435
+ tools_removed = []
436
+
437
+ prior_tier = prior.get("previous_tier")
438
+ tier_changed = bool(prior_tier and prior_tier != calculated_tier)
439
+
440
+ return {
441
+ "tools_added": tools_added,
442
+ "tools_removed": tools_removed,
443
+ "tier_changed": tier_changed,
323
444
  }
324
445
 
325
446
 
@@ -332,7 +453,7 @@ def main() -> None:
332
453
  "--tier-override",
333
454
  default=None,
334
455
  help="Force a specific tier (must be one of Quick, Forge, Forge+, Deep — case-sensitive)."
335
- " Invalid values are flagged in the output rather than rejected, so step-04 can"
456
+ " Invalid values are flagged in the output rather than rejected, so step 4 can"
336
457
  " surface the warning to the user.",
337
458
  )
338
459
  parser.add_argument(
@@ -349,6 +470,16 @@ def main() -> None:
349
470
  help="Environment variable name to check for security-scan availability"
350
471
  " (informational only — does NOT affect tier). Default: SNYK_TOKEN.",
351
472
  )
473
+ parser.add_argument(
474
+ "--prior-state-from",
475
+ default=None,
476
+ help="Optional path to a previous-run forge-tier.yaml. When provided,"
477
+ " the script reads it and surfaces previous_tier, previous_tools,"
478
+ " previous_detection_date, and previous_ccc_* fields under the 'prior'"
479
+ " key — removing YAML-parse responsibility from the step prompt."
480
+ " Missing file or unreadable YAML returns the first-run shape (all"
481
+ " null/empty) without erroring.",
482
+ )
352
483
  args = parser.parse_args()
353
484
 
354
485
  payload = detect(args)
@@ -8,7 +8,7 @@ Takes a JSON payload on stdin describing a target repo's file tree plus the
8
8
  contents of a small set of root manifests, and returns whether a workspace
9
9
  layout is present, which manifest kind drives it, and the list of resolved
10
10
  workspaces. The helper does NO file I/O — the caller (typically
11
- skf-brief-skill step-02 §1b) fetches files via `gh api` / local filesystem
11
+ skf-brief-skill step 2 §1b) fetches files via `gh api` / local filesystem
12
12
  and pipes the relevant content in.
13
13
 
14
14
  Detection runs in priority order; the first matching detector wins: