bmad-module-skill-forge 2.2.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (437) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +49 -49
  3. package/docs/404.md +10 -8
  4. package/docs/_data/pinned.yaml +22 -13
  5. package/docs/_internal/STABILITY.md +58 -35
  6. package/docs/agents.md +39 -53
  7. package/docs/architecture.md +65 -51
  8. package/docs/bmad-synergy.md +53 -51
  9. package/docs/campaign.md +127 -75
  10. package/docs/concepts.md +34 -28
  11. package/docs/examples.md +70 -68
  12. package/docs/forge-auto.md +34 -31
  13. package/docs/getting-started.md +118 -57
  14. package/docs/how-it-works.md +25 -21
  15. package/docs/index.md +15 -15
  16. package/docs/skill-model.md +70 -59
  17. package/docs/troubleshooting.md +143 -18
  18. package/docs/verifying-a-skill.md +87 -57
  19. package/docs/why-skf.md +24 -22
  20. package/docs/workflows.md +274 -110
  21. package/package.json +18 -10
  22. package/src/forger/preferences.yaml +8 -1
  23. package/src/knowledge/agentskills-spec.md +2 -0
  24. package/src/knowledge/ccc-bridge.md +40 -24
  25. package/src/knowledge/confidence-tiers.md +9 -5
  26. package/src/knowledge/overview.md +3 -3
  27. package/src/knowledge/progressive-capability.md +8 -8
  28. package/src/knowledge/provenance-tracking.md +9 -5
  29. package/src/knowledge/qmd-registry.md +3 -3
  30. package/src/knowledge/skf-knowledge-index.csv +2 -2
  31. package/src/knowledge/skill-lifecycle.md +18 -11
  32. package/src/knowledge/split-body-strategy.md +0 -3
  33. package/src/knowledge/tool-resolution.md +4 -2
  34. package/src/knowledge/version-paths.md +101 -15
  35. package/src/module-help.csv +3 -3
  36. package/src/module.yaml +10 -0
  37. package/src/shared/data/ast-grep-recipes.yaml +608 -0
  38. package/src/shared/data/ide-context-files.json +30 -0
  39. package/src/shared/health-check.md +31 -59
  40. package/src/shared/references/description-guard-protocol.md +12 -9
  41. package/src/shared/references/feasibility-report-schema.md +19 -13
  42. package/src/shared/references/headless-gate-convention.md +55 -3
  43. package/src/shared/references/output-contract-schema.md +19 -8
  44. package/src/shared/references/pipeline-contracts.md +94 -29
  45. package/src/shared/references/tessl-review.md +77 -0
  46. package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
  47. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
  48. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
  49. package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
  50. package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
  51. package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
  52. package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
  53. package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
  54. package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
  55. package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
  56. package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
  57. package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
  58. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
  59. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
  60. package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
  61. package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
  62. package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
  63. package/src/shared/scripts/skf-atomic-write.py +233 -64
  64. package/src/shared/scripts/skf-build-change-manifest.py +2753 -28
  65. package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
  66. package/src/shared/scripts/skf-check-unit-records.py +184 -0
  67. package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
  68. package/src/shared/scripts/skf-classify-changed-files.py +772 -0
  69. package/src/shared/scripts/skf-comention-pairs.py +1333 -123
  70. package/src/shared/scripts/skf-compare-file-hashes.py +267 -105
  71. package/src/shared/scripts/skf-count-imports.py +1867 -0
  72. package/src/shared/scripts/skf-count-tokens.py +1 -1
  73. package/src/shared/scripts/skf-derive-assembly-shape.py +21 -3
  74. package/src/shared/scripts/skf-description-guard.py +99 -11
  75. package/src/shared/scripts/skf-detect-docs.py +694 -2
  76. package/src/shared/scripts/skf-detect-language.py +486 -107
  77. package/src/shared/scripts/skf-detect-registry.py +887 -0
  78. package/src/shared/scripts/skf-detect-scripts-assets.py +197 -55
  79. package/src/shared/scripts/skf-detect-tools.py +209 -27
  80. package/src/shared/scripts/skf-detect-workspaces.py +506 -30
  81. package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
  82. package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
  83. package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
  84. package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
  85. package/src/shared/scripts/skf-extract-public-api.py +5247 -96
  86. package/src/shared/scripts/skf-extraction-inventory.py +1280 -0
  87. package/src/shared/scripts/skf-extraction-snapshot.py +664 -0
  88. package/src/shared/scripts/skf-fetch-temporal.py +674 -0
  89. package/src/shared/scripts/skf-find-cycles.py +97 -29
  90. package/src/shared/scripts/skf-find-test-report.py +476 -0
  91. package/src/shared/scripts/skf-forge-tier-rw.py +469 -205
  92. package/src/shared/scripts/skf-github-fetch.py +365 -0
  93. package/src/shared/scripts/skf-github-probe.py +700 -0
  94. package/src/shared/scripts/skf-hash-content.py +180 -20
  95. package/src/shared/scripts/skf-language-corpora.py +1 -1
  96. package/src/shared/scripts/skf-load-provenance.py +161 -27
  97. package/src/shared/scripts/skf-manifest-ops.py +1 -1
  98. package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
  99. package/src/shared/scripts/skf-merge-doc-urls.py +143 -20
  100. package/src/shared/scripts/skf-names-present.py +221 -0
  101. package/src/shared/scripts/skf-pair-intersect.py +119 -28
  102. package/src/shared/scripts/skf-parse-gaps.py +1384 -0
  103. package/src/shared/scripts/skf-preapply.py +1 -1
  104. package/src/shared/scripts/skf-preflight.py +176 -28
  105. package/src/shared/scripts/skf-promote-staged.py +430 -0
  106. package/src/shared/scripts/skf-provenance-gap-dispatch.py +64 -8
  107. package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
  108. package/src/shared/scripts/skf-quick-batch.py +774 -0
  109. package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
  110. package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
  111. package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
  112. package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
  113. package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
  114. package/src/shared/scripts/skf-resolve-authoritative-files.py +155 -14
  115. package/src/shared/scripts/skf-resolve-package.py +523 -106
  116. package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
  117. package/src/shared/scripts/skf-run-lock.py +577 -0
  118. package/src/shared/scripts/skf-scan-manifests.py +998 -57
  119. package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
  120. package/src/shared/scripts/skf-severity-classify.py +712 -108
  121. package/src/shared/scripts/skf-shape-detect.py +298 -204
  122. package/src/shared/scripts/skf-shard-body.py +9 -8
  123. package/src/shared/scripts/skf-skill-inventory.py +2412 -89
  124. package/src/shared/scripts/skf-skills-module.py +472 -0
  125. package/src/shared/scripts/skf-source-tree.py +1796 -0
  126. package/src/shared/scripts/skf-structural-diff.py +787 -121
  127. package/src/shared/scripts/skf-tessl-review.py +706 -0
  128. package/src/shared/scripts/skf-update-active-symlink.py +120 -67
  129. package/src/shared/scripts/skf-update-run-state.py +624 -0
  130. package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
  131. package/src/shared/scripts/skf-validate-brief-schema.py +23 -2
  132. package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
  133. package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
  134. package/src/shared/scripts/skf-validate-output.py +712 -64
  135. package/src/shared/scripts/skf-validate-pins.py +1 -1
  136. package/src/shared/scripts/skf-verify-no-trace.py +116 -10
  137. package/src/shared/scripts/skf-verify-provenance-completeness.py +3945 -152
  138. package/src/shared/scripts/skf-write-skill-brief.py +406 -25
  139. package/src/shared/tool-requirements.yaml +201 -0
  140. package/src/skf-analyze-source/SKILL.md +41 -36
  141. package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
  142. package/src/skf-analyze-source/customize.toml +36 -24
  143. package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
  144. package/src/skf-analyze-source/references/continue.md +2 -13
  145. package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
  146. package/src/skf-analyze-source/references/generate-briefs.md +121 -55
  147. package/src/skf-analyze-source/references/headless-contract.md +42 -15
  148. package/src/skf-analyze-source/references/identify-units.md +96 -110
  149. package/src/skf-analyze-source/references/init.md +73 -71
  150. package/src/skf-analyze-source/references/map-and-detect.md +123 -105
  151. package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
  152. package/src/skf-analyze-source/references/recommend.md +51 -70
  153. package/src/skf-analyze-source/references/scan-project.md +60 -84
  154. package/src/skf-analyze-source/references/scan-root.md +39 -0
  155. package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
  156. package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
  157. package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
  158. package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
  159. package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
  160. package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
  161. package/src/skf-audit-skill/SKILL.md +43 -51
  162. package/src/skf-audit-skill/assets/drift-report-template.md +28 -3
  163. package/src/skf-audit-skill/customize.toml +41 -20
  164. package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
  165. package/src/skf-audit-skill/references/doc-drift.md +141 -0
  166. package/src/skf-audit-skill/references/headless-contract.md +29 -0
  167. package/src/skf-audit-skill/references/health-check.md +2 -0
  168. package/src/skf-audit-skill/references/init.md +149 -210
  169. package/src/skf-audit-skill/references/re-index.md +103 -110
  170. package/src/skf-audit-skill/references/report.md +96 -33
  171. package/src/skf-audit-skill/references/semantic-diff.md +34 -25
  172. package/src/skf-audit-skill/references/severity-classify.md +79 -65
  173. package/src/skf-audit-skill/references/severity-rules.md +15 -6
  174. package/src/skf-audit-skill/references/structural-diff.md +80 -90
  175. package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
  176. package/src/skf-audit-skill/scripts/render-drift-tables.py +712 -0
  177. package/src/skf-brief-skill/SKILL.md +37 -26
  178. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  179. package/src/skf-brief-skill/assets/scope-templates.md +18 -25
  180. package/src/skf-brief-skill/assets/skill-brief-schema.md +30 -23
  181. package/src/skf-brief-skill/customize.toml +48 -20
  182. package/src/skf-brief-skill/references/analyze-target.md +144 -127
  183. package/src/skf-brief-skill/references/confirm-brief.md +11 -9
  184. package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
  185. package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
  186. package/src/skf-brief-skill/references/gather-intent.md +87 -182
  187. package/src/skf-brief-skill/references/headless-args.md +84 -16
  188. package/src/skf-brief-skill/references/health-check.md +1 -1
  189. package/src/skf-brief-skill/references/invocation-contract.md +18 -13
  190. package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
  191. package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
  192. package/src/skf-brief-skill/references/scope-definition.md +123 -47
  193. package/src/skf-brief-skill/references/step-auto-brief.md +26 -100
  194. package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
  195. package/src/skf-brief-skill/references/version-resolution.md +7 -10
  196. package/src/skf-brief-skill/references/write-brief.md +66 -57
  197. package/src/skf-campaign/SKILL.md +31 -34
  198. package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
  199. package/src/skf-campaign/customize.toml +47 -16
  200. package/src/skf-campaign/references/campaign-contracts.md +39 -29
  201. package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
  202. package/src/skf-campaign/references/health-check.md +7 -16
  203. package/src/skf-campaign/references/step-01-setup.md +46 -84
  204. package/src/skf-campaign/references/step-02-strategy.md +31 -30
  205. package/src/skf-campaign/references/step-03-pins.md +26 -18
  206. package/src/skf-campaign/references/step-04-provenance.md +24 -23
  207. package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
  208. package/src/skf-campaign/references/step-06-batch.md +41 -34
  209. package/src/skf-campaign/references/step-07-capstone.md +40 -25
  210. package/src/skf-campaign/references/step-08-verify.md +28 -29
  211. package/src/skf-campaign/references/step-09-refine.md +29 -30
  212. package/src/skf-campaign/references/step-10-export.md +48 -36
  213. package/src/skf-campaign/references/step-11-maintenance.md +31 -34
  214. package/src/skf-campaign/references/step-resume.md +37 -68
  215. package/src/skf-campaign/scripts/campaign-deps.py +57 -18
  216. package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
  217. package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
  218. package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
  219. package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
  220. package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
  221. package/src/skf-campaign/scripts/campaign-report.py +204 -37
  222. package/src/skf-campaign/scripts/campaign-state.py +1128 -0
  223. package/src/skf-campaign/scripts/campaign-status.py +10 -74
  224. package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
  225. package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
  226. package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
  227. package/src/skf-campaign/templates/campaign-report-template.md +5 -1
  228. package/src/skf-campaign/templates/kickoff-template.md +0 -14
  229. package/src/skf-create-skill/SKILL.md +27 -20
  230. package/src/skf-create-skill/assets/compile-assembly-rules.md +11 -8
  231. package/src/skf-create-skill/assets/skill-sections.md +21 -11
  232. package/src/skf-create-skill/customize.toml +20 -8
  233. package/src/skf-create-skill/references/authoritative-files-protocol.md +36 -58
  234. package/src/skf-create-skill/references/batch-mode.md +81 -0
  235. package/src/skf-create-skill/references/ccc-index-check.md +27 -0
  236. package/src/skf-create-skill/references/compile.md +119 -70
  237. package/src/skf-create-skill/references/component-extraction.md +113 -91
  238. package/src/skf-create-skill/references/enrich.md +41 -23
  239. package/src/skf-create-skill/references/entry-points-by-hand.md +37 -0
  240. package/src/skf-create-skill/references/extract.md +204 -162
  241. package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
  242. package/src/skf-create-skill/references/extraction-patterns-tracing.md +11 -11
  243. package/src/skf-create-skill/references/extraction-patterns.md +588 -201
  244. package/src/skf-create-skill/references/generate-artifacts.md +145 -93
  245. package/src/skf-create-skill/references/health-check.md +1 -1
  246. package/src/skf-create-skill/references/load-brief.md +53 -36
  247. package/src/skf-create-skill/references/relabel-rule.md +12 -0
  248. package/src/skf-create-skill/references/report.md +37 -57
  249. package/src/skf-create-skill/references/source-resolution-protocols.md +58 -169
  250. package/src/skf-create-skill/references/step-auto-shard.md +20 -93
  251. package/src/skf-create-skill/references/step-doc-rot.md +44 -60
  252. package/src/skf-create-skill/references/step-doc-sources.md +28 -19
  253. package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
  254. package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
  255. package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
  256. package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
  257. package/src/skf-create-skill/references/validate.md +173 -99
  258. package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
  259. package/src/skf-create-stack-skill/SKILL.md +29 -56
  260. package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
  261. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
  262. package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
  263. package/src/skf-create-stack-skill/customize.toml +23 -12
  264. package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
  265. package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
  266. package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
  267. package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
  268. package/src/skf-create-stack-skill/references/generate-output.md +138 -128
  269. package/src/skf-create-stack-skill/references/init.md +90 -32
  270. package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
  271. package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
  272. package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
  273. package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
  274. package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
  275. package/src/skf-create-stack-skill/references/report.md +39 -33
  276. package/src/skf-create-stack-skill/references/validate.md +20 -39
  277. package/src/skf-drop-skill/SKILL.md +28 -51
  278. package/src/skf-drop-skill/customize.toml +44 -30
  279. package/src/skf-drop-skill/references/execute.md +83 -146
  280. package/src/skf-drop-skill/references/health-check.md +2 -0
  281. package/src/skf-drop-skill/references/invocation-contract.md +64 -0
  282. package/src/skf-drop-skill/references/report.md +46 -30
  283. package/src/skf-drop-skill/references/select.md +122 -124
  284. package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
  285. package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
  286. package/src/skf-export-skill/SKILL.md +40 -62
  287. package/src/skf-export-skill/assets/managed-section-format.md +14 -50
  288. package/src/skf-export-skill/assets/snippet-format.md +3 -1
  289. package/src/skf-export-skill/customize.toml +42 -13
  290. package/src/skf-export-skill/references/generate-snippet.md +54 -46
  291. package/src/skf-export-skill/references/health-check.md +2 -0
  292. package/src/skf-export-skill/references/invocation-contract.md +15 -0
  293. package/src/skf-export-skill/references/load-skill.md +88 -62
  294. package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
  295. package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
  296. package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
  297. package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
  298. package/src/skf-export-skill/references/package.md +15 -73
  299. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
  300. package/src/skf-export-skill/references/result-envelope.md +31 -16
  301. package/src/skf-export-skill/references/summary.md +41 -17
  302. package/src/skf-export-skill/references/token-report.md +4 -12
  303. package/src/skf-export-skill/references/update-context.md +118 -237
  304. package/src/skf-forger/SKILL.md +61 -37
  305. package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
  306. package/src/skf-forger/customize.toml +43 -0
  307. package/src/skf-forger/references/pipeline-mode.md +73 -18
  308. package/src/skf-forger/scripts/forge-status.py +299 -0
  309. package/src/skf-forger/scripts/parse-pipeline.py +322 -54
  310. package/src/skf-forger/scripts/pipeline-gate.py +428 -0
  311. package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
  312. package/src/skf-quick-skill/SKILL.md +54 -53
  313. package/src/skf-quick-skill/assets/skill-template.md +4 -2
  314. package/src/skf-quick-skill/customize.toml +51 -20
  315. package/src/skf-quick-skill/references/batch-mode.md +74 -21
  316. package/src/skf-quick-skill/references/compile.md +30 -31
  317. package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
  318. package/src/skf-quick-skill/references/finalize.md +69 -18
  319. package/src/skf-quick-skill/references/halt-contract.md +41 -29
  320. package/src/skf-quick-skill/references/health-check.md +5 -3
  321. package/src/skf-quick-skill/references/quick-extract.md +114 -45
  322. package/src/skf-quick-skill/references/registry-resolution.md +9 -64
  323. package/src/skf-quick-skill/references/resolve-target.md +116 -66
  324. package/src/skf-quick-skill/references/write-and-validate.md +79 -40
  325. package/src/skf-refine-architecture/SKILL.md +43 -53
  326. package/src/skf-refine-architecture/customize.toml +49 -20
  327. package/src/skf-refine-architecture/references/compile.md +97 -54
  328. package/src/skf-refine-architecture/references/exit-codes.md +30 -0
  329. package/src/skf-refine-architecture/references/finding-storage.md +21 -0
  330. package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
  331. package/src/skf-refine-architecture/references/improvements.md +14 -20
  332. package/src/skf-refine-architecture/references/init.md +98 -57
  333. package/src/skf-refine-architecture/references/issue-detection.md +32 -39
  334. package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
  335. package/src/skf-refine-architecture/references/report.md +65 -54
  336. package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
  337. package/src/skf-rename-skill/SKILL.md +19 -54
  338. package/src/skf-rename-skill/customize.toml +32 -26
  339. package/src/skf-rename-skill/references/execute.md +230 -184
  340. package/src/skf-rename-skill/references/exit-codes.md +7 -7
  341. package/src/skf-rename-skill/references/health-check.md +1 -7
  342. package/src/skf-rename-skill/references/invocation-contract.md +25 -0
  343. package/src/skf-rename-skill/references/report.md +63 -52
  344. package/src/skf-rename-skill/references/select.md +197 -115
  345. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
  346. package/src/skf-setup/SKILL.md +26 -26
  347. package/src/skf-setup/customize.toml +14 -10
  348. package/src/skf-setup/references/auto-index.md +50 -63
  349. package/src/skf-setup/references/ccc-index.md +29 -90
  350. package/src/skf-setup/references/detect-and-tier.md +58 -67
  351. package/src/skf-setup/references/health-check.md +2 -0
  352. package/src/skf-setup/references/invocation-contract.md +14 -0
  353. package/src/skf-setup/references/report.md +43 -166
  354. package/src/skf-setup/references/tier-rules.md +5 -5
  355. package/src/skf-setup/references/write-config.md +34 -57
  356. package/src/skf-test-skill/SKILL.md +17 -57
  357. package/src/skf-test-skill/assets/output-section-formats.md +33 -34
  358. package/src/skf-test-skill/customize.toml +27 -13
  359. package/src/skf-test-skill/references/coherence-check.md +134 -62
  360. package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
  361. package/src/skf-test-skill/references/coverage-check.md +209 -264
  362. package/src/skf-test-skill/references/detect-mode.md +5 -5
  363. package/src/skf-test-skill/references/external-validators.md +94 -82
  364. package/src/skf-test-skill/references/health-check.md +6 -7
  365. package/src/skf-test-skill/references/init.md +164 -91
  366. package/src/skf-test-skill/references/invocation-contract.md +66 -0
  367. package/src/skf-test-skill/references/migration-section-rules.md +5 -2
  368. package/src/skf-test-skill/references/report.md +164 -142
  369. package/src/skf-test-skill/references/score.md +80 -107
  370. package/src/skf-test-skill/references/scoring-rules.md +42 -155
  371. package/src/skf-test-skill/references/source-access-protocol.md +18 -94
  372. package/src/skf-test-skill/references/step-hard-gate.md +51 -33
  373. package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
  374. package/src/skf-test-skill/scripts/build-result-context.py +380 -0
  375. package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
  376. package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
  377. package/src/skf-test-skill/scripts/compute-score.py +223 -45
  378. package/src/skf-test-skill/scripts/gap-ledger.py +1666 -0
  379. package/src/skf-test-skill/scripts/hard-gate.py +203 -0
  380. package/src/skf-test-skill/scripts/load-coverage-inputs.py +1198 -0
  381. package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
  382. package/src/skf-test-skill/scripts/reconcile-coverage.py +547 -79
  383. package/src/skf-test-skill/scripts/score-signatures.py +479 -0
  384. package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
  385. package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
  386. package/src/skf-test-skill/scripts/verify-declared-numerator.py +133 -56
  387. package/src/skf-test-skill/templates/test-report-template.md +21 -15
  388. package/src/skf-update-skill/SKILL.md +38 -25
  389. package/src/skf-update-skill/customize.toml +27 -12
  390. package/src/skf-update-skill/references/detect-changes.md +253 -223
  391. package/src/skf-update-skill/references/gap-driven.md +336 -0
  392. package/src/skf-update-skill/references/health-check.md +31 -5
  393. package/src/skf-update-skill/references/init.md +254 -84
  394. package/src/skf-update-skill/references/invocation-contract.md +12 -0
  395. package/src/skf-update-skill/references/manual-section-rules.md +6 -23
  396. package/src/skf-update-skill/references/merge.md +182 -49
  397. package/src/skf-update-skill/references/re-extract.md +53 -262
  398. package/src/skf-update-skill/references/report.md +102 -35
  399. package/src/skf-update-skill/references/write.md +357 -155
  400. package/src/skf-update-skill/scripts/skf-new-file-diff.py +183 -46
  401. package/src/skf-verify-stack/SKILL.md +23 -42
  402. package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
  403. package/src/skf-verify-stack/customize.toml +55 -22
  404. package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
  405. package/src/skf-verify-stack/references/coverage.md +67 -64
  406. package/src/skf-verify-stack/references/exit-codes.md +21 -8
  407. package/src/skf-verify-stack/references/init.md +107 -62
  408. package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
  409. package/src/skf-verify-stack/references/integrations.md +106 -65
  410. package/src/skf-verify-stack/references/report.md +59 -42
  411. package/src/skf-verify-stack/references/requirements.md +32 -9
  412. package/src/skf-verify-stack/references/synthesize.md +53 -39
  413. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
  414. package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
  415. package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
  416. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
  417. package/tools/cli/commands/install.js +11 -4
  418. package/tools/cli/commands/status.js +15 -3
  419. package/tools/cli/commands/update.js +11 -4
  420. package/tools/cli/lib/tool-check.js +320 -0
  421. package/tools/cli/lib/version-check.js +22 -9
  422. package/tools/cli/skf-cli.js +4 -1
  423. package/.gitattributes +0 -16
  424. package/.nvmrc +0 -1
  425. package/docs/_internal/RELEASING.md +0 -679
  426. package/release-audits/v1.0.0-launch-audit.md +0 -728
  427. package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
  428. package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
  429. package/src/skf-campaign/manifest.yaml +0 -15
  430. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
  431. package/src/skf-create-skill/references/ecosystem-check.md +0 -74
  432. package/src/skf-drop-skill/references/headless-contract.md +0 -18
  433. package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
  434. package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
  435. package/src/skf-update-skill/references/validate.md +0 -124
  436. package/tools/validate-docs-drift.js +0 -233
  437. package/tools/validate-docs-links.js +0 -665
@@ -0,0 +1,532 @@
1
+ # /// script
2
+ # requires-python = ">=3.11"
3
+ # dependencies = []
4
+ # ///
5
+ """SKF CCC Git Hygiene — keep ccc's index folders and SKF's workspace lock
6
+ out of git outside the project root.
7
+
8
+ ccc adds `/.cocoindex_code/` to `.gitignore` only when it creates a project
9
+ at the top of a git checkout whose `.git` is a folder. That leaves two gaps,
10
+ one per subcommand:
11
+
12
+ - In an SKF workspace clone the `.gitignore` ccc edits is usually
13
+ tracked, so the edit is a local change that makes git refuse a later
14
+ checkout of another ref, and SKF's `.skf-workspace.lock` shows as
15
+ untracked.
16
+ - A ccc project in a subfolder of a checkout, in a linked worktree or in
17
+ a submodule gets no entry at all, so git lists its index database as
18
+ untracked and a `git add -A` would commit it.
19
+
20
+ CLI:
21
+
22
+ uv run skf-ccc-git-hygiene.py workspace --repo <clone path>
23
+ uv run skf-ccc-git-hygiene.py nested --dir <source folder> --project-root <project root>
24
+
25
+ `~` is expanded in --repo, --dir, --project-root and SKF_WORKSPACE.
26
+
27
+ workspace, in order:
28
+
29
+ 1. Guard: act only when --repo lies under
30
+ `{SKF_WORKSPACE or ~/.skf/workspace}/repos/` and is the top level of its
31
+ git work tree (`git rev-parse --show-toplevel`). Anything else is
32
+ skipped without a write.
33
+ 2. Exclude: append `.cocoindex_code/` (unanchored, so an index in a
34
+ subfolder is covered too) and `/.skf-workspace.lock`, under an SKF
35
+ header, to the file `git rev-parse --git-path info/exclude` names (the
36
+ common one for a linked worktree). An entry already there as an exact
37
+ line is not added again. This runs before any `.gitignore` change, so
38
+ the index folder never shows between the two steps; when it fails,
39
+ `.gitignore` is left alone.
40
+ 3. `.gitignore` repair: when the working-tree `.gitignore` is exactly what
41
+ ccc's append makes of the committed one (line endings normalized on
42
+ both sides; ccc writes nothing when its entry is already a line),
43
+ restore it with `git checkout -- .gitignore`, which reapplies autocrlf
44
+ and eol attributes. When `.gitignore` is untracked and holds exactly
45
+ ccc's two lines, delete it. Any other change, a link, conflict stages,
46
+ an unusual mode or a deleted tracked file is kept.
47
+
48
+ nested:
49
+
50
+ Skips a missing folder, the project root, a `.cocoindex_code` link and a
51
+ folder without `.cocoindex_code/settings.yml`. It then runs
52
+ `git check-ignore -q --no-index` for `settings.yml`, `target_sqlite.db`
53
+ and `cocoindex.db/mdb/data.mdb` under `.cocoindex_code/`, one call per
54
+ path (`-q` refuses several). When any of them is not ignored, it creates
55
+ `.cocoindex_code/.gitignore` holding `*` so the folder ignores itself.
56
+ The write is create-only: an existing file is never overwritten, and a
57
+ write that fails removes the partial file it created. When git
58
+ already tracks index files there, the notice names the
59
+ `git rm -r --cached` remedy. It never reads or writes the project's
60
+ `.gitignore` or any `info/exclude`.
61
+
62
+ Output: one JSON object on one stdout line (ASCII).
63
+
64
+ workspace:
65
+ mode "workspace"
66
+ status "ok" | "skipped"
67
+ skip_reason null | "missing" | "not-a-workspace-clone" |
68
+ "git-unavailable" | "git-failed" | "not-a-clone-root"
69
+ repo --repo as given
70
+ exclude_path absolute path of the exclude file, or null
71
+ exclude_added the entries appended by this run, in order
72
+ gitignore_action null when skipped | "none" | "restored" | "deleted" |
73
+ "kept"
74
+ warnings one entry per failed sub-step
75
+
76
+ nested:
77
+ mode "nested"
78
+ status "ok" | "skipped"
79
+ skip_reason null | "missing" | "project-root" |
80
+ "index-dir-is-link" | "no-ccc-project" |
81
+ "git-unavailable" | "not-a-work-tree" | "git-failed"
82
+ index_dir --dir as given plus "/.cocoindex_code"
83
+ action null when skipped | "none" | "wrote" | "exists" |
84
+ "failed"
85
+ tracked_db_files tracked paths under the folder other than
86
+ settings.yml and .gitignore
87
+ notice one line for the user, or null
88
+ warnings one entry per failed sub-step
89
+
90
+ Exit codes:
91
+ 0 every "ok" and "skipped" result, including partial failures reported
92
+ in `warnings`
93
+ 1 unexpected error: {"status": "error", "message": ...} on stderr,
94
+ nothing on stdout
95
+ 2 usage error (argparse)
96
+
97
+ git runs with the git location variables (GIT_DIR, GIT_INDEX_FILE, ...)
98
+ removed from its environment. A git hook or `git rebase --exec` exports
99
+ them (GIT_DIR too when run in a linked worktree); inherited by
100
+ `git -C <clone>`, they would make the helper touch another repository.
101
+
102
+ It never edits a project's own `.gitignore`: in a workspace clone it only
103
+ restores the committed file or deletes the one ccc created, and elsewhere
104
+ it writes only inside a ccc index folder.
105
+
106
+ It never gates: callers treat any non-zero exit or missing JSON as "did not
107
+ run" and continue.
108
+ """
109
+
110
+ from __future__ import annotations
111
+
112
+ import argparse
113
+ import contextlib
114
+ import json
115
+ import os
116
+ import shutil
117
+ import subprocess
118
+ import sys
119
+ from pathlib import Path
120
+
121
+ CCC_DIR = ".cocoindex_code"
122
+ CCC_ENTRY = "/.cocoindex_code/"
123
+ CCC_BLOCK = b"# CocoIndex Code (ccc)\n/.cocoindex_code/\n" # what ccc init appends
124
+ EXCLUDE_HEADER = "# SKF: ccc index folders and the workspace lock stay out of git"
125
+ EXCLUDE_ENTRIES = (".cocoindex_code/", "/.skf-workspace.lock")
126
+ INDEX_PROBES = (
127
+ ".cocoindex_code/settings.yml",
128
+ ".cocoindex_code/target_sqlite.db",
129
+ ".cocoindex_code/cocoindex.db/mdb/data.mdb",
130
+ )
131
+ SELF_IGNORE = b"# Created by SKF so git ignores this ccc index folder.\n*\n"
132
+ NOT_DB_FILES = {".cocoindex_code/settings.yml", ".cocoindex_code/.gitignore"}
133
+ GIT_TIMEOUT_SEC = 30
134
+ # Keep identical to GIT_LOCATION_VARS in skf-check-workspace-drift.py.
135
+ GIT_LOCATION_VARS = (
136
+ "GIT_DIR",
137
+ "GIT_WORK_TREE",
138
+ "GIT_INDEX_FILE",
139
+ "GIT_PREFIX",
140
+ "GIT_COMMON_DIR",
141
+ "GIT_OBJECT_DIRECTORY",
142
+ "GIT_ALTERNATE_OBJECT_DIRECTORIES",
143
+ "GIT_NAMESPACE",
144
+ )
145
+
146
+
147
+ # Keep identical to _is_link_or_junction in skf-atomic-write.py
148
+ # (test/test-skf-ccc-git-hygiene.py pins the copy).
149
+ def _is_link_or_junction(p: Path) -> bool:
150
+ """True for POSIX symlinks AND Windows junctions/symlinks.
151
+
152
+ `Path.is_symlink()` is False for Windows junctions; os.readlink succeeds
153
+ for both symlinks and junctions (since CPython 3.8 on Windows). A regular
154
+ directory raises OSError on readlink, which is the signal we want to
155
+ refuse replacement. On Windows, any other reparse point (a cloud-sync
156
+ placeholder, a deduplicated file, an app execution alias) raises
157
+ ValueError: it does not redirect to another path, so it is not a link.
158
+ """
159
+ if p.is_symlink():
160
+ return True
161
+ if not p.exists() and not p.is_symlink():
162
+ return False
163
+ try:
164
+ os.readlink(p)
165
+ return True
166
+ except (OSError, ValueError):
167
+ return False
168
+
169
+
170
+ def _resolve_outside_cwd(command: str) -> str | None:
171
+ """shutil.which with a CWD-shim guard. Returns the resolved path or None.
172
+
173
+ shutil.which on Windows searches the current directory ahead of PATH,
174
+ and CWD here may be a repository SKF does not control — a bare-name
175
+ lookup resolving into CWD would execute a planted shim (e.g. git.exe).
176
+ Such a resolution is treated as not-found. Explicit paths supplied by
177
+ callers (containing a separator) are honored as-is. Keep the code
178
+ identical to the sibling guards in skf-merge-ccc-exclusions.py,
179
+ skf-detect-tools.py, skf-qmd-classify-collections.py,
180
+ skf-source-tree.py, skf-tessl-review.py and
181
+ skf-verify-provenance-completeness.py
182
+ (test/test-skf-ccc-git-hygiene.py pins it against
183
+ skf-merge-ccc-exclusions.py).
184
+ """
185
+ resolved = shutil.which(command)
186
+ if resolved is None:
187
+ return None
188
+ if os.sep in command or (os.altsep and os.altsep in command):
189
+ return resolved
190
+ resolved_dir = os.path.dirname(resolved)
191
+ if resolved_dir:
192
+ cwd = os.path.normcase(os.path.abspath(os.getcwd()))
193
+ if os.path.normcase(os.path.abspath(resolved_dir)) == cwd:
194
+ return None
195
+ return resolved
196
+
197
+
198
+ def _git(root: Path, *args: str) -> tuple[int, bytes, str] | None:
199
+ """Run `git -C <root> <args>` with the git location variables removed.
200
+
201
+ Returns None when git cannot be found (or only as a shim in CWD) or
202
+ cannot start, (-1, b"", "timeout") on a timeout, and otherwise
203
+ (returncode, stdout bytes, stderr text). LC_ALL=C keeps git's messages
204
+ in one locale.
205
+ """
206
+ exe = _resolve_outside_cwd("git")
207
+ if exe is None:
208
+ return None
209
+ env = {k: v for k, v in os.environ.items() if k not in GIT_LOCATION_VARS}
210
+ env["LC_ALL"] = "C"
211
+ try:
212
+ result = subprocess.run(
213
+ [exe, "-C", str(root), *args],
214
+ stdin=subprocess.DEVNULL,
215
+ capture_output=True,
216
+ env=env,
217
+ timeout=GIT_TIMEOUT_SEC,
218
+ )
219
+ except subprocess.TimeoutExpired:
220
+ return -1, b"", "timeout"
221
+ except (OSError, ValueError):
222
+ return None
223
+ return result.returncode, result.stdout, result.stderr.decode("utf-8", errors="replace")
224
+
225
+
226
+ def _same_path(a: str | Path, b: str | Path) -> bool:
227
+ """Path identity that covers Windows case and 8.3 names, forward-slash
228
+ git output and macOS /private."""
229
+ return os.path.normcase(os.path.realpath(a)) == os.path.normcase(os.path.realpath(b))
230
+
231
+
232
+ def _norm(b: bytes) -> bytes:
233
+ return b.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
234
+
235
+
236
+ def is_ccc_edit(committed: bytes | None, worktree: bytes) -> bool:
237
+ """True when `worktree` is exactly what ccc's .gitignore append makes of `committed`
238
+ (None = no tracked .gitignore). Line endings are normalized on both sides, so ccc's
239
+ text-mode rewrite (LF on POSIX, CRLF throughout on Windows) matches."""
240
+ w = _norm(worktree)
241
+ if committed is None:
242
+ return w == CCC_BLOCK
243
+ base = _norm(committed)
244
+ if CCC_ENTRY in base.decode("utf-8", "surrogateescape").splitlines():
245
+ return False # ccc writes nothing when its entry is already a line
246
+ sep = b"\n" if base and not base.endswith(b"\n") else b""
247
+ return w == base + sep + CCC_BLOCK
248
+
249
+
250
+ def _ensure_excludes(path: Path, warnings: list[str]) -> list[str] | None:
251
+ """Append the EXCLUDE_ENTRIES missing from `path` as exact lines.
252
+
253
+ Returns the entries added ([] when all were there), or None after a
254
+ read or write failure, which is recorded in `warnings`.
255
+ """
256
+ try:
257
+ try:
258
+ cur = path.read_bytes()
259
+ except FileNotFoundError:
260
+ cur = b""
261
+ text = _norm(cur).decode("utf-8", "surrogateescape")
262
+ present = {line.rstrip(" \t") for line in text.split("\n")}
263
+ missing = [entry for entry in EXCLUDE_ENTRIES if entry not in present]
264
+ if not missing:
265
+ return []
266
+ lead = "\n" if cur and not cur.endswith((b"\n", b"\r")) else ""
267
+ block = lead + EXCLUDE_HEADER + "\n" + "".join(entry + "\n" for entry in missing)
268
+ path.parent.mkdir(parents=True, exist_ok=True)
269
+ # O_BINARY (Windows only; 0 elsewhere) keeps the LF line endings.
270
+ fd = os.open(
271
+ path,
272
+ os.O_WRONLY | os.O_CREAT | os.O_APPEND | getattr(os, "O_BINARY", 0),
273
+ 0o644,
274
+ )
275
+ try:
276
+ os.write(fd, block.encode("utf-8"))
277
+ finally:
278
+ os.close(fd)
279
+ except OSError as e:
280
+ warnings.append(f"could not update {path}: {e.strerror or e}")
281
+ return None
282
+ return missing
283
+
284
+
285
+ def _repair_gitignore(repo: Path, warnings: list[str]) -> str:
286
+ """Undo ccc's edit to the clone's top-level .gitignore; see the module docstring."""
287
+ gi = repo / ".gitignore"
288
+ if _is_link_or_junction(gi):
289
+ return "kept"
290
+ res = _git(repo, "ls-files", "-z", "--stage", "--", ".gitignore")
291
+ if res is None or res[0] != 0:
292
+ warnings.append("git ls-files failed for .gitignore; left it alone")
293
+ return "kept"
294
+ entries = [e for e in res[1].split(b"\0") if e]
295
+ try:
296
+ if not entries: # untracked
297
+ if not gi.exists():
298
+ return "none"
299
+ if not gi.is_file():
300
+ return "kept"
301
+ if not is_ccc_edit(None, gi.read_bytes()):
302
+ return "kept"
303
+ gi.unlink()
304
+ return "deleted"
305
+ if len(entries) != 1: # conflict stages
306
+ return "kept"
307
+ meta = entries[0].split(b"\t", 1)[0].split(b" ")
308
+ if len(meta) != 3:
309
+ return "kept"
310
+ mode, sha, stage = meta
311
+ if stage != b"0" or mode not in (b"100644", b"100755"):
312
+ return "kept"
313
+ if not gi.is_file(): # a deleted tracked file is someone else's change
314
+ return "kept"
315
+ blob = _git(repo, "cat-file", "blob", sha.decode("ascii"))
316
+ if blob is None or blob[0] != 0:
317
+ warnings.append("git cat-file failed for .gitignore; left it alone")
318
+ return "kept"
319
+ worktree = gi.read_bytes()
320
+ except OSError as e:
321
+ warnings.append(f"could not read or delete {gi}: {e.strerror or e}")
322
+ return "kept"
323
+ if _norm(worktree) == _norm(blob[1]):
324
+ return "none" # unchanged, or line endings only: left alone
325
+ if not is_ccc_edit(blob[1], worktree):
326
+ return "kept"
327
+ res = _git(repo, "checkout", "-q", "--", ".gitignore")
328
+ if res is None or res[0] != 0:
329
+ warnings.append("git checkout -- .gitignore failed; left it alone")
330
+ return "kept"
331
+ return "restored"
332
+
333
+
334
+ def workspace(repo_arg: str) -> dict:
335
+ """The `workspace` subcommand; returns the output object."""
336
+ out = {
337
+ "mode": "workspace",
338
+ "status": "skipped",
339
+ "skip_reason": None,
340
+ "repo": repo_arg,
341
+ "exclude_path": None,
342
+ "exclude_added": [],
343
+ "gitignore_action": None,
344
+ "warnings": [],
345
+ }
346
+ repo = Path(os.path.expanduser(repo_arg))
347
+ if not repo.is_dir():
348
+ out["skip_reason"] = "missing"
349
+ return out
350
+ ws = os.environ.get("SKF_WORKSPACE") or "~/.skf/workspace"
351
+ repos_root = os.path.normcase(os.path.realpath(os.path.join(os.path.expanduser(ws), "repos")))
352
+ real = os.path.normcase(os.path.realpath(repo))
353
+ try:
354
+ inside = os.path.commonpath([real, repos_root]) == repos_root and real != repos_root
355
+ except ValueError: # different drives on Windows
356
+ inside = False
357
+ if not inside:
358
+ out["skip_reason"] = "not-a-workspace-clone"
359
+ return out
360
+ top = _git(repo, "rev-parse", "--show-toplevel")
361
+ if top is None:
362
+ out["skip_reason"] = "git-unavailable"
363
+ return out
364
+ if top[0] == -1:
365
+ out["skip_reason"] = "git-failed"
366
+ return out
367
+ if top[0] != 0 or os.path.normcase(os.path.realpath(os.fsdecode(top[1].strip()))) != real:
368
+ out["skip_reason"] = "not-a-clone-root"
369
+ return out
370
+ out["status"] = "ok"
371
+ added = None
372
+ gp = _git(repo, "rev-parse", "--git-path", "info/exclude")
373
+ if gp is None or gp[0] != 0:
374
+ out["warnings"].append("git rev-parse --git-path info/exclude failed")
375
+ else:
376
+ path = Path(os.fsdecode(gp[1].strip()))
377
+ if not path.is_absolute():
378
+ path = repo / path
379
+ out["exclude_path"] = os.path.abspath(path)
380
+ added = _ensure_excludes(path, out["warnings"])
381
+ if added is None:
382
+ out["gitignore_action"] = "kept"
383
+ out["warnings"].append(
384
+ ".gitignore left alone: restoring it without the exclude would show the index folder"
385
+ )
386
+ return out
387
+ out["exclude_added"] = added
388
+ out["gitignore_action"] = _repair_gitignore(repo, out["warnings"])
389
+ return out
390
+
391
+
392
+ def _nested_notice(action: str, index_dir: str, tracked: int, reason: str = "") -> str | None:
393
+ if action == "wrote" and tracked == 0:
394
+ return (
395
+ f"Wrote {index_dir}/.gitignore so git ignores the ccc index in that folder; "
396
+ f"your own .gitignore files are unchanged."
397
+ )
398
+ if action == "wrote":
399
+ return (
400
+ f"Wrote {index_dir}/.gitignore so git ignores new ccc index files in that folder, "
401
+ f"but git already tracks {tracked} of its files; to stop tracking them, run: "
402
+ f"git rm -r --cached -- \"{index_dir}\""
403
+ )
404
+ if action == "exists":
405
+ return (
406
+ f"git lists files in {index_dir} as untracked, and the .gitignore already in that "
407
+ f"folder does not ignore them; add a line holding only * to {index_dir}/.gitignore."
408
+ )
409
+ if action == "failed":
410
+ return (
411
+ f"Could not write {index_dir}/.gitignore ({reason}); git lists the ccc index in "
412
+ f"that folder as untracked. Create that file holding the line * to keep it out of git."
413
+ )
414
+ return None
415
+
416
+
417
+ def nested(dir_arg: str, project_root: str) -> dict:
418
+ """The `nested` subcommand; returns the output object."""
419
+ index_dir = dir_arg.rstrip("/\\") + "/" + CCC_DIR
420
+ out = {
421
+ "mode": "nested",
422
+ "status": "skipped",
423
+ "skip_reason": None,
424
+ "index_dir": index_dir,
425
+ "action": None,
426
+ "tracked_db_files": 0,
427
+ "notice": None,
428
+ "warnings": [],
429
+ }
430
+ d = Path(os.path.expanduser(dir_arg))
431
+ if not d.is_dir():
432
+ out["skip_reason"] = "missing"
433
+ return out
434
+ if _same_path(d, os.path.expanduser(project_root)):
435
+ out["skip_reason"] = "project-root"
436
+ return out
437
+ idx = d / CCC_DIR
438
+ if _is_link_or_junction(idx):
439
+ out["skip_reason"] = "index-dir-is-link"
440
+ return out
441
+ if not (idx / "settings.yml").is_file():
442
+ out["skip_reason"] = "no-ccc-project"
443
+ return out
444
+ listed = False
445
+ for probe in INDEX_PROBES: # one call per path: -q refuses several
446
+ res = _git(d, "check-ignore", "-q", "--no-index", "--", probe)
447
+ if res is None:
448
+ out["skip_reason"] = "git-unavailable"
449
+ return out
450
+ if res[0] == 1:
451
+ listed = True
452
+ elif res[0] == 128:
453
+ out["skip_reason"] = "not-a-work-tree"
454
+ return out
455
+ elif res[0] != 0:
456
+ out["skip_reason"] = "git-failed"
457
+ return out
458
+ out["status"] = "ok"
459
+ if not listed:
460
+ out["action"] = "none"
461
+ return out
462
+ tracked = _git(d, "ls-files", "-z", "--", CCC_DIR) # paths relative to d
463
+ if tracked is not None and tracked[0] == 0:
464
+ files = [os.fsdecode(f) for f in tracked[1].split(b"\0") if f]
465
+ out["tracked_db_files"] = sum(1 for f in files if f not in NOT_DB_FILES)
466
+ target = idx / ".gitignore"
467
+ try:
468
+ fd = os.open(
469
+ target,
470
+ os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0),
471
+ 0o644,
472
+ )
473
+ except FileExistsError:
474
+ out["action"] = "exists"
475
+ out["notice"] = _nested_notice("exists", index_dir, out["tracked_db_files"])
476
+ return out
477
+ except OSError as e:
478
+ return _nested_failed(out, e.strerror or str(e))
479
+ reason = None
480
+ try:
481
+ try:
482
+ if os.write(fd, SELF_IGNORE) != len(SELF_IGNORE):
483
+ reason = "short write"
484
+ finally:
485
+ os.close(fd)
486
+ except OSError as e:
487
+ reason = e.strerror or str(e)
488
+ if reason is not None:
489
+ # Remove the partial file this run created, so the next run writes it
490
+ # again instead of reporting a foreign .gitignore.
491
+ with contextlib.suppress(OSError):
492
+ os.unlink(target)
493
+ return _nested_failed(out, reason)
494
+ out["action"] = "wrote"
495
+ out["notice"] = _nested_notice("wrote", index_dir, out["tracked_db_files"])
496
+ return out
497
+
498
+
499
+ def _nested_failed(out: dict, reason: str) -> dict:
500
+ """Record a failed create or write of the self-ignoring file in `out`."""
501
+ index_dir = out["index_dir"]
502
+ out["action"] = "failed"
503
+ out["warnings"].append(f"could not create {index_dir}/.gitignore: {reason}")
504
+ out["notice"] = _nested_notice("failed", index_dir, 0, reason)
505
+ return out
506
+
507
+
508
+ def main(argv: list[str] | None = None) -> int:
509
+ parser = argparse.ArgumentParser(
510
+ description="Keep ccc's index folders and SKF's workspace lock out of git.",
511
+ )
512
+ sub = parser.add_subparsers(dest="command", required=True)
513
+ ws = sub.add_parser("workspace", help="repair and exclude in an SKF workspace clone")
514
+ ws.add_argument("--repo", required=True, help="the workspace clone's top-level folder")
515
+ ne = sub.add_parser("nested", help="make a nested ccc index folder ignore itself")
516
+ ne.add_argument("--dir", required=True, help="the local source folder ccc indexed")
517
+ ne.add_argument("--project-root", required=True, help="the SKF project root")
518
+ args = parser.parse_args(argv)
519
+ try:
520
+ if args.command == "workspace":
521
+ result = workspace(args.repo)
522
+ else:
523
+ result = nested(args.dir, args.project_root)
524
+ except Exception as e: # noqa: BLE001 — any failure means "did not run"
525
+ print(json.dumps({"status": "error", "message": f"{type(e).__name__}: {e}"}), file=sys.stderr)
526
+ return 1
527
+ print(json.dumps(result))
528
+ return 0
529
+
530
+
531
+ if __name__ == "__main__":
532
+ sys.exit(main())