safeword 0.78.2 → 0.78.4

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 (224) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  2. package/codex-plugin/hooks.json +5 -5
  3. package/codex-plugin/skills/bdd/references/SCENARIOS.md +3 -0
  4. package/codex-plugin/skills/bdd/references/TDD.md +11 -5
  5. package/codex-plugin/skills/closeout/SKILL.md +24 -16
  6. package/codex-plugin/skills/finish-review/SKILL.md +4 -3
  7. package/codex-plugin/skills/lint/SKILL.md +1 -1
  8. package/codex-plugin/skills/review-spec/SKILL.md +5 -1
  9. package/codex-plugin/skills/self-review/SKILL.md +2 -2
  10. package/codex-plugin/skills/ticket-system/SKILL.md +3 -0
  11. package/dist/{architecture-N5ZC4CDU.js → architecture-TCSDEFKH.js} +9 -9
  12. package/dist/{architecture-document-UBA5UT4J.js → architecture-document-L2ADWVMV.js} +6 -6
  13. package/dist/{architecture-monorepo-E4JUYJGV.js → architecture-monorepo-NKOQZBEY.js} +4 -4
  14. package/dist/{boundary-HLJILY7R.js → boundary-QP2H7C46.js} +6 -6
  15. package/dist/{chunk-NOTBZCKG.js → chunk-2XR7J5WL.js} +93 -42
  16. package/dist/chunk-2XR7J5WL.js.map +1 -0
  17. package/dist/{chunk-4CZS4YJV.js → chunk-3OKZ3X4O.js} +3 -3
  18. package/dist/{chunk-GZ36UZJG.js → chunk-73TABQRT.js} +10 -10
  19. package/dist/{chunk-4CKCRZDO.js → chunk-7ULZND56.js} +2 -2
  20. package/dist/{chunk-34NCOTP5.js → chunk-7XELS325.js} +2 -2
  21. package/dist/{chunk-USPVNZ63.js → chunk-BZWO3BVD.js} +11 -7
  22. package/dist/chunk-BZWO3BVD.js.map +1 -0
  23. package/dist/{chunk-BQTC6YGN.js → chunk-D5ENZR6N.js} +2 -2
  24. package/dist/{chunk-SME5SU3O.js → chunk-DBXX5K74.js} +258 -48
  25. package/dist/chunk-DBXX5K74.js.map +1 -0
  26. package/dist/{chunk-V63TSN3C.js → chunk-FOYGZ4MK.js} +31 -10
  27. package/dist/chunk-FOYGZ4MK.js.map +1 -0
  28. package/dist/{chunk-ZRV7TOCX.js → chunk-G5AFQXXW.js} +2 -2
  29. package/dist/{chunk-AXAGJLMP.js → chunk-G757F3EU.js} +4 -4
  30. package/dist/{chunk-KXHVFCDT.js → chunk-GFDINP7M.js} +153 -27
  31. package/dist/chunk-GFDINP7M.js.map +1 -0
  32. package/dist/{chunk-S2T356DA.js → chunk-HF3DXZEA.js} +7 -10
  33. package/dist/chunk-HF3DXZEA.js.map +1 -0
  34. package/dist/{chunk-NSZGI7HK.js → chunk-HNCUIXDY.js} +9 -9
  35. package/dist/{chunk-NSZGI7HK.js.map → chunk-HNCUIXDY.js.map} +1 -1
  36. package/dist/{chunk-BSOUMHMQ.js → chunk-IPTM5VE7.js} +101 -1094
  37. package/dist/chunk-IPTM5VE7.js.map +1 -0
  38. package/dist/chunk-JBOLNV2L.js +21 -0
  39. package/dist/chunk-JBOLNV2L.js.map +1 -0
  40. package/dist/chunk-JSR3HXFT.js +1231 -0
  41. package/dist/chunk-JSR3HXFT.js.map +1 -0
  42. package/dist/{chunk-BAVDDAN3.js → chunk-KS2ESRQL.js} +9 -9
  43. package/dist/chunk-LFC6CMMX.js +136 -0
  44. package/dist/chunk-LFC6CMMX.js.map +1 -0
  45. package/dist/{chunk-QTG2EXFT.js → chunk-LUNHU6Z3.js} +2 -2
  46. package/dist/{chunk-QTG2EXFT.js.map → chunk-LUNHU6Z3.js.map} +1 -1
  47. package/dist/{chunk-WVDRXD67.js → chunk-MLBZFB5A.js} +1 -1
  48. package/dist/chunk-MLBZFB5A.js.map +1 -0
  49. package/dist/{chunk-KFVEPQ7X.js → chunk-MVFHTUQ3.js} +2 -2
  50. package/dist/{chunk-TJ627SKI.js → chunk-O3GZ77F7.js} +15 -1
  51. package/dist/chunk-O3GZ77F7.js.map +1 -0
  52. package/dist/{chunk-Q5KLE6ZF.js → chunk-OKPRQGFG.js} +2 -2
  53. package/dist/{chunk-QUDIOBHP.js → chunk-PD7ZXRHV.js} +139 -18
  54. package/dist/chunk-PD7ZXRHV.js.map +1 -0
  55. package/dist/chunk-QAHJSG53.js +30 -0
  56. package/dist/chunk-QAHJSG53.js.map +1 -0
  57. package/dist/{chunk-3SOJBMM6.js → chunk-R6BSRNU4.js} +6 -6
  58. package/dist/{chunk-3SOJBMM6.js.map → chunk-R6BSRNU4.js.map} +1 -1
  59. package/dist/{chunk-J7UDLLTK.js → chunk-RQ7RZYNB.js} +39 -25
  60. package/dist/chunk-RQ7RZYNB.js.map +1 -0
  61. package/dist/{chunk-664YNAMR.js → chunk-RTFPKJJV.js} +5 -5
  62. package/dist/{chunk-GKR44BCH.js → chunk-RVDXBHUG.js} +2 -2
  63. package/dist/chunk-THLEVFEX.js +22 -0
  64. package/dist/chunk-THLEVFEX.js.map +1 -0
  65. package/dist/{chunk-A7Z27COA.js → chunk-VFWAZLEF.js} +3 -3
  66. package/dist/{chunk-I4KBHRZW.js → chunk-VJUPD7JI.js} +7 -7
  67. package/dist/{chunk-3EJNDIAO.js → chunk-VZ5B2SIO.js} +2 -2
  68. package/dist/chunk-WAMQTZAH.js +19 -0
  69. package/dist/chunk-WAMQTZAH.js.map +1 -0
  70. package/dist/{chunk-FDHJ63FZ.js → chunk-WVA4Q6BX.js} +1 -1
  71. package/dist/{chunk-FDHJ63FZ.js.map → chunk-WVA4Q6BX.js.map} +1 -1
  72. package/dist/{chunk-AV5LTEME.js → chunk-YZ5JGDOV.js} +30 -134
  73. package/dist/chunk-YZ5JGDOV.js.map +1 -0
  74. package/dist/{cleanup-X556LNWM.js → cleanup-JNLZ7AXA.js} +9 -9
  75. package/dist/{cleanup-command-RV2Q7G72.js → cleanup-command-MFAYNYRW.js} +15 -15
  76. package/dist/cli.js +152 -109
  77. package/dist/cli.js.map +1 -1
  78. package/dist/{clients-HP23HRN2.js → clients-7AJBKEWF.js} +2 -2
  79. package/dist/codex-bootstrap-KFNUS63Y.js +252 -0
  80. package/dist/codex-bootstrap-KFNUS63Y.js.map +1 -0
  81. package/dist/{codex-hook-IPX3PDXX.js → codex-hook-TNMYVOR7.js} +57 -49
  82. package/dist/codex-hook-TNMYVOR7.js.map +1 -0
  83. package/dist/{codify-J5ZLJ3HK.js → codify-RBWBRXQO.js} +11 -8
  84. package/dist/codify-RBWBRXQO.js.map +1 -0
  85. package/dist/{commands-L4AINARC.js → commands-4UXHBZFD.js} +127 -71
  86. package/dist/commands-4UXHBZFD.js.map +1 -0
  87. package/dist/{config-LUSJAK6B.js → config-GS7BTZJD.js} +2 -2
  88. package/dist/{configured-paths-SPQ5XWOM.js → configured-paths-CREUDQF3.js} +4 -4
  89. package/dist/contract-42CIWIWV.js +8 -0
  90. package/dist/{coordinator-SA75UB3C.js → coordinator-WOPGWDXK.js} +231 -144
  91. package/dist/coordinator-WOPGWDXK.js.map +1 -0
  92. package/dist/corpus-CAFYEMBO.js +15 -0
  93. package/dist/{doctor-DSC36R4U.js → doctor-AZP5D7QP.js} +21 -21
  94. package/dist/{feature-directories-VOHVV26X.js → feature-directories-6TLM7Y4H.js} +6 -6
  95. package/dist/{finalization-7SMLMVLV.js → finalization-AQT3SOP7.js} +4 -4
  96. package/dist/{gh-cli-ZMO7D2KE.js → gh-cli-XCQT5ST7.js} +2 -2
  97. package/dist/{github-rest-KGKS4V2H.js → github-rest-CXFJ2NFF.js} +2 -2
  98. package/dist/index.js +2 -2
  99. package/dist/{job-CDADLJDF.js → job-4POTQJZK.js} +20 -21
  100. package/dist/job-4POTQJZK.js.map +1 -0
  101. package/dist/{learning-sync-6BTHM6ZC.js → learning-sync-QVXBOP6J.js} +4 -4
  102. package/dist/{legacy-global-guidance-2JL4Y5EB.js → legacy-global-guidance-CPGUUNJI.js} +5 -5
  103. package/dist/{lint-gherkin-COSHVS6Z.js → lint-gherkin-LZ45NG4A.js} +7 -7
  104. package/dist/{operations-XVRQSLJG.js → operations-ODOA5CDG.js} +18 -16
  105. package/dist/{output-3LB2OEHN.js → output-QTZCFLAY.js} +2 -2
  106. package/dist/{packet-5WT5M3OS.js → packet-5G6RT2EW.js} +2 -2
  107. package/dist/presets/typescript/index.js +2 -2
  108. package/dist/{profile-UMKQ5JVO.js → profile-QGLZNLRU.js} +7 -7
  109. package/dist/{prompt-GWYDFMLO.js → prompt-UCTSCXSS.js} +2 -2
  110. package/dist/remove-UQPEXXNP.js +23 -0
  111. package/dist/{retro-IFSVU5KS.js → retro-ELKR6GHF.js} +8 -8
  112. package/dist/{retro-draft-spool-4S3TCHWL.js → retro-draft-spool-M75BTT32.js} +2 -2
  113. package/dist/{retro-extract-OSBFAXLU.js → retro-extract-RA6THOC4.js} +2 -2
  114. package/dist/{review-pr-6S2352CK.js → review-pr-MN6QESAJ.js} +2 -2
  115. package/dist/{review-pr-publication-CGHCRRUI.js → review-pr-publication-6AVOGWR2.js} +2 -2
  116. package/dist/{run-5JESKXUR.js → run-JFDGRAH2.js} +4 -4
  117. package/dist/{self-report-VPHBPPSJ.js → self-report-5APRPB5P.js} +2 -2
  118. package/dist/status-ATFG6Q3K.js +21 -0
  119. package/dist/{status-EX4L3ELO.js → status-HIQSP3F5.js} +21 -21
  120. package/dist/{sync-config-O7J7IDGE.js → sync-config-XPAJKBRO.js} +5 -5
  121. package/dist/{sync-tracker-U2GYOFUO.js → sync-tracker-3E5BWX73.js} +6 -6
  122. package/dist/{test-execution-6LIPMBIH.js → test-execution-VDECX7TO.js} +8 -7
  123. package/dist/{test-execution-6LIPMBIH.js.map → test-execution-VDECX7TO.js.map} +1 -1
  124. package/dist/{test-plan-VASHZXJ7.js → test-plan-2L7A7UZV.js} +7 -6
  125. package/dist/{test-plan-VASHZXJ7.js.map → test-plan-2L7A7UZV.js.map} +1 -1
  126. package/dist/{ticket-new-3YXDIWPC.js → ticket-new-HZKNZ5GO.js} +11 -11
  127. package/dist/ticket-new-HZKNZ5GO.js.map +1 -0
  128. package/dist/{ticket-sync-LLUBXZ2R.js → ticket-sync-4QJD3KOL.js} +5 -5
  129. package/dist/{tracker-map-HR4MBYG5.js → tracker-map-CFMFIEER.js} +2 -2
  130. package/dist/{tracker-sync-YDICZN66.js → tracker-sync-MOPJE6M7.js} +2 -2
  131. package/package.json +1 -1
  132. package/templates/SAFEWORD.md +2 -2
  133. package/templates/commands/lint.md +1 -1
  134. package/templates/scripts/closeout-cleanup.ts +127 -31
  135. package/templates/skills/bdd/SCENARIOS.md +3 -0
  136. package/templates/skills/bdd/TDD.md +11 -5
  137. package/templates/skills/closeout/SKILL.md +25 -17
  138. package/templates/skills/finish-review/SKILL.md +4 -3
  139. package/templates/skills/lint/SKILL.md +1 -1
  140. package/templates/skills/review-spec/SKILL.md +5 -1
  141. package/templates/skills/self-review/SKILL.md +2 -2
  142. package/templates/skills/ticket-system/SKILL.md +3 -0
  143. package/dist/chunk-6S7CTKGC.js +0 -128
  144. package/dist/chunk-6S7CTKGC.js.map +0 -1
  145. package/dist/chunk-AV5LTEME.js.map +0 -1
  146. package/dist/chunk-BSOUMHMQ.js.map +0 -1
  147. package/dist/chunk-J7UDLLTK.js.map +0 -1
  148. package/dist/chunk-KXHVFCDT.js.map +0 -1
  149. package/dist/chunk-NOTBZCKG.js.map +0 -1
  150. package/dist/chunk-QUDIOBHP.js.map +0 -1
  151. package/dist/chunk-S2T356DA.js.map +0 -1
  152. package/dist/chunk-SME5SU3O.js.map +0 -1
  153. package/dist/chunk-TJ627SKI.js.map +0 -1
  154. package/dist/chunk-USPVNZ63.js.map +0 -1
  155. package/dist/chunk-V63TSN3C.js.map +0 -1
  156. package/dist/chunk-WRUXO3WA.js +0 -48
  157. package/dist/chunk-WRUXO3WA.js.map +0 -1
  158. package/dist/chunk-WVDRXD67.js.map +0 -1
  159. package/dist/codex-bootstrap-MGY2D6HU.js +0 -178
  160. package/dist/codex-bootstrap-MGY2D6HU.js.map +0 -1
  161. package/dist/codex-hook-IPX3PDXX.js.map +0 -1
  162. package/dist/codify-J5ZLJ3HK.js.map +0 -1
  163. package/dist/commands-L4AINARC.js.map +0 -1
  164. package/dist/contract-KEZY6YC3.js +0 -8
  165. package/dist/coordinator-SA75UB3C.js.map +0 -1
  166. package/dist/corpus-PPOBTF4J.js +0 -15
  167. package/dist/job-CDADLJDF.js.map +0 -1
  168. package/dist/remove-E6HGNHOU.js +0 -22
  169. package/dist/status-7VIGNTUI.js +0 -21
  170. package/dist/ticket-new-3YXDIWPC.js.map +0 -1
  171. /package/dist/{architecture-N5ZC4CDU.js.map → architecture-TCSDEFKH.js.map} +0 -0
  172. /package/dist/{architecture-document-UBA5UT4J.js.map → architecture-document-L2ADWVMV.js.map} +0 -0
  173. /package/dist/{architecture-monorepo-E4JUYJGV.js.map → architecture-monorepo-NKOQZBEY.js.map} +0 -0
  174. /package/dist/{boundary-HLJILY7R.js.map → boundary-QP2H7C46.js.map} +0 -0
  175. /package/dist/{chunk-4CZS4YJV.js.map → chunk-3OKZ3X4O.js.map} +0 -0
  176. /package/dist/{chunk-GZ36UZJG.js.map → chunk-73TABQRT.js.map} +0 -0
  177. /package/dist/{chunk-4CKCRZDO.js.map → chunk-7ULZND56.js.map} +0 -0
  178. /package/dist/{chunk-34NCOTP5.js.map → chunk-7XELS325.js.map} +0 -0
  179. /package/dist/{chunk-BQTC6YGN.js.map → chunk-D5ENZR6N.js.map} +0 -0
  180. /package/dist/{chunk-ZRV7TOCX.js.map → chunk-G5AFQXXW.js.map} +0 -0
  181. /package/dist/{chunk-AXAGJLMP.js.map → chunk-G757F3EU.js.map} +0 -0
  182. /package/dist/{chunk-BAVDDAN3.js.map → chunk-KS2ESRQL.js.map} +0 -0
  183. /package/dist/{chunk-KFVEPQ7X.js.map → chunk-MVFHTUQ3.js.map} +0 -0
  184. /package/dist/{chunk-Q5KLE6ZF.js.map → chunk-OKPRQGFG.js.map} +0 -0
  185. /package/dist/{chunk-664YNAMR.js.map → chunk-RTFPKJJV.js.map} +0 -0
  186. /package/dist/{chunk-GKR44BCH.js.map → chunk-RVDXBHUG.js.map} +0 -0
  187. /package/dist/{chunk-A7Z27COA.js.map → chunk-VFWAZLEF.js.map} +0 -0
  188. /package/dist/{chunk-I4KBHRZW.js.map → chunk-VJUPD7JI.js.map} +0 -0
  189. /package/dist/{chunk-3EJNDIAO.js.map → chunk-VZ5B2SIO.js.map} +0 -0
  190. /package/dist/{cleanup-X556LNWM.js.map → cleanup-JNLZ7AXA.js.map} +0 -0
  191. /package/dist/{cleanup-command-RV2Q7G72.js.map → cleanup-command-MFAYNYRW.js.map} +0 -0
  192. /package/dist/{clients-HP23HRN2.js.map → clients-7AJBKEWF.js.map} +0 -0
  193. /package/dist/{config-LUSJAK6B.js.map → config-GS7BTZJD.js.map} +0 -0
  194. /package/dist/{configured-paths-SPQ5XWOM.js.map → configured-paths-CREUDQF3.js.map} +0 -0
  195. /package/dist/{contract-KEZY6YC3.js.map → contract-42CIWIWV.js.map} +0 -0
  196. /package/dist/{corpus-PPOBTF4J.js.map → corpus-CAFYEMBO.js.map} +0 -0
  197. /package/dist/{doctor-DSC36R4U.js.map → doctor-AZP5D7QP.js.map} +0 -0
  198. /package/dist/{feature-directories-VOHVV26X.js.map → feature-directories-6TLM7Y4H.js.map} +0 -0
  199. /package/dist/{finalization-7SMLMVLV.js.map → finalization-AQT3SOP7.js.map} +0 -0
  200. /package/dist/{gh-cli-ZMO7D2KE.js.map → gh-cli-XCQT5ST7.js.map} +0 -0
  201. /package/dist/{github-rest-KGKS4V2H.js.map → github-rest-CXFJ2NFF.js.map} +0 -0
  202. /package/dist/{learning-sync-6BTHM6ZC.js.map → learning-sync-QVXBOP6J.js.map} +0 -0
  203. /package/dist/{legacy-global-guidance-2JL4Y5EB.js.map → legacy-global-guidance-CPGUUNJI.js.map} +0 -0
  204. /package/dist/{lint-gherkin-COSHVS6Z.js.map → lint-gherkin-LZ45NG4A.js.map} +0 -0
  205. /package/dist/{operations-XVRQSLJG.js.map → operations-ODOA5CDG.js.map} +0 -0
  206. /package/dist/{output-3LB2OEHN.js.map → output-QTZCFLAY.js.map} +0 -0
  207. /package/dist/{packet-5WT5M3OS.js.map → packet-5G6RT2EW.js.map} +0 -0
  208. /package/dist/{profile-UMKQ5JVO.js.map → profile-QGLZNLRU.js.map} +0 -0
  209. /package/dist/{prompt-GWYDFMLO.js.map → prompt-UCTSCXSS.js.map} +0 -0
  210. /package/dist/{remove-E6HGNHOU.js.map → remove-UQPEXXNP.js.map} +0 -0
  211. /package/dist/{retro-IFSVU5KS.js.map → retro-ELKR6GHF.js.map} +0 -0
  212. /package/dist/{retro-draft-spool-4S3TCHWL.js.map → retro-draft-spool-M75BTT32.js.map} +0 -0
  213. /package/dist/{retro-extract-OSBFAXLU.js.map → retro-extract-RA6THOC4.js.map} +0 -0
  214. /package/dist/{review-pr-6S2352CK.js.map → review-pr-MN6QESAJ.js.map} +0 -0
  215. /package/dist/{review-pr-publication-CGHCRRUI.js.map → review-pr-publication-6AVOGWR2.js.map} +0 -0
  216. /package/dist/{run-5JESKXUR.js.map → run-JFDGRAH2.js.map} +0 -0
  217. /package/dist/{self-report-VPHBPPSJ.js.map → self-report-5APRPB5P.js.map} +0 -0
  218. /package/dist/{status-7VIGNTUI.js.map → status-ATFG6Q3K.js.map} +0 -0
  219. /package/dist/{status-EX4L3ELO.js.map → status-HIQSP3F5.js.map} +0 -0
  220. /package/dist/{sync-config-O7J7IDGE.js.map → sync-config-XPAJKBRO.js.map} +0 -0
  221. /package/dist/{sync-tracker-U2GYOFUO.js.map → sync-tracker-3E5BWX73.js.map} +0 -0
  222. /package/dist/{ticket-sync-LLUBXZ2R.js.map → ticket-sync-4QJD3KOL.js.map} +0 -0
  223. /package/dist/{tracker-map-HR4MBYG5.js.map → tracker-map-CFMFIEER.js.map} +0 -0
  224. /package/dist/{tracker-sync-YDICZN66.js.map → tracker-sync-MOPJE6M7.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/health.ts","../src/claude-plugin/delivery-schema.ts","../src/utils/architecture-records.ts","../src/utils/glossary.ts","../src/utils/validation.ts","../src/utils/personas.ts","../src/utils/ticket-index-warnings.ts","../src/codex-plugin/delivery-schema.ts","../src/lifecycle/schema.ts","../src/lifecycle/cursor.ts"],"sourcesContent":["/**\n * Config-health verification core (ticket 3293WH).\n *\n * Shared by typed status and setup convergence so mutating commands can prove\n * their own postcondition at exit. Deliberately network-free: a postcondition\n * check must not depend on the registry being reachable.\n *\n * Uses reconcile() with dryRun to detect missing files and configuration\n * issues.\n */\n\nimport { execFileSync } from 'node:child_process';\nimport { readdirSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport {\n detectUnanchoredPhaseState,\n type PhaseAnchorScope,\n} from '../templates/hooks/lib/phase-provenance.js';\nimport { schemaForClaudeDelivery } from './claude-plugin/delivery-schema.js';\nimport { hasImportLinterScaffoldTarget } from './packs/python/files.js';\nimport { getPythonToolDependencyGaps } from './packs/python/setup.js';\nimport { getMissingPacks } from './packs/registry.js';\nimport type { ProjectType } from './packs/types.js';\nimport { typescriptPackages } from './packs/typescript/files.js';\nimport { reconcile } from './reconcile.js';\nimport { BDD_LANE_FILE_PATHS, BDD_LANE_SCRIPT, type SafewordSchema } from './schema.js';\nimport { inspectTicketIndexConflicts, readTickets } from './ticket-sync/index.js';\nimport { listArchitectureRecords } from './utils/architecture-records.js';\nimport {\n type ConfiguredPathKey,\n defaultConfiguredPath,\n readConfiguredDocumentationSources,\n readConfiguredPath,\n resolveConfiguredPath,\n resolveTicketsDirectory,\n} from './utils/configured-paths.js';\nimport { createProjectContext } from './utils/context.js';\nimport {\n collectExecutableFeatureFiles,\n createPhaseAnchorEnvironment,\n featureSourceFileName,\n findFeatureSourcePath,\n hasDefaultExecutableFeatureFiles,\n} from './utils/feature-source.js';\nimport { readFrontmatterScalar } from './utils/frontmatter.js';\nimport { exists, isDirectory, readFileSafe, readJson } from './utils/fs.js';\nimport { FeatureParseError, findFeatureLineageIssues } from './utils/gherkin-feature.js';\nimport { parseGlossary, validateGlossary } from './utils/glossary.js';\nimport { header, info, listItem, success, warn } from './utils/output.js';\nimport { parsePersonas, validatePersonas } from './utils/personas.js';\nimport { toRepoRelativePath } from './utils/repo-path.js';\nimport {\n buildCoverageReport,\n buildCoverageReportFromFeature,\n buildSurfaceCoverageReportFromFeature,\n type CoverageReport,\n findMixedCriteriaJtbds,\n findRulesMissingRejectionPaths,\n isRuleId,\n type SurfaceCoverageReport,\n} from './utils/scenario-coverage.js';\nimport { buildIndexConflictListMessage } from './utils/ticket-index-warnings.js';\nimport { formatTicketReference } from './utils/ticket-reference.js';\nimport { findDanglingDependencies, findTicketsInCycles } from './utils/ticket-relations.js';\nimport { WORKSPACE_ROOTS } from './utils/workspace-roots.js';\nimport { VERSION } from './version.js';\n\n/**\n * Check for missing files from write actions\n * @param cwd\n * @param actions\n */\nfunction findMissingFiles(cwd: string, actions: { type: string; path: string }[]): string[] {\n const issues: string[] = [];\n for (const action of actions) {\n if (action.type === 'write' && !exists(nodePath.join(cwd, action.path))) {\n issues.push(`Missing: ${action.path}`);\n }\n }\n return issues;\n}\n\nfunction missingClaudeSettingsIssues(cwd: string, schema: SafewordSchema): string[] {\n const required = '.claude/settings.json' in schema.jsonMerges;\n return required && !exists(nodePath.join(cwd, '.claude', 'settings.json'))\n ? ['Missing: .claude/settings.json']\n : [];\n}\n\n// Persona and glossary content validation stays separate because their parsers\n// and diagnostics differ. Their configured-path lifecycle is shared with the\n// other user-authored project-knowledge files below. The parallel lookup and\n// validation paths were assessed in ticket XEP59N; combining their different\n// matching and diagnostic rules would cost clarity.\n\n/**\n * Validate personas.md when present, routing through any configured\n * `paths.personas` override. Returns one issue string per persona\n * validation error, formatted as `personas.md:LINE: MESSAGE`.\n *\n * Two failure modes:\n * - Default location absent → no issue (scaffold is optional until JTBDs\n * reference personas).\n * - Configured override set but file absent → loud failure (user opted\n * in; typo would otherwise silently strand persona references). Ticket\n * K7N2QM.\n */\ntype ConfiguredKnowledgeFileKey = Extract<\n ConfiguredPathKey,\n 'glossary' | 'principles' | 'personas' | 'surfaces'\n>;\n\nconst PATH_ONLY_KNOWLEDGE_KEYS = ['principles', 'surfaces'] as const;\nconst CONFIGURED_KNOWLEDGE_KEYS: readonly ConfiguredKnowledgeFileKey[] = [\n 'personas',\n 'principles',\n 'surfaces',\n 'glossary',\n];\n\n/** Factual configured-file health shared by user-authored project knowledge. */\nfunction findConfiguredKnowledgeIssues(cwd: string, key: ConfiguredKnowledgeFileKey): string[] {\n const override = readConfiguredPath(cwd, key);\n if (override === undefined) return [];\n const configuredPath = resolveConfiguredPath(cwd, key);\n if (isDirectory(configuredPath)) return [`${key}-path: ${override}: not a file`];\n if (exists(configuredPath)) return [];\n return [`${key}-path: ${override}: file not found`];\n}\n\n/** Non-destructively identify a default knowledge file stranded by an override. */\nfunction findConfiguredKnowledgeAdvisories(cwd: string, key: ConfiguredKnowledgeFileKey): string[] {\n const override = readConfiguredPath(cwd, key);\n if (override === undefined) return [];\n const defaultPath = defaultConfiguredPath(cwd, key);\n if (!exists(defaultPath)) return [];\n return [\n `${nodePath.relative(cwd, defaultPath)} exists but paths.${key} points to ${override} — legacy file is orphaned. Consider removing.`,\n ];\n}\n\nfunction findPersonaIssues(cwd: string): string[] {\n const configuredIssues = findConfiguredKnowledgeIssues(cwd, 'personas');\n if (configuredIssues.length > 0) return configuredIssues;\n const filePath = resolveConfiguredPath(cwd, 'personas');\n const content = readFileSafe(filePath);\n\n if (content === undefined) return [];\n\n const errors = validatePersonas(parsePersonas(content));\n return errors.map(error => `personas.md:${error.line}: ${error.message}`);\n}\n\n/**\n * Both-namespace-roots advisory (ticket 9MMWS7): both `.project/` and\n * `.safeword-project/` present means a migration was left half-finished (or\n * the dirs were created independently). Zero-exit nudge naming the finishing\n * action; silent on any single root — declining migration is never a nag.\n */\nfunction findNamespaceAdvisories(cwd: string): string[] {\n if (\n isDirectory(nodePath.join(cwd, '.project')) &&\n isDirectory(nodePath.join(cwd, '.safeword-project'))\n ) {\n return [\n 'Both .project/ and .safeword-project/ exist — safeword reads .project/. Run `safeword project install` to merge them automatically; authored collisions are archived under .safeword/namespace-migration-conflicts-v1/.',\n ];\n }\n return [];\n}\n\n/**\n * Validate glossary.md when present, routing through any configured\n * `paths.glossary` override. Returns one issue string per glossary\n * validation error, formatted as `glossary.md:LINE: MESSAGE`. Same two\n * failure modes as {@link findPersonaIssues} — absent default is silent\n * (scaffold is optional), configured-but-missing fails loudly (ticket\n * YR6C49, mirrors K7N2QM).\n */\nfunction findGlossaryIssues(cwd: string): string[] {\n const configuredIssues = findConfiguredKnowledgeIssues(cwd, 'glossary');\n if (configuredIssues.length > 0) return configuredIssues;\n const filePath = resolveConfiguredPath(cwd, 'glossary');\n const content = readFileSafe(filePath);\n\n if (content === undefined) return [];\n\n const errors = validateGlossary(parseGlossary(content));\n return errors.map(error => `glossary.md:${error.line}: ${error.message}`);\n}\n\n/**\n * Cucumber-harness misalignment advisories (ticket 56JCFZ, TB3.AC1+AC2).\n * Persistent and zero-exit — the setup-time notice alone is ignorable:\n * - Host harness detected, safeword's lane absent, `paths.*` unset → name\n * the harness and the exact config lines to add. Silent once set.\n * - Host harness AND safeword's starter lane both present (a repo bitten by\n * pre-56JCFZ scaffolding) → enumerate the leftover lane surface, derived\n * from the schema constants so the list can't drift. Never edits/deletes.\n */\nfunction findCucumberHarnessAdvisories(\n cwd: string,\n // Detection already ran once for this invocation (createProjectContext →\n // detectProjectType); reuse it instead of re-sweeping the filesystem.\n { existingCucumberHarness, scaffoldBddLane }: ProjectType,\n): string[] {\n if (existingCucumberHarness === undefined) return [];\n\n if (scaffoldBddLane) {\n if (isWorkspaceCucumberHarness(existingCucumberHarness)) {\n return [\n `A workspace Cucumber harness (${existingCucumberHarness}) is preserved alongside Safeword's root starter lane. This is informational: Safeword does not remove or modify project test infrastructure. The starter lane reads the default workspace features directories; set paths.features / paths.steps only when this harness uses nonstandard locations.`,\n ];\n }\n return [buildLeftoverLaneAdvisory(cwd, existingCucumberHarness)];\n }\n\n // paths.features alone silences this: the readers consume only the\n // features directory. paths.steps matters only to the scaffolded runner\n // (relocated TypeScript steps), which a host-harness repo isn't using.\n if (readConfiguredPath(cwd, 'features') !== undefined || hasDefaultExecutableFeatureFiles(cwd)) {\n return [];\n }\n return [\n `Detected a cucumber harness (${existingCucumberHarness}) but paths.features is not set in .safeword/config.json — project codify, project lint-gherkin, and doctor cannot see your suite. Add e.g. \"paths\": { \"features\": \"tests/behaviors\", \"steps\": \"tests/steps\" } (paths.steps only matters when the scaffolded runner reads relocated TypeScript steps).`,\n ];\n}\n\nfunction isWorkspaceCucumberHarness(evidence: string): boolean {\n return WORKSPACE_ROOTS.some(root => evidence.startsWith(`${root}/`));\n}\n\n/** Enumerate the starter-lane leftovers (files/deps/script) from schema constants. */\nfunction buildLeftoverLaneAdvisory(cwd: string, evidence: string): string {\n const leftovers: string[] = BDD_LANE_FILE_PATHS.filter(filePath =>\n exists(nodePath.join(cwd, filePath)),\n );\n\n const manifest = readPackageJsonSafe(cwd);\n const developmentDependencies = manifest?.devDependencies ?? {};\n leftovers.push(\n ...typescriptPackages.conditional.scaffoldBddLane.filter(dependency =>\n Object.hasOwn(developmentDependencies, dependency),\n ),\n );\n if (typeof manifest?.scripts?.[BDD_LANE_SCRIPT] === 'string') {\n leftovers.push(`\"${BDD_LANE_SCRIPT}\" script`);\n }\n\n return `Duplicate BDD lane: a cucumber harness (${evidence}) coexists with safeword's starter lane. Leftover scaffold: ${leftovers.join(', ')}. Safeword never removes these — if the host harness is the real suite, delete the leftovers and set paths.features / paths.steps in .safeword/config.json so safeword reads it.`;\n}\n\ninterface PackageJsonManifest {\n scripts?: Record<string, unknown>;\n devDependencies?: Record<string, string>;\n}\n\nfunction readPackageJsonSafe(cwd: string): PackageJsonManifest | undefined {\n return readJson(nodePath.join(cwd, 'package.json')) as PackageJsonManifest | undefined;\n}\n\nfunction findDocumentationSourceIssues(cwd: string): string[] {\n return readConfiguredDocumentationSources(cwd).flatMap(source => {\n if (source.type !== 'local') return [];\n return exists(source.resolvedPath)\n ? []\n : [`docs-source: ${source.path}: file or directory not found`];\n });\n}\n\n/** Ticket folder names under the tickets root (excluding `completed/`), or\n * empty when the root is missing/unreadable. */\nfunction listTicketIds(ticketsRoot: string): string[] {\n try {\n return readdirSync(ticketsRoot, { withFileTypes: true })\n .filter(entry => entry.isDirectory() && entry.name !== 'completed')\n .map(entry => entry.name);\n } catch {\n return [];\n }\n}\n\n/**\n * Surface architecture-claim mismatches as non-blocking advisories (ticket\n * K4BWTQ). Structural only — no prose extraction (YR6C49 ruling): when an\n * in-progress ticket's impl-plan.md Design alignment section carries content\n * (not `skip:`) but the resolved `paths.architecture` location does not\n * exist, the claim cannot be honoring anything recorded. Zero-exit.\n */\nfunction findArchitectureAdvisories(cwd: string): string[] {\n const ticketsRoot = resolveTicketsDirectory(cwd);\n const ticketIds = listTicketIds(ticketsRoot);\n\n const resolved = resolveConfiguredPath(cwd, 'architecture');\n if (listArchitectureRecords(resolved).kind !== 'absent') return [];\n\n return ticketIds.flatMap(ticketId => {\n const ticketDirectory = nodePath.join(ticketsRoot, ticketId);\n const ticketContent = readFileSafe(nodePath.join(ticketDirectory, 'ticket.md'));\n if (ticketContent === undefined || !isInProgress(ticketContent)) return [];\n const implPlan = readFileSafe(nodePath.join(ticketDirectory, 'impl-plan.md'));\n if (implPlan === undefined) return [];\n if (!archAlignmentHasContent(implPlan)) return [];\n return [\n `${ticketId}: impl-plan.md Design alignment claims alignment, but no architecture record exists at ${resolved} — record the decision or mark the section skip:`,\n ];\n });\n}\n\n/** Whether the impl plan's canonical or legacy alignment section carries real content\n * (non-empty, not a `skip:` annotation). */\nfunction archAlignmentHasContent(implPlanContent: string): boolean {\n let isInSection = false;\n const body: string[] = [];\n for (const raw of implPlanContent.split('\\n')) {\n const line = raw.trim();\n if (line.startsWith('## ')) {\n const heading = line.slice(3).trim().toLowerCase();\n isInSection = heading === 'design alignment' || heading === 'arch alignment';\n continue;\n }\n // Principle alignment is the structured Markdown table in this shared\n // section. Architecture claims are the remaining prose below it.\n if (isInSection && line !== '' && !line.startsWith('|')) body.push(line);\n }\n if (body.length === 0) return false;\n return !(body.length === 1 && (body[0] ?? '').toLowerCase().startsWith('skip:'));\n}\n\n/**\n * Surface scenario-lineage coverage gaps as non-blocking advisories (ticket\n * XT1FFM). In a Git worktree, scoped to the current diff's in-progress tickets\n * and feature sources; projects without Git retain the original whole-project\n * view. This excludes done predecessors whose pre-scheme scenarios are the\n * out-of-scope migration case (epic DZ2NM5/D5), and keeps the report focused\n * on the work the developer is actually building. Each selected ticket's feature\n * source or legacy (spec.md, test-definitions.md) pair is cross-referenced into\n * uncovered ACs, stale AC refs, and orphan scenarios. Coverage gaps stay zero-exit\n * advisories; invalid Gherkin is a health issue because the source cannot be read.\n */\ninterface CoverageDiagnostics {\n issues: string[];\n advisories: string[];\n}\n\nfunction emptyCoverageDiagnostics(): CoverageDiagnostics {\n return { issues: [], advisories: [] };\n}\n\nconst CURRENT_WORK_BASE_REFS = [\n 'refs/remotes/origin/HEAD',\n 'refs/remotes/origin/main',\n 'refs/remotes/origin/master',\n 'refs/heads/main',\n 'refs/heads/master',\n] as const;\n\n/** Read Git output without letting a failed scope probe crash health. */\nfunction readGit(cwd: string, args: readonly string[]): string | undefined {\n try {\n return execFileSync('git', args, {\n cwd,\n encoding: 'utf8',\n maxBuffer: 10 * 1024 * 1024,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n } catch {\n return undefined;\n }\n}\n\nfunction addNulSeparatedPaths(target: Set<string>, output: string): void {\n for (const path of output.split('\\0')) {\n if (path !== '') target.add(path);\n }\n}\n\nfunction currentWorkPathsWithHead(cwd: string): string[] | undefined {\n const baseReference = CURRENT_WORK_BASE_REFS.find(\n candidate => readGit(cwd, ['rev-parse', '--verify', '--quiet', candidate]) !== undefined,\n );\n if (baseReference === undefined) return undefined;\n const mergeBase = readGit(cwd, ['merge-base', 'HEAD', baseReference])?.trim();\n if (mergeBase === undefined || mergeBase === '') return undefined;\n const committed = readGit(cwd, [\n 'diff',\n '--relative',\n '--name-only',\n '-z',\n `${mergeBase}...HEAD`,\n ]);\n const working = readGit(cwd, ['diff', '--relative', '--name-only', '-z', 'HEAD']);\n if (committed === undefined || working === undefined) return undefined;\n\n const paths = new Set<string>();\n addNulSeparatedPaths(paths, committed);\n addNulSeparatedPaths(paths, working);\n return [...paths];\n}\n\nfunction currentWorkPathsWithoutHead(cwd: string): string[] | undefined {\n const staged = readGit(cwd, ['diff', '--cached', '--name-only', '-z']);\n if (staged === undefined) return undefined;\n const paths = new Set<string>();\n addNulSeparatedPaths(paths, staged);\n return [...paths];\n}\n\n/**\n * Current branch + worktree paths, or undefined when no trustworthy scope can\n * be derived. An unavailable Git base retains the whole-project diagnostic so\n * exit-code-affecting Gherkin validation is never silently skipped.\n */\nfunction currentWorkPaths(cwd: string): string[] | undefined {\n if (readGit(cwd, ['rev-parse', '--is-inside-work-tree'])?.trim() !== 'true') return undefined;\n\n const hasHead = readGit(cwd, ['rev-parse', '--verify', '--quiet', 'HEAD^{commit}']);\n const changedPaths =\n hasHead === undefined ? currentWorkPathsWithoutHead(cwd) : currentWorkPathsWithHead(cwd);\n if (changedPaths === undefined) return undefined;\n\n const untracked = readGit(cwd, ['ls-files', '--others', '--exclude-standard', '-z']);\n if (untracked === undefined) return undefined;\n const paths = new Set(changedPaths);\n addNulSeparatedPaths(paths, untracked);\n return [...paths];\n}\n\nfunction ticketIdsFromChangedPaths(paths: readonly string[], ticketPrefix: string): Set<string> {\n const ticketIds = new Set<string>();\n for (const path of paths) {\n if (!path.startsWith(ticketPrefix)) continue;\n const [ticketId] = path.slice(ticketPrefix.length).split('/', 1);\n if (ticketId !== undefined && ticketId !== '') ticketIds.add(ticketId);\n }\n return ticketIds;\n}\n\ninterface ChangedFeatureSources {\n paths: ReadonlySet<string>;\n fileNames: ReadonlySet<string>;\n}\n\nfunction addTicketsForChangedFeatureSources(\n cwd: string,\n ticketsRoot: string,\n featureFiles: readonly string[],\n changedFeatures: ChangedFeatureSources,\n ticketIds: Set<string>,\n): void {\n for (const ticketId of listTicketIds(ticketsRoot)) {\n const featurePath = findFeatureSourcePath(cwd, ticketId, featureFiles);\n if (\n (featurePath !== undefined && changedFeatures.paths.has(featurePath)) ||\n (featurePath === undefined &&\n changedFeatures.fileNames.has(featureSourceFileName(cwd, ticketId)))\n ) {\n ticketIds.add(ticketId);\n }\n }\n}\n\n/** The tickets whose metadata or executable feature source enters current work. */\nfunction currentWorkCoverageTicketIds(\n cwd: string,\n ticketsRoot: string,\n featureFiles: readonly string[],\n): string[] {\n const paths = currentWorkPaths(cwd);\n if (paths === undefined) return listTicketIds(ticketsRoot);\n if (paths.length === 0) return [];\n\n const ticketsRelative = toRepoRelativePath(cwd, ticketsRoot).split(nodePath.sep).join('/');\n if (ticketsRelative === '..' || ticketsRelative.startsWith('../')) return [];\n const ticketPrefix = `${ticketsRelative}/`;\n const changedFeatures: ChangedFeatureSources = {\n paths: new Set(\n paths.filter(path => path.endsWith('.feature')).map(path => nodePath.resolve(cwd, path)),\n ),\n fileNames: new Set(\n paths.filter(path => path.endsWith('.feature')).map(path => nodePath.basename(path)),\n ),\n };\n const ticketIds = ticketIdsFromChangedPaths(paths, ticketPrefix);\n if (changedFeatures.paths.size === 0) return [...ticketIds];\n\n addTicketsForChangedFeatureSources(cwd, ticketsRoot, featureFiles, changedFeatures, ticketIds);\n return [...ticketIds];\n}\n\nfunction findCoverageDiagnostics(cwd: string): CoverageDiagnostics {\n const ticketsRoot = resolveTicketsDirectory(cwd);\n const all = emptyCoverageDiagnostics();\n const configuredFeatures = readConfiguredPath(cwd, 'features');\n const anchorEnvironment = createPhaseAnchorEnvironment(cwd, configuredFeatures);\n const featureFiles = collectExecutableFeatureFiles(cwd);\n for (const ticketId of currentWorkCoverageTicketIds(cwd, ticketsRoot, featureFiles)) {\n const ticketDiagnostics = coverageDiagnosticsForTicket(\n cwd,\n ticketsRoot,\n ticketId,\n featureFiles,\n );\n all.issues.push(...ticketDiagnostics.issues);\n all.advisories.push(...ticketDiagnostics.advisories);\n const anchorAdvisory = phaseAnchorAdvisoryForTicket(\n cwd,\n ticketsRoot,\n ticketId,\n anchorEnvironment,\n );\n if (anchorAdvisory !== undefined) all.advisories.push(anchorAdvisory);\n }\n return all;\n}\n\n/**\n * Phase-anchor advisory (issue #824, epic #808; HGYGND artifact-path grammar):\n * an in-progress feature ticket whose current phase carries no valid\n * `phase_anchors` entry gets a zero-exit nudge — the at-rest view of the\n * anchor substrate, verified against the working tree (a filesystem reader is\n * injected, so existence + shape are checked, not just format). Advisory only:\n * enforcement belongs to the deliverable-boundary gate (#810). Hex-shaped\n * legacy anchors are grandfathered by the detector and never re-litigated.\n */\nfunction phaseAnchorAdvisoryForTicket(\n cwd: string,\n ticketsRoot: string,\n ticketId: string,\n anchorEnvironment: Omit<PhaseAnchorScope, 'ticketPath'>,\n): string | undefined {\n const content = readFileSafe(nodePath.join(ticketsRoot, ticketId, 'ticket.md'));\n if (content === undefined || !isInProgress(content)) return undefined;\n const ticketDirectory = nodePath.join(ticketsRoot, ticketId);\n const ticketPath = toRepoRelativePath(cwd, ticketDirectory);\n const scope = { ticketPath, ...anchorEnvironment };\n const verdict = detectUnanchoredPhaseState(content, scope, relpath =>\n readFileSafe(nodePath.join(cwd, relpath)),\n );\n if (verdict.kind !== 'unanchored') return undefined;\n return `${formatCoverageTicketLabel(ticketId, content)}: ${verdict.reason} The anchor is the exited phase's artifact — boundary checks verify it against the tree.`;\n}\n\n/** Build coverage advisories for one ticket, or none if it is not an\n * in-progress, spec-bearing ticket. */\nfunction coverageDiagnosticsForTicket(\n cwd: string,\n ticketsRoot: string,\n ticketId: string,\n featureFiles: readonly string[],\n): CoverageDiagnostics {\n const ticketDirectory = nodePath.join(ticketsRoot, ticketId);\n const ticketContent = readFileSafe(nodePath.join(ticketDirectory, 'ticket.md'));\n if (ticketContent === undefined || !isInProgress(ticketContent))\n return emptyCoverageDiagnostics();\n\n const specContent = readFileSafe(nodePath.join(ticketDirectory, 'spec.md'));\n if (specContent === undefined) return emptyCoverageDiagnostics();\n\n const featureSource = readFeatureSource(cwd, ticketId, featureFiles);\n try {\n const report =\n featureSource === undefined\n ? buildCoverageReport(\n specContent,\n readFileSafe(nodePath.join(ticketDirectory, 'test-definitions.md')),\n )\n : buildCoverageReportFromFeature(specContent, featureSource.content);\n const surfaceReport =\n featureSource === undefined\n ? undefined\n : buildSurfaceCoverageReportFromFeature(specContent, featureSource.content);\n const lineageIssues =\n featureSource === undefined\n ? []\n : formatFeatureLineageIssues(cwd, ticketId, featureSource, ticketContent);\n const ruleTier = ruleTierDiagnostics(ticketId, specContent, featureSource, ticketContent);\n return {\n issues: [...lineageIssues, ...ruleTier.issues],\n advisories: [\n ...formatCoverageReport(ticketId, report, ticketContent),\n ...ruleTier.advisories,\n ...formatSurfaceCoverageReport(ticketId, surfaceReport, ticketContent),\n ],\n };\n } catch (parseError: unknown) {\n if (parseError instanceof FeatureParseError && featureSource !== undefined) {\n return {\n issues: [\n `${formatCoverageTicketLabel(ticketId, ticketContent)}: ${nodePath.relative(cwd, featureSource.path)}: invalid Gherkin feature: ${parseError.message}`,\n ],\n advisories: [],\n };\n }\n throw parseError;\n }\n}\n\n/** Rule-tier findings for one ticket: mixed-criteria JTBDs (issues) and\n * numbered rules missing a rejection-path scenario (advisories). */\nfunction ruleTierDiagnostics(\n ticketId: string,\n specContent: string,\n featureSource: FeatureSource | undefined,\n ticketContent: string,\n): CoverageDiagnostics {\n const label = formatCoverageTicketLabel(ticketId, ticketContent);\n return {\n issues: findMixedCriteriaJtbds(specContent).map(\n jtbd =>\n `${label}: JTBD ${jtbd} declares both Acceptance Criteria and numbered Rules; keep one criteria kind per job — convert one set or split the job`,\n ),\n advisories:\n featureSource === undefined\n ? []\n : findRulesMissingRejectionPaths(specContent, featureSource.content).map(\n ruleId =>\n `${label}: numbered rule ${ruleId} has no example of the rule being broken — add a @rejection-tagged scenario under it`,\n ),\n };\n}\n\nfunction formatFeatureLineageIssues(\n cwd: string,\n ticketId: string,\n featureSource: FeatureSource,\n ticketContent: string,\n): string[] {\n const label = formatCoverageTicketLabel(ticketId, ticketContent);\n const relativePath = nodePath.relative(cwd, featureSource.path);\n return findFeatureLineageIssues(featureSource.content).map(\n issue => `${label}: ${relativePath}: ${issue}`,\n );\n}\n\ninterface FeatureSource {\n path: string;\n content: string;\n}\n\nfunction readFeatureSource(\n cwd: string,\n ticketFolder: string,\n featureFiles: readonly string[],\n): FeatureSource | undefined {\n const featurePath = findFeatureSourcePath(cwd, ticketFolder, featureFiles);\n const content = featurePath === undefined ? undefined : readFileSafe(featurePath);\n return featurePath === undefined || content === undefined\n ? undefined\n : { path: featurePath, content };\n}\n\n/** Whether a ticket.md's frontmatter declares `status: in_progress`. */\nfunction isInProgress(ticketContent: string): boolean {\n const lines = ticketContent.split('\\n');\n if (lines[0]?.trim() !== '---') return false;\n for (let index = 1; index < lines.length; index += 1) {\n const line = (lines[index] ?? '').trim();\n if (line === '---') return false;\n if (line === 'status: in_progress') return true;\n }\n return false;\n}\n\n/** Render a coverage report into one advisory string per finding. */\nfunction formatCoverageReport(\n ticketId: string,\n report: CoverageReport,\n ticketContent: string,\n): string[] {\n const ticketLabel = formatCoverageTicketLabel(ticketId, ticketContent);\n return [\n ...report.uncovered.map(id =>\n isRuleId(id)\n ? `${ticketLabel}: numbered rule ${id} has no scenario illustrating it (uncovered) — add a scenario tagged @${id}`\n : `${ticketLabel}: acceptance criterion ${id} has no scenario (uncovered)`,\n ),\n ...report.stale.map(reference =>\n isRuleId(reference)\n ? `${ticketLabel}: scenario ref ${reference} matches no numbered rule under its JTBD (stale ref) — retag to a declared rule or declare it in spec.md`\n : `${ticketLabel}: scenario ref ${reference} matches no AC under its JTBD (stale ref)`,\n ),\n ...report.orphan.map(reference =>\n isRuleId(reference)\n ? `${ticketLabel}: scenario ref ${reference} names no JTBD in spec.md (orphan) — fix the tag's JTBD id or add that JTBD to spec.md`\n : `${ticketLabel}: scenario ref ${reference} names no JTBD in spec.md (orphan)`,\n ),\n ];\n}\n\nfunction formatSurfaceCoverageReport(\n ticketId: string,\n report: SurfaceCoverageReport | undefined,\n ticketContent: string,\n): string[] {\n if (report === undefined) return [];\n const ticketLabel = formatCoverageTicketLabel(ticketId, ticketContent);\n return [\n ...report.missing.map(\n surface =>\n `${ticketLabel}: affected surface ${surface.name} has no @surface.* scenario tag (uncovered surface)`,\n ),\n ...report.stale.map(\n slug =>\n `${ticketLabel}: scenario surface tag @surface.${slug} is not listed under spec.md ## Surfaces Affected (stale surface)`,\n ),\n ];\n}\n\nfunction formatCoverageTicketLabel(ticketId: string, ticketContent?: string): string {\n const id = readFrontmatterScalar(ticketContent, 'id');\n const slug = readFrontmatterScalar(ticketContent, 'slug');\n if (id !== undefined && slug !== undefined) return formatTicketReference(id, slug);\n\n const dashIndex = ticketId.indexOf('-');\n return dashIndex === -1\n ? ticketId\n : formatTicketReference(ticketId.slice(0, dashIndex), ticketId.slice(dashIndex + 1));\n}\n\nfunction ticketReferenceOf(id: string, labelById: ReadonlyMap<string, string>): string {\n const title = labelById.get(id);\n return title === undefined ? id : formatTicketReference(id, title);\n}\n\n/**\n * Warn-only checks (dangling + cycle) over one directed relation field —\n * depends_on (soft, AKZJXC) and blocked_on (hard, MBGQ89). Generic over the edge\n * set; only the field name and cycle wording differ.\n */\nfunction relationAdvisoriesFor(\n nodes: { id: string; dependsOn: string[] }[],\n danglingField: string,\n cycleLabel: string,\n labelById: ReadonlyMap<string, string>,\n): string[] {\n const dangling = findDanglingDependencies(nodes).map(\n ({ from, missing }) =>\n `${ticketReferenceOf(from, labelById)}: ${danglingField} ${missing} — no such ticket (dangling ref)`,\n );\n const cyclic = findTicketsInCycles(nodes);\n const cycle =\n cyclic.length > 0\n ? [\n `${cycleLabel} cycle among: ${cyclic.map(id => ticketReferenceOf(id, labelById)).join(', ')} (break the loop)`,\n ]\n : [];\n return [...dangling, ...cycle];\n}\n\n/**\n * Surface structured-relation problems as non-blocking advisories (ticket\n * AKZJXC): a `depends_on` pointing at a ticket absent from the corpus (dangling\n * ref), and dependency cycles (A→B→A). Warn-only — a target may live on another\n * branch or in completed/, and a cycle is a planning smell, not a config fault.\n * Reads the full corpus (active + completed) so cross-status edges resolve.\n * Zero-exit.\n */\nfunction findRelationAdvisories(cwd: string): string[] {\n const ticketsDirectory = resolveTicketsDirectory(cwd);\n let entries;\n try {\n const { active, completed } = readTickets(ticketsDirectory);\n entries = [...active, ...completed];\n } catch {\n return [];\n }\n\n const labelById = new Map(entries.map(entry => [entry.id, entry.title]));\n const statusById = new Map(entries.map(entry => [entry.id, entry.status]));\n\n // MBGQ89: a blocked_on_override is stale once every listed blocker is `done`\n // (done auto-unblocks, so the override no longer does anything — clean it up).\n const staleOverrides = entries\n .filter(\n entry =>\n entry.blockedOnOverride !== undefined &&\n entry.blockedOn.length > 0 &&\n entry.blockedOn.every(id => statusById.get(id) === 'done'),\n )\n .map(\n entry =>\n `${ticketReferenceOf(entry.id, labelById)}: blocked_on_override is stale — every blocker is done; remove it`,\n );\n\n return [\n ...relationAdvisoriesFor(\n entries.map(entry => ({ id: entry.id, dependsOn: entry.dependsOn })),\n 'depends_on',\n 'dependency',\n labelById,\n ),\n ...relationAdvisoriesFor(\n entries.map(entry => ({ id: entry.id, dependsOn: entry.blockedOn })),\n 'blocked_on',\n 'blocked_on',\n labelById,\n ),\n ...staleOverrides,\n ];\n}\n\n/**\n * Check for missing text patch markers\n * @param cwd\n * @param actions\n */\nfunction findMissingPatches(\n cwd: string,\n actions: { type: string; path: string; definition?: { marker: string } }[],\n): string[] {\n const issues: string[] = [];\n for (const action of actions) {\n if (action.type !== 'text-patch') continue;\n\n const fullPath = nodePath.join(cwd, action.path);\n if (exists(fullPath)) {\n const content = readFileSafe(fullPath) ?? '';\n if (action.definition && !content.includes(action.definition.marker)) {\n issues.push(`${action.path} missing safeword link`);\n }\n } else {\n issues.push(`${action.path} file missing`);\n }\n }\n return issues;\n}\n\nexport interface HealthStatus {\n configured: boolean;\n projectVersion: string | undefined;\n cliVersion: string;\n updateAvailable: boolean;\n latestVersion: string | undefined;\n issues: string[];\n /**\n * Non-blocking diagnostics — reported to the user but do NOT gate\n * non-zero exit. Use for situations where safeword's operation is\n * unaffected but a cleanup or attention is warranted (e.g., legacy\n * default-location file orphaned by a configured `paths.*` override).\n */\n advisories: string[];\n missingPackages: string[];\n missingPacks: string[];\n missingPythonTools: string[];\n}\n\n/**\n * Check project configuration health using reconcile dryRun\n * @param cwd\n */\nexport interface CheckHealthOptions {\n /**\n * When true, package/pack installation state is excluded from the health\n * result — `missingPackages` and `missingPacks` come back empty regardless\n * of what is on disk. Used by the setup/upgrade self-verify when install was\n * deliberately skipped (`SAFEWORD_SKIP_INSTALL`): a command that was told not\n * to install must not then fault itself for absent packages. Config-file\n * health (missing files, broken patches, persona/glossary) is still checked.\n * Standalone `check` leaves this false so the diagnostic reports the truth.\n */\n skipPackageChecks?: boolean;\n /** Ignore declaration gaps during a fresh setup that already emitted manual install guidance. */\n skipPythonToolChecks?: boolean;\n /** Schema view used by the mutating command whose postcondition is being checked. */\n schema?: SafewordSchema;\n}\n\nfunction findMissingPythonToolDeclarations(\n cwd: string,\n context: ReturnType<typeof createProjectContext>,\n): string[] {\n if (!context.languages?.python) return [];\n return [\n ...new Set(\n getPythonToolDependencyGaps(cwd, hasImportLinterScaffoldTarget).flatMap(gap => gap.tools),\n ),\n ];\n}\n\nexport async function checkHealth(\n cwd: string,\n options: CheckHealthOptions = {},\n): Promise<HealthStatus> {\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n\n // Check if configured\n if (!exists(safewordDirectory)) {\n return {\n configured: false,\n projectVersion: undefined,\n cliVersion: VERSION,\n updateAvailable: false,\n latestVersion: undefined,\n issues: [],\n advisories: [],\n missingPackages: [],\n missingPacks: [],\n missingPythonTools: [],\n };\n }\n\n // Read project version\n const versionPath = nodePath.join(safewordDirectory, 'version');\n const projectVersion = readFileSafe(versionPath)?.trim() ?? undefined;\n\n // Use reconcile with dryRun to detect issues\n const ctx = createProjectContext(cwd);\n const healthSchema = options.schema ?? schemaForClaudeDelivery(cwd);\n const result = await reconcile(healthSchema, 'upgrade', ctx, {\n dryRun: true,\n });\n\n // Collect issues from write actions and text patches\n // Filter out chmod (paths[] instead of path) and json-merge/unmerge (incompatible definition)\n const actionsWithPath = result.actions.filter(\n (\n a,\n ): a is Exclude<\n (typeof result.actions)[number],\n { type: 'chmod' } | { type: 'json-merge' } | { type: 'json-unmerge' }\n > => a.type !== 'chmod' && a.type !== 'json-merge' && a.type !== 'json-unmerge',\n );\n const issues: string[] = [\n ...findMissingFiles(cwd, actionsWithPath),\n ...findMissingPatches(cwd, actionsWithPath),\n ...findPersonaIssues(cwd),\n ...PATH_ONLY_KNOWLEDGE_KEYS.flatMap(key => findConfiguredKnowledgeIssues(cwd, key)),\n ...findGlossaryIssues(cwd),\n ...findDocumentationSourceIssues(cwd),\n ...missingClaudeSettingsIssues(cwd, healthSchema),\n ];\n\n // Check for missing language packs (unless install was deliberately skipped)\n const missingPacks = options.skipPackageChecks ? [] : getMissingPacks(cwd);\n const missingPythonTools = options.skipPythonToolChecks\n ? []\n : findMissingPythonToolDeclarations(cwd, ctx);\n const coverageDiagnostics = findCoverageDiagnostics(cwd);\n const ticketIndexConflicts = inspectTicketIndexConflicts(cwd);\n issues.push(...coverageDiagnostics.issues);\n\n return {\n configured: true,\n projectVersion,\n cliVersion: VERSION,\n updateAvailable: false,\n latestVersion: undefined,\n issues,\n advisories: [\n ...(ticketIndexConflicts.length === 0\n ? []\n : [buildIndexConflictListMessage(ticketIndexConflicts)]),\n ...findNamespaceAdvisories(cwd),\n ...CONFIGURED_KNOWLEDGE_KEYS.flatMap(key => findConfiguredKnowledgeAdvisories(cwd, key)),\n ...findCucumberHarnessAdvisories(cwd, ctx.projectType),\n ...coverageDiagnostics.advisories,\n ...findRelationAdvisories(cwd),\n ...findArchitectureAdvisories(cwd),\n ],\n missingPackages: options.skipPackageChecks ? [] : result.packagesToInstall,\n missingPacks,\n missingPythonTools,\n };\n}\n\nexport interface ReportHealthOptions {\n /**\n * Replaces the default `Run \\`safeword install\\` …` instruction on every\n * failure branch. Used by the post-upgrade self-verify (ticket 3293WH\n * AC5): reconciliation telling the user to run `safeword install` as the fix is\n * a contradiction — an issue the reconcile just ran couldn't fix won't be\n * fixed by running it again.\n */\n repairHint?: string;\n}\n\ninterface FailureSection {\n title: string;\n lines: string[];\n render: (line: string) => void;\n defaultHint: string;\n}\n\n/** The highest-priority failure section to report, or undefined when healthy.\n * Packs first (explains missing files), then packages, then issues. */\nfunction firstFailureSection(health: HealthStatus): FailureSection | undefined {\n if (health.missingPacks.length > 0) {\n return {\n title: 'Missing Language Packs',\n lines: health.missingPacks.map(pack => `${pack} pack not installed`),\n render: listItem,\n defaultHint: 'Run `safeword install` to install missing packs',\n };\n }\n if (health.missingPackages.length > 0) {\n return {\n title: 'Missing Packages',\n lines: health.missingPackages,\n render: listItem,\n defaultHint: 'Run `safeword install` to install missing packages',\n };\n }\n if (health.missingPythonTools.length > 0) {\n return {\n title: 'Missing Python Tools',\n lines: health.missingPythonTools,\n render: listItem,\n defaultHint: 'Run `safeword install` to declare missing Python tools',\n };\n }\n if (health.issues.length > 0) {\n return {\n title: 'Issues Found',\n lines: health.issues,\n render: warn,\n defaultHint: 'Run `safeword install` to repair configuration',\n };\n }\n return undefined;\n}\n\n/**\n * Report issues or success\n * @param health\n * @param options\n * @returns true if there are issues requiring attention\n */\nexport function reportHealthSummary(\n health: HealthStatus,\n options: ReportHealthOptions = {},\n): boolean {\n // Advisories first: non-blocking diagnostics that must surface even when\n // issues exist (the failure branch below returns early). Ticket 9MMWS7\n // exposed the old ordering, which silently swallowed advisories on any\n // unhealthy project.\n if (health.advisories.length > 0) {\n header('Advisories');\n for (const advisory of health.advisories) {\n warn(advisory);\n }\n }\n\n const failure = firstFailureSection(health);\n if (failure === undefined) {\n success('\\nConfiguration is healthy');\n return false;\n }\n\n header(failure.title);\n for (const line of failure.lines) {\n failure.render(line);\n }\n info(`\\n${options.repairHint ?? failure.defaultHint}`);\n return true;\n}\n","import { SAFEWORD_SCHEMA, type SafewordSchema } from '../schema.js';\nimport { legacyObservationIsEmpty, observeClaudeLegacy } from './legacy-classifier.js';\nimport { hasLegacyClaudePluginMode, readClaudePluginMode } from './migration-state.js';\n\nfunction withoutLegacyClaude<T>(values: Record<string, T>): Record<string, T> {\n return Object.fromEntries(\n Object.entries(values).filter(([path]) => !path.startsWith('.claude/')),\n );\n}\n\n/** Select the project schema for native versus retained legacy Claude delivery. */\nexport function schemaForClaudeDelivery(cwd: string): SafewordSchema {\n const legacyPluginMode = hasLegacyClaudePluginMode(cwd);\n const nativePluginMode = readClaudePluginMode(cwd) !== undefined;\n if (\n !legacyPluginMode &&\n !nativePluginMode &&\n !legacyObservationIsEmpty(observeClaudeLegacy(cwd))\n ) {\n return SAFEWORD_SCHEMA;\n }\n return {\n ...SAFEWORD_SCHEMA,\n ownedDirs: SAFEWORD_SCHEMA.ownedDirs.filter(path => !path.startsWith('.claude')),\n sharedDirs: SAFEWORD_SCHEMA.sharedDirs.filter(path => !path.startsWith('.claude')),\n deprecatedFiles: SAFEWORD_SCHEMA.deprecatedFiles.filter(path => !path.startsWith('.claude/')),\n deprecatedDirs: SAFEWORD_SCHEMA.deprecatedDirs.filter(path => !path.startsWith('.claude')),\n ownedFiles: withoutLegacyClaude(SAFEWORD_SCHEMA.ownedFiles),\n managedFiles: withoutLegacyClaude(SAFEWORD_SCHEMA.managedFiles),\n jsonMerges: withoutLegacyClaude(SAFEWORD_SCHEMA.jsonMerges),\n };\n}\n","/**\n * Lists a project's architecture records (ticket K4BWTQ).\n *\n * The resolved `paths.architecture` location may be a single markdown file\n * (the architecture record itself) or a directory of ADRs — each top-level\n * `.md` file except README.md, accept-any naming, no recursion. See the\n * M6D315 replan for why this reuses `paths.architecture` instead of a\n * separate ADR-location field.\n */\n\nimport { readdirSync, statSync } from 'node:fs';\nimport nodePath from 'node:path';\n\ntype ArchitectureLocationKind = 'file' | 'directory' | 'absent';\n\nexport interface ArchitectureRecords {\n kind: ArchitectureLocationKind;\n /** Absolute paths of the record files; empty when none exist. */\n records: string[];\n}\n\nexport function listArchitectureRecords(resolvedPath: string): ArchitectureRecords {\n // try/catch in addition to throwIfNoEntry: statSync still throws ENOTDIR\n // when a path *component* is a file (nodejs/node#56993) — reachable here\n // via a misconfigured paths.architecture; treat it as absent, not a crash.\n let stats;\n try {\n stats = statSync(resolvedPath, { throwIfNoEntry: false });\n } catch {\n return { kind: 'absent', records: [] };\n }\n if (stats?.isFile()) {\n return { kind: 'file', records: [resolvedPath] };\n }\n if (stats?.isDirectory()) {\n const records = readdirSync(resolvedPath, { withFileTypes: true })\n .filter(entry => entry.isFile() && entry.name.endsWith('.md') && entry.name !== 'README.md')\n .map(entry => nodePath.join(resolvedPath, entry.name));\n return { kind: 'directory', records };\n }\n return { kind: 'absent', records: [] };\n}\n","/**\n * Glossary file model — parsing, validation, and lookup.\n *\n * Project-level glossary lives at the resolved namespace root (or the\n * path configured at `paths.glossary` in `.safeword/config.json`). Each\n * entry is a level-2 markdown block with a `## Term` header, a required\n * `**Definition:**` line, and optional `**Used in:**`, `**Example:**`,\n * `**Do not confuse with:**`, and `**Aliases:**` lines.\n *\n * Schema is intentionally lenient — unknown `**Field:**` lines are\n * tolerated for forward-compat, and the arcade-prototype\n * `**Used in**:` (colon outside the bold) variant parses identically\n * to `**Used in:**`. The required schema is just `## Term` + Definition;\n * everything else evolves per-team.\n *\n * See ticket YR6C49 for the full spec.\n */\n\nimport { readFileSync } from 'node:fs';\n\nimport { resolveConfiguredPath } from './configured-paths.js';\nimport { computeSkipMask, stripInlineComments } from './markdown-sections.js';\nimport { findDuplicates, groupByLine } from './validation.js';\n\n/**\n * A parsed glossary entry — name + Definition (required), plus any\n * optional fields the entry authored. Aliases is always present\n * (possibly empty) so callers can iterate without an optional-chain.\n */\nexport interface ParsedGlossaryEntry {\n name: string;\n definition: string;\n usedIn?: string;\n example?: string;\n doNotConfuseWith?: string;\n aliases: string[];\n /** 1-indexed line number of the `## ` header. */\n lineNumber: number;\n}\n\n/** A validation error with a 1-indexed line reference into the source content. */\nexport interface GlossaryValidationError {\n line: number;\n message: string;\n}\n\n/**\n * Result of resolving a glossary reference against the parsed entries.\n *\n * Discriminated union — `match` is guaranteed when `status === 'valid'`;\n * `suggestion` is only meaningful when `status === 'unknown'`. Callers\n * narrow on `status` without optional chaining.\n */\nexport type GlossaryReferenceResult =\n { status: 'valid'; match: ParsedGlossaryEntry } | { status: 'unknown'; suggestion?: string };\n\n/**\n * Resolve a glossary reference against the on-disk glossary file.\n *\n * Reads from `paths.glossary` in `.safeword/config.json` when configured;\n * falls back to the namespace-root default otherwise. Degrades\n * gracefully on a missing or unreadable file — returns\n * `{ status: 'unknown' }` rather than throwing, regardless of whether the\n * resolved path is the default or a configured override. The loud signal\n * on configured-but-missing lives in `safeword doctor`, not here — keep\n * this lookup cheap and side-effect-free (mirrors\n * `validatePersonaReference`, ticket K7N2QM).\n */\nexport function validateGlossaryReference(cwd: string, input: string): GlossaryReferenceResult {\n let content: string;\n try {\n const filePath = resolveConfiguredPath(cwd, 'glossary');\n content = readFileSync(filePath, 'utf8');\n } catch {\n return { status: 'unknown' };\n }\n return lookupGlossaryReference(parseGlossary(content), input);\n}\n\n/**\n * Look up a glossary reference against parsed entries.\n *\n * Match priority: exact term name → exact alias → casing-mismatch\n * suggestion → unknown. Pure — no I/O.\n */\nexport function lookupGlossaryReference(\n entries: readonly ParsedGlossaryEntry[],\n input: string,\n): GlossaryReferenceResult {\n if (input.length === 0) return { status: 'unknown' };\n\n for (const entry of entries) {\n if (entry.name === input || entry.aliases.includes(input)) {\n return { status: 'valid', match: entry };\n }\n }\n\n // Casing-mismatch detection — suggest the canonical spelling when the\n // only difference is case (on a term name or an alias).\n const lowered = input.toLowerCase();\n for (const entry of entries) {\n if (entry.name.toLowerCase() === lowered) {\n return { status: 'unknown', suggestion: entry.name };\n }\n const aliasMatch = entry.aliases.find(alias => alias.toLowerCase() === lowered);\n if (aliasMatch !== undefined) {\n return { status: 'unknown', suggestion: entry.name };\n }\n }\n\n return { status: 'unknown' };\n}\n\n/**\n * Group alias → header line numbers across all entries. Unlike\n * {@link groupByLine} (one key per entry), each entry contributes one key\n * per declared alias.\n */\nfunction groupAliasesByLine(entries: readonly ParsedGlossaryEntry[]): Map<string, number[]> {\n const grouped = new Map<string, number[]>();\n for (const entry of entries) {\n addEntryAliasesToGroup(entry, grouped);\n }\n return grouped;\n}\n\nfunction addEntryAliasesToGroup(entry: ParsedGlossaryEntry, grouped: Map<string, number[]>): void {\n for (const alias of entry.aliases) {\n if (alias.length === 0) continue;\n const lines = grouped.get(alias) ?? [];\n lines.push(entry.lineNumber);\n grouped.set(alias, lines);\n }\n}\n\n/**\n * Flag aliases that collide with a declared term name. Lookup must\n * resolve a string to exactly one term; an alias that shadows a real\n * term name is ambiguous. A self-alias (alias equal to its own term's\n * name) is harmless redundancy and not flagged.\n */\nfunction findAliasShadowingTerms(\n entries: readonly ParsedGlossaryEntry[],\n): GlossaryValidationError[] {\n const termLines = new Map<string, number>();\n for (const entry of entries) {\n if (entry.name.length > 0 && !termLines.has(entry.name)) {\n termLines.set(entry.name, entry.lineNumber);\n }\n }\n const errors: GlossaryValidationError[] = [];\n for (const entry of entries) {\n for (const alias of entry.aliases) {\n const termLine = termLines.get(alias);\n if (termLine !== undefined && termLine !== entry.lineNumber) {\n errors.push({\n line: entry.lineNumber,\n message: `alias \"${alias}\" shadows term defined at line ${termLine}`,\n });\n }\n }\n }\n return errors;\n}\n\n/**\n * Validate parsed glossary entries. Returns a list of\n * {@link GlossaryValidationError} with 1-indexed line numbers; empty list\n * means the file is well-formed.\n *\n * Checks (each independent, all errors collected — never throws):\n * - Every entry has a non-empty term name.\n * - Every entry has a non-empty `**Definition:**`.\n * - Term names are unique within the file.\n * - Aliases are unique across all terms.\n * - No alias shadows a declared term name (ambiguous lookup).\n */\nexport function validateGlossary(\n entries: readonly ParsedGlossaryEntry[],\n): GlossaryValidationError[] {\n const errors: GlossaryValidationError[] = [];\n for (const entry of entries) {\n if (entry.name.length === 0) {\n errors.push({ line: entry.lineNumber, message: 'header is missing term name' });\n }\n if (entry.definition.trim().length === 0) {\n const label = entry.name.length === 0 ? 'entry' : `\"${entry.name}\"`;\n errors.push({ line: entry.lineNumber, message: `${label} is missing Definition` });\n }\n }\n errors.push(\n ...findDuplicates(\n groupByLine(entries, entry => entry.name),\n 'term',\n ),\n ...findDuplicates(groupAliasesByLine(entries), 'alias'),\n ...findAliasShadowingTerms(entries),\n );\n return errors;\n}\n\n/**\n * The string-valued fields a `**Field:**` line can populate. Aliases is\n * excluded — it parses to an array and does not accumulate across lines.\n */\ntype StringFieldKey = 'definition' | 'usedIn' | 'example' | 'doNotConfuseWith';\n\n/**\n * Maps a `**Field:**` prefix to the corresponding property on\n * `ParsedGlossaryEntry`. Lookup is by exact-prefix; unknown prefixes are\n * silently ignored (forward-compat per ticket scope).\n */\nconst FIELD_PROPERTY_MAP: ReadonlyMap<string, StringFieldKey> = new Map([\n ['**Definition:**', 'definition'],\n ['**Used in:**', 'usedIn'],\n ['**Example:**', 'example'],\n ['**Do not confuse with:**', 'doNotConfuseWith'],\n]);\n\n/**\n * Normalize the colon-outside variant `**Foo**:` to the canonical\n * colon-inside form `**Foo:**` so a single prefix lookup table covers\n * both. Arcade's prototype glossary mixes both conventions on adjacent\n * lines — the parser must tolerate either.\n *\n * Bounded: only inspects the leading `**...**:` segment; no backtracking.\n */\nfunction normalizeFieldColon(line: string): string {\n if (!line.startsWith('**')) return line;\n const closeBold = line.indexOf('**', 2);\n if (closeBold === -1) return line;\n if (line.charAt(closeBold + 2) !== ':') return line;\n // Splice: `<prefix>**` + `:**` + `<rest after `**:`>` →\n // `**Foo**: bar` becomes `**Foo:** bar`.\n return `${line.slice(0, closeBold)}:**${line.slice(closeBold + 3)}`;\n}\n\n/**\n * If the line begins with one of the known `**Field:**` prefixes, return\n * the property + value to assign. Otherwise return undefined.\n */\nfunction parseFieldLine(line: string): { property: StringFieldKey; value: string } | undefined {\n const normalized = normalizeFieldColon(line);\n for (const [prefix, property] of FIELD_PROPERTY_MAP) {\n if (normalized.startsWith(prefix)) {\n return { property, value: normalized.slice(prefix.length).trim() };\n }\n }\n return undefined;\n}\n\n/**\n * Whether a line looks like a `**Field:**` declaration (known or not).\n * Used to terminate continuation accumulation on an unknown field line\n * so it isn't swallowed into the previous field's value. Accepts the\n * colon-outside variant via normalization first.\n */\nfunction looksLikeFieldDeclaration(line: string): boolean {\n const normalized = normalizeFieldColon(line);\n if (!normalized.startsWith('**')) return false;\n // Require non-empty content between the opening `**` and the `:**` close.\n return normalized.indexOf(':**') > 2;\n}\n\n/**\n * Parse the comma-separated alias list from a `**Aliases:** foo, bar` line.\n * Empty trailing-whitespace yields an empty list. Returns undefined when\n * the line isn't an Aliases line.\n */\nfunction parseAliasLine(line: string): string[] | undefined {\n if (!line.startsWith('**Aliases:**')) return undefined;\n const raw = line.slice('**Aliases:**'.length).trim();\n return raw.length === 0 ? [] : raw.split(',').map(part => part.trim());\n}\n\n/**\n * Outcome of applying one line to the active entry:\n * - `field` — a string field was set; the caller accumulates continuation\n * lines into `field`.\n * - `aliases` — the aliases line was consumed; stop accumulating.\n * - `none` — no known prefix matched; the line is a continuation candidate.\n */\ntype LineOutcome =\n { kind: 'field'; field: StringFieldKey } | { kind: 'aliases' } | { kind: 'none' };\n\n/**\n * Apply a recognized field/alias line to the active entry. Unknown\n * `**Field:**` lines are tolerated per ticket scope (returns `none`).\n */\nfunction applyLineToEntry(line: string, entry: ParsedGlossaryEntry): LineOutcome {\n const aliases = parseAliasLine(line);\n if (aliases !== undefined) {\n entry.aliases = aliases;\n return { kind: 'aliases' };\n }\n const field = parseFieldLine(line);\n if (field) {\n entry[field.property] = field.value;\n return { kind: 'field', field: field.property };\n }\n return { kind: 'none' };\n}\n\n/**\n * Append a continuation line to the active string field, soft-wrap style:\n * single space between the existing text and the trimmed continuation.\n */\nfunction appendContinuation(entry: ParsedGlossaryEntry, field: StringFieldKey, line: string): void {\n const existing = entry[field] ?? '';\n const addition = line.trim();\n entry[field] = existing.length === 0 ? addition : `${existing} ${addition}`;\n}\n\n/**\n * Apply one body line (a line within a `## Term` block) to the active\n * entry and return the field that should accumulate subsequent\n * continuation lines. A blank line, an aliases line, or an unknown\n * `**Field:**` declaration resets accumulation (returns undefined).\n */\nfunction consumeBodyLine(\n line: string,\n entry: ParsedGlossaryEntry,\n activeField: StringFieldKey | undefined,\n): StringFieldKey | undefined {\n if (line.trim().length === 0) return undefined;\n const outcome = applyLineToEntry(line, entry);\n if (outcome.kind === 'field') return outcome.field;\n if (outcome.kind === 'aliases' || looksLikeFieldDeclaration(line)) return undefined;\n if (activeField !== undefined) appendContinuation(entry, activeField, line);\n return activeField;\n}\n\n/**\n * If the line is a level-2 header (`## Name`, or a bare/empty `##`),\n * return the (possibly empty) term name with inline comments stripped.\n * Returns undefined for non-header lines. An empty name is surfaced as a\n * validation error downstream, not dropped here — so the bad line still\n * produces an entry the validator can point at.\n */\nfunction parseTermHeader(line: string): string | undefined {\n if (line === '##') return '';\n if (line.startsWith('## ')) return stripInlineComments(line.slice(3)).trim();\n return undefined;\n}\n\n/**\n * Parse glossary entries from markdown content.\n *\n * Walks lines once, tracking the active `## Term` block. Skip-mask hides\n * fenced code and block HTML comments. Inline HTML comments are stripped\n * from header text before name extraction. Known `**Field:**` lines (plus\n * the arcade colon-outside variant) populate the matching property on the\n * active entry; unknown `**Field:**` lines are silently tolerated. Pure\n * — no I/O.\n */\nexport function parseGlossary(content: string): ParsedGlossaryEntry[] {\n const lines = content.split('\\n');\n const skip = computeSkipMask(lines);\n const entries: ParsedGlossaryEntry[] = [];\n let current: ParsedGlossaryEntry | undefined;\n // The field currently accumulating continuation lines. Reset on a blank\n // line, a new `## ` header, or an aliases line.\n let activeField: StringFieldKey | undefined;\n\n for (const [index, line] of lines.entries()) {\n if (skip[index] === true) continue;\n const headerName = parseTermHeader(line);\n if (headerName !== undefined) {\n if (current) entries.push(current);\n current = {\n name: headerName,\n definition: '',\n aliases: [],\n lineNumber: index + 1,\n };\n activeField = undefined;\n continue;\n }\n if (!current) continue;\n activeField = consumeBodyLine(line, current, activeField);\n }\n\n if (current) entries.push(current);\n return entries;\n}\n","/**\n * Shared validation helpers for the `## `-block file models (personas,\n * glossary). Both group parsed entries by a key and flag duplicates with a\n * uniform `duplicate <kind> \"<value>\" (also at line <others>)` message.\n * Extracted per ticket JZXVKN (Rule of Three, after WQ4RH3's skip-mask lift).\n */\n\n/** A validation finding with a 1-indexed line reference into the source. */\nexport interface ValidationIssue {\n line: number;\n message: string;\n}\n\n/**\n * Group entries by a derived key → the 1-indexed header line numbers that\n * produced it. Empty keys are skipped. Works for any parsed entry carrying a\n * `lineNumber` (ParsedPersona, ParsedGlossaryEntry, …).\n */\nexport function groupByLine<T extends { lineNumber: number }>(\n entries: readonly T[],\n pick: (entry: T) => string,\n): Map<string, number[]> {\n const grouped = new Map<string, number[]>();\n for (const entry of entries) {\n const key = pick(entry);\n if (key.length === 0) continue;\n const lines = grouped.get(key) ?? [];\n lines.push(entry.lineNumber);\n grouped.set(key, lines);\n }\n return grouped;\n}\n\n/**\n * Produce duplicate-detection issues from a grouping (key → header line\n * numbers): every key with more than one line yields one issue per line,\n * naming the others. `kind` labels the value class (e.g. \"persona name\",\n * \"persona code\", \"term\", \"alias\").\n */\nexport function findDuplicates(grouped: Map<string, number[]>, kind: string): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n for (const [value, lines] of grouped) {\n if (lines.length <= 1) continue;\n for (const line of lines) {\n const others = lines.filter(other => other !== line).join(', ');\n issues.push({ line, message: `duplicate ${kind} \"${value}\" (also at line ${others})` });\n }\n }\n return issues;\n}\n","/**\n * Persona file model — derivation, parsing, validation, and lookup.\n *\n * Project-level personas live at the resolved namespace root as\n * second-level markdown blocks. Each block has a name, an optional\n * parenthesized short code (auto-derived if absent), a `**Role:**` line,\n * and an optional `**Context:**` block.\n *\n * Persisted short codes follow the compatibility pattern\n * `^[A-Z][A-Z0-9]{1,5}$` (2-6 chars). Newly derived codes use the narrower\n * canonical 3-4 character convention; explicitly authored new codes should use\n * 2-4 characters, while persisted 5-6 character codes remain compatible.\n *\n * See ticket 7YN5QB for the full spec.\n */\n\nimport { readFileSync } from 'node:fs';\n\nimport { resolveConfiguredPath } from './configured-paths.js';\nimport { computeSkipMask, stripInlineComments } from './markdown-sections.js';\nimport { findDuplicates, groupByLine } from './validation.js';\n\n// The three constants below are exported for workspace-internal use (tests\n// asserting the canonical bounds, docs referencing them without hardcoding,\n// future code in the same package). They are deliberately NOT re-exported\n// from `src/presets/typescript/index.ts` — customers interact with persona\n// validation through `safeword doctor`, not by reading these constants\n// directly. Promoting them to safeword's public preset surface would make\n// the values part of safeword's semver contract (changing 6 → 8 would\n// become a breaking change), and there's no current consumer that needs\n// that commitment.\n\n/** Maximum length of a newly derived canonical short code. */\nconst MAX_DERIVED_CODE_LENGTH = 4;\n/** Minimum persona name length — single-char names are rejected at validation. */\nconst MIN_NAME_LENGTH = 2;\n/** Pattern for a valid persona short code. */\nexport const PERSONA_CODE_PATTERN = /^[A-Z][A-Z0-9]{1,5}$/;\n/** Pattern for a newly derived canonical persona short code. */\nexport const CANONICAL_PERSONA_CODE_PATTERN = /^[A-Z][A-Z0-9]{2,3}$/;\n\n/**\n * Derive a short code from a persona name.\n *\n * Single-word names use their first 3 characters (\"Auditor\" → \"AUD\").\n * Two-word names use two characters from the first word plus the second word's\n * initial (\"Platform Operator\" → \"PLO\"). Names with 3+ words use their first\n * 4 initials. Apostrophes are removed within a word; other punctuation separates\n * words; digits are preserved.\n *\n * Note: the returned code may not pass {@link PERSONA_CODE_PATTERN} for\n * pathological inputs (e.g., digit-first names like \"3 Amigos\" → \"3A\").\n * Pattern enforcement happens at validation time, not derivation time.\n */\nexport function derivePersonaCode(name: string): string {\n const trimmed = name.trim();\n if (trimmed.length === 0) return '';\n\n const withoutApostrophes = trimmed.replaceAll(/['’]/g, '');\n const cleaned = withoutApostrophes.replaceAll(/[^A-Z0-9\\s]/gi, ' ');\n const words = cleaned.split(/\\s+/).filter(word => word.length > 0);\n\n const [firstWord] = words;\n if (!firstWord) return '';\n\n // String.charAt returns '' for empty strings — no narrowing needed and\n // no non-null assertion (each word is non-empty per the filter above,\n // but TypeScript can't prove that on indexed access).\n let derived: string;\n if (words.length === 1) {\n derived = firstWord.slice(0, 3);\n } else if (words.length === 2) {\n derived = `${firstWord.slice(0, 2)}${words[1]?.charAt(0) ?? ''}`;\n } else {\n derived = words.map(word => word.charAt(0)).join('');\n }\n\n return derived.toUpperCase().slice(0, MAX_DERIVED_CODE_LENGTH);\n}\n\n/** Derive the pre-canonical code used by safeword before 3–4 letter defaults. */\nfunction deriveLegacyPersonaCode(name: string): string {\n const cleaned = name.trim().replaceAll(/[^A-Z0-9\\s]/gi, '');\n const words = cleaned.split(/\\s+/).filter(word => word.length > 0);\n const [firstWord] = words;\n if (!firstWord) return '';\n\n const derived =\n words.length === 1 ? firstWord.slice(0, 2) : words.map(word => word.charAt(0)).join('');\n return derived.toUpperCase().slice(0, 6);\n}\n\n/** Whether a persona name passes the minimum-length requirement. */\nexport function isValidPersonaName(name: string): boolean {\n return name.trim().length >= MIN_NAME_LENGTH;\n}\n\n/** Whether a code matches the persona-code pattern. */\nexport function isValidPersonaCode(code: string): boolean {\n return PERSONA_CODE_PATTERN.test(code);\n}\n\n/**\n * A parsed persona block — name, code (possibly empty before resolution),\n * line number of the header (1-indexed), and whether the user explicitly\n * authored the code via `## Name (CODE)` syntax.\n */\nexport interface ParsedPersona {\n name: string;\n /** Empty string when no code was authored (will be filled by {@link resolvePersonaCodes}). */\n rawCode: string;\n /** True when the code came from `## Name (CODE)` syntax; false when absent in source. */\n explicit: boolean;\n /** 1-indexed line number of the `## ` header. */\n lineNumber: number;\n /** Whether a `**Role:**` line was found in the block body. */\n hasRole: boolean;\n}\n\n/** A resolved persona — code is always populated (derived if not explicit). */\nexport interface ResolvedPersona extends ParsedPersona {\n code: string;\n codeError?: 'non-canonical-derived-code' | 'collision-space-exhausted';\n}\n\n/** A validation error with a 1-indexed line reference into the source content. */\nexport interface PersonaValidationError {\n line: number;\n message: string;\n}\n\n/**\n * Extract name and (optional) code from a `## ...` header line.\n *\n * Parsed manually rather than with regex to avoid super-linear-backtracking\n * vulnerabilities flagged by `regexp/no-super-linear-backtracking`. The\n * `(CODE)` suffix is detected by checking for a trailing `)` and locating\n * its matching `(` via `lastIndexOf` — no quantifier overlap. Inline HTML\n * comments are stripped from the body before name/code extraction so a\n * trailing `<!-- ... -->` doesn't corrupt the parsed name.\n */\nfunction parseHeaderLine(line: string): { name: string; rawCode: string | undefined } | undefined {\n if (!line.startsWith('## ')) return undefined;\n const body = stripInlineComments(line.slice(3)).trimEnd();\n if (body.endsWith(')')) {\n const openParen = body.lastIndexOf('(');\n if (openParen !== -1) {\n const namePart = body.slice(0, openParen).trim();\n const codePart = body.slice(openParen + 1, -1).trim();\n return { name: namePart, rawCode: codePart };\n }\n }\n return { name: body.trim(), rawCode: undefined };\n}\n\n/**\n * Parse persona blocks from markdown content.\n *\n * A block starts at a level-2 header (`## ...`) and runs until the next\n * level-2 header or end of file. The header may include a parenthesized\n * code (`## Name (PO)`) or omit it (`## Name`). The body is scanned for\n * a `**Role:**` line; presence is recorded but the role text isn't\n * extracted here (validation only needs the existence check).\n *\n * Pure — no I/O.\n */\nexport function parsePersonas(content: string): ParsedPersona[] {\n const lines = content.split('\\n');\n const skip = computeSkipMask(lines);\n const personas: ParsedPersona[] = [];\n let current: ParsedPersona | undefined;\n\n for (const [index, line] of lines.entries()) {\n if (skip[index] === true) continue;\n const header = parseHeaderLine(line);\n if (header) {\n if (current) personas.push(current);\n current = {\n name: header.name,\n rawCode: header.rawCode ?? '',\n explicit: header.rawCode !== undefined,\n lineNumber: index + 1,\n hasRole: false,\n };\n continue;\n }\n if (current && line.startsWith('**Role:**')) {\n current.hasRole = true;\n }\n }\n\n if (current) personas.push(current);\n return personas;\n}\n\n/** Allocate a deterministic collision suffix without exceeding four characters. */\nfunction allocateDerivedCode(\n base: string,\n claimed: ReadonlySet<string>,\n): { code: string; exhausted: boolean } {\n if (!claimed.has(base)) return { code: base, exhausted: false };\n\n for (let suffix = 2; ; suffix += 1) {\n const suffixText = String(suffix);\n const prefixLength = MAX_DERIVED_CODE_LENGTH - suffixText.length;\n if (prefixLength < 1) return { code: base, exhausted: true };\n const candidate = `${base.slice(0, prefixLength)}${suffixText}`;\n if (!claimed.has(candidate)) return { code: candidate, exhausted: false };\n }\n}\n\n/**\n * Resolve auto-derived codes with collision avoidance.\n *\n * For each persona without an explicit code, derive one from the name.\n * If the derived code is already taken (by a user-authored explicit code\n * or a previously-resolved derivation in the same pass), append a numeric\n * suffix starting at 2 (`PO` → `PO2` → `PO3` → ...).\n *\n * Explicit codes are claimed up-front so derived codes always lose\n * collision disputes against user-authored ones.\n */\nexport function resolvePersonaCodes(parsed: readonly ParsedPersona[]): ResolvedPersona[] {\n const claimed = new Set<string>();\n for (const persona of parsed) {\n if (persona.explicit && persona.rawCode.length > 0) {\n claimed.add(persona.rawCode);\n }\n }\n\n const resolved: ResolvedPersona[] = [];\n for (const persona of parsed) {\n if (persona.explicit) {\n resolved.push({ ...persona, code: persona.rawCode });\n continue;\n }\n const base = derivePersonaCode(persona.name);\n const allocation = allocateDerivedCode(base, claimed);\n if (!allocation.exhausted) claimed.add(allocation.code);\n let codeError: ResolvedPersona['codeError'];\n if (allocation.exhausted) {\n codeError = 'collision-space-exhausted';\n } else if (!CANONICAL_PERSONA_CODE_PATTERN.test(allocation.code)) {\n codeError = 'non-canonical-derived-code';\n }\n resolved.push({ ...persona, code: allocation.code, codeError });\n }\n\n return resolved;\n}\n\n/**\n * Validate parsed personas. Returns a list of {@link PersonaValidationError}\n * with 1-indexed line numbers; empty list means the file is well-formed.\n *\n * Checks (each independent):\n * - Persona name is ≥ {@link MIN_NAME_LENGTH} characters\n * - Header has a name (not just `## (CODE)`)\n * - Block has a `**Role:**` line\n * - Persona names are unique within the file\n * - Resolved codes are unique within the file\n * - Resolved codes match {@link PERSONA_CODE_PATTERN}\n * (digit-first names like \"3 Amigos\" derive non-conformant codes and\n * surface here with the explicit-override prompt)\n */\nfunction validateNameAndRole(persona: ParsedPersona): PersonaValidationError[] {\n const errors: PersonaValidationError[] = [];\n if (persona.name.length === 0) {\n errors.push({ line: persona.lineNumber, message: 'missing persona name' });\n } else if (!isValidPersonaName(persona.name)) {\n errors.push({\n line: persona.lineNumber,\n message: 'persona name must be at least 2 characters',\n });\n }\n if (!persona.hasRole) {\n errors.push({ line: persona.lineNumber, message: 'missing Role line' });\n }\n return errors;\n}\n\n/** Produce pattern-violation errors for resolved personas. */\nfunction findPatternErrors(resolved: readonly ResolvedPersona[]): PersonaValidationError[] {\n const errors: PersonaValidationError[] = [];\n for (const persona of resolved) {\n if (persona.code.length === 0) continue;\n if (persona.codeError === 'non-canonical-derived-code') {\n errors.push({\n line: persona.lineNumber,\n message: `name produces non-canonical code \"${persona.code}\" — author an explicit 2–4 letter code via \\`## Name (CODE)\\``,\n });\n continue;\n }\n if (persona.codeError === 'collision-space-exhausted') {\n errors.push({\n line: persona.lineNumber,\n message: `canonical collision space exhausted for \"${persona.code}\" — author an explicit 2–4 letter code via \\`## Name (CODE)\\``,\n });\n continue;\n }\n if (isValidPersonaCode(persona.code)) continue;\n const message = persona.explicit\n ? `code \"${persona.code}\" violates pattern ${PERSONA_CODE_PATTERN.source}`\n : `name produces non-conformant code \"${persona.code}\" — author explicit code via \\`## Name (CODE)\\``;\n errors.push({ line: persona.lineNumber, message });\n }\n return errors;\n}\n\nexport function validatePersonas(parsed: readonly ParsedPersona[]): PersonaValidationError[] {\n const resolved = resolvePersonaCodes(parsed);\n return [\n ...parsed.flatMap(persona => validateNameAndRole(persona)),\n ...findDuplicates(\n groupByLine(parsed, persona => persona.name),\n 'persona name',\n ),\n ...findPatternErrors(resolved),\n ...findDuplicates(\n groupByLine(resolved, persona => persona.code),\n 'persona code',\n ),\n ];\n}\n\n/**\n * Result of resolving a persona reference against the file.\n *\n * Discriminated union — `match` is guaranteed when `status === 'valid'`;\n * `suggestion` is only meaningful (and only ever populated) when\n * `status === 'unknown'`. Callers can narrow without optional chaining\n * after checking `status`.\n */\nexport type PersonaReferenceResult =\n { status: 'valid'; match: ResolvedPersona } | { status: 'unknown'; suggestion?: string };\n\ninterface LegacyPersonaAlias {\n code: string;\n persona: ResolvedPersona;\n}\n\nfunction resolveLegacyPersonaAliases(personas: readonly ResolvedPersona[]): LegacyPersonaAlias[] {\n const claimed = new Set(\n personas.filter(persona => persona.codeError === undefined).map(persona => persona.code),\n );\n const aliases: LegacyPersonaAlias[] = [];\n\n for (const persona of personas) {\n if (persona.codeError !== undefined) continue;\n const base = deriveLegacyPersonaCode(persona.name);\n if (base === '' || base === persona.code) continue;\n\n let candidate = base;\n let suffix = 2;\n while (claimed.has(candidate)) {\n candidate = `${base}${suffix}`;\n suffix += 1;\n }\n if (!isValidPersonaCode(candidate)) continue;\n claimed.add(candidate);\n aliases.push({ code: candidate, persona });\n }\n\n return aliases;\n}\n\nfunction findCasingSuggestion(\n personas: readonly ResolvedPersona[],\n legacyAliases: readonly LegacyPersonaAlias[],\n input: string,\n): string | undefined {\n const lowered = input.toLowerCase();\n for (const persona of personas) {\n if (persona.codeError !== undefined) continue;\n if (persona.code.toLowerCase() === lowered) return persona.code;\n if (persona.name.toLowerCase() === lowered) return persona.name;\n }\n const legacyAlias = legacyAliases.find(alias => alias.code.toLowerCase() === lowered);\n if (legacyAlias) return legacyAlias.code;\n return undefined;\n}\n\n/**\n * Look up a persona reference against a parsed-and-resolved list.\n *\n * Strict on casing: `\"po\"` against existing `PO` returns\n * `{ status: 'unknown', suggestion: 'PO' }`. Lenient matching would\n * silently alias persona codes that legitimately differ by case\n * (`PO` vs `Po` vs `PO2`).\n *\n * Match priority: exact code → exact name → casing-mismatch suggestion.\n *\n * Pure — no I/O. Wrap with {@link validatePersonaReference} for the file-reading\n * path.\n */\nexport function lookupPersonaReference(\n personas: readonly ResolvedPersona[],\n input: string,\n): PersonaReferenceResult {\n if (input.length === 0) return { status: 'unknown' };\n\n for (const persona of personas) {\n if (persona.codeError !== undefined) continue;\n if (persona.code === input || persona.name === input) {\n return { status: 'valid', match: persona };\n }\n }\n\n const legacyAliases = resolveLegacyPersonaAliases(personas);\n const legacyMatch = legacyAliases.find(alias => alias.code === input);\n if (legacyMatch) return { status: 'valid', match: legacyMatch.persona };\n\n return { status: 'unknown', suggestion: findCasingSuggestion(personas, legacyAliases, input) };\n}\n\n/**\n * Resolve a persona reference against the on-disk personas file.\n *\n * Reads from `paths.personas` in `.safeword/config.json` when configured;\n * falls back to the namespace-root default otherwise. Degrades\n * gracefully on a missing or unreadable file — returns\n * `{ status: 'unknown' }` rather than throwing, regardless of whether the\n * resolved path is the default or a configured override. Strict\n * validation lives in `safeword doctor`; this lookup API is meant to be\n * cheap, consistent, and side-effect-free. Do NOT change the unknown\n * return to a throw for configured-but-missing — `safeword doctor` owns\n * the loud signal (ticket K7N2QM).\n */\nexport function validatePersonaReference(cwd: string, input: string): PersonaReferenceResult {\n let content: string;\n try {\n const filePath = resolveConfiguredPath(cwd, 'personas');\n content = readFileSync(filePath, 'utf8');\n } catch {\n return { status: 'unknown' };\n }\n const personas = resolvePersonaCodes(parsePersonas(content));\n return lookupPersonaReference(personas, input);\n}\n","const SYNCTICKETS_QUIET_COMMAND = '`safeword project sync-tickets --quiet`';\n\nexport function buildIndexConflictListMessage(paths: string[]): string {\n return (\n `Ticket index file(s) contained merge-conflict markers: ${paths.join(', ')}. ` +\n `Run ${SYNCTICKETS_QUIET_COMMAND} after resolving the merge conflict.`\n );\n}\n","import type { SafewordSchema } from '../schema.js';\nimport { codexFinalizationIsComplete } from './finalization.js';\n\nfunction withoutFinalizedCodexEntries<T>(\n values: Readonly<Record<string, T>>,\n retiredPaths: ReadonlySet<string>,\n): Record<string, T> {\n return Object.fromEntries(Object.entries(values).filter(([path]) => !retiredPaths.has(path)));\n}\n\n/** Keep explicit Codex finalization stable across later project reconciliation. */\nexport function schemaForCodexDelivery(cwd: string, schema: SafewordSchema): SafewordSchema {\n if (!codexFinalizationIsComplete(cwd)) return schema;\n const retiredPaths = new Set(schema.codexMigration.cleanupFiles);\n return {\n ...schema,\n deprecatedFiles: schema.deprecatedFiles.filter(path => !retiredPaths.has(path)),\n ownedFiles: withoutFinalizedCodexEntries(schema.ownedFiles, retiredPaths),\n managedFiles: withoutFinalizedCodexEntries(schema.managedFiles, retiredPaths),\n jsonMerges: withoutFinalizedCodexEntries(schema.jsonMerges, retiredPaths),\n textPatches: withoutFinalizedCodexEntries(schema.textPatches, retiredPaths),\n legacyTextPatches: withoutFinalizedCodexEntries(schema.legacyTextPatches, retiredPaths),\n contracts: withoutFinalizedCodexEntries(schema.contracts, retiredPaths),\n };\n}\n","import { schemaForClaudeDelivery } from '../claude-plugin/delivery-schema.js';\nimport { schemaForCodexDelivery } from '../codex-plugin/delivery-schema.js';\nimport { type SafewordSchema, schemaForProjectSurfaces } from '../schema.js';\n\nexport function projectLifecycleSchema(cwd: string, agents: readonly string[]): SafewordSchema {\n const deliverySchema = schemaForCodexDelivery(cwd, schemaForClaudeDelivery(cwd));\n return schemaForProjectSurfaces(deliverySchema, [\n 'core',\n ...(agents.includes('cursor') ? (['cursor'] as const) : []),\n ]);\n}\n","import { existsSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { type CliResult, createResult } from '../cli-protocol/result.js';\nimport { isCursorProjectPath, type SafewordSchema } from '../schema.js';\nimport { projectLifecycleSchema } from './schema.js';\n\n/**\n * Safeword-owned Cursor files declared by the schema. Cursor has no host\n * process to interrogate, so its project-local assets are the only surface\n * evidence available.\n */\nfunction cursorOwnedFiles(schema: SafewordSchema): readonly string[] {\n return [...Object.keys(schema.ownedFiles), ...Object.keys(schema.managedFiles)].filter(path =>\n isCursorProjectPath(path),\n );\n}\n\n/** True when this project already carries Safeword-owned Cursor assets. */\nexport function hasCursorProjectAssets(cwd: string, schema: SafewordSchema): boolean {\n return cursorOwnedFiles(schema).some(path => existsSync(nodePath.join(cwd, path)));\n}\n\n/**\n * Observe the Cursor surface from its own assets rather than mirroring the\n * project result, so a Cursor-only gap cannot be reported as healthy.\n */\nexport function observeCursorProject(cwd: string, schema: SafewordSchema): CliResult {\n const owned = cursorOwnedFiles(schema);\n const missing = owned.filter(path => !existsSync(nodePath.join(cwd, path)));\n if (missing.length === 0) {\n return createResult({\n state: 'healthy',\n data: {\n command: 'cursor status',\n coverage: 'project-owned Cursor assets',\n owned_files: owned.length,\n },\n });\n }\n return createResult({\n state: 'action_required',\n findings: [\n {\n code: 'CURSOR_ASSETS_MISSING',\n message: `${missing.length} Safeword-owned Cursor file(s) are missing; run \\`safeword install --agents=cursor\\`.`,\n severity: 'warning',\n },\n ],\n nextActions: [\n { command: 'safeword install --agents=cursor', mutates: true, requiresHuman: false },\n ],\n data: {\n command: 'cursor status',\n coverage: 'project-owned Cursor assets',\n owned_files: owned.length,\n missing_files: missing.length,\n },\n });\n}\n\n/**\n * Advise when a project carries Cursor assets that the current selection\n * excludes, so an upgrade cannot silently leave them at an older release.\n * Resolves the Cursor-inclusive schema itself — the selection's own schema has\n * every Cursor path filtered out and could never detect them.\n */\nexport function unselectedCursorFinding(\n cwd: string,\n agents: readonly string[],\n): CliResult['findings'] {\n if (agents.includes('cursor')) return [];\n if (!hasCursorProjectAssets(cwd, projectLifecycleSchema(cwd, ['cursor']))) return [];\n return [\n {\n code: 'CURSOR_NOT_SELECTED',\n message:\n 'This project has Safeword-owned Cursor assets that the current selection excludes; include `--agents=cursor` to keep them current.',\n severity: 'info',\n },\n ];\n}\n"],"mappings":"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","names":["readdirSync","nodePath","readFileSync","header","nodePath","readdirSync","nodePath","nodePath"]}
@@ -103,10 +103,10 @@ function scanTreeForFile(directory, filename, depth, maxDepth) {
103
103
  }
104
104
  return void 0;
105
105
  }
106
- function findFileMatchingInTree(cwd, predicate, maxDepth = 10) {
107
- return scanTreeForMatch(cwd, predicate, 0, maxDepth);
106
+ function findFileMatchingInTree(cwd, predicate, maxDepth = 10, excludedDirectories = /* @__PURE__ */ new Set()) {
107
+ return scanTreeForMatch(cwd, predicate, 0, maxDepth, excludedDirectories);
108
108
  }
109
- function scanTreeForMatch(directory, predicate, depth, maxDepth) {
109
+ function scanTreeForMatch(directory, predicate, depth, maxDepth, excludedDirectories) {
110
110
  if (depth > maxDepth) return void 0;
111
111
  let entries;
112
112
  try {
@@ -119,12 +119,9 @@ function scanTreeForMatch(directory, predicate, depth, maxDepth) {
119
119
  }
120
120
  for (const entry of entries) {
121
121
  if (!isScannableSubdirectory(entry)) continue;
122
- const result = scanTreeForMatch(
123
- nodePath.join(directory, entry.name),
124
- predicate,
125
- depth + 1,
126
- maxDepth
127
- );
122
+ const child = nodePath.join(directory, entry.name);
123
+ if (excludedDirectories.has(child)) continue;
124
+ const result = scanTreeForMatch(child, predicate, depth + 1, maxDepth, excludedDirectories);
128
125
  if (result !== void 0) return result;
129
126
  }
130
127
  return void 0;
@@ -239,4 +236,4 @@ export {
239
236
  readJson,
240
237
  writeJson
241
238
  };
242
- //# sourceMappingURL=chunk-S2T356DA.js.map
239
+ //# sourceMappingURL=chunk-HF3DXZEA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/fs.ts"],"sourcesContent":["/**\n * File system utilities for CLI operations\n */\n\nimport {\n chmodSync,\n type Dirent,\n existsSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n realpathSync,\n rmdirSync,\n rmSync,\n statSync,\n writeFileSync,\n} from 'node:fs';\nimport nodePath from 'node:path';\n\n// Get the directory of this module (for locating templates)\nconst __dirname = import.meta.dirname;\n\n/**\n * Get path to bundled templates directory.\n * Works in both development (src/) and production (dist/) contexts.\n *\n * Note: We check for SAFEWORD.md to distinguish from src/templates/ which\n * contains TypeScript source files (config.ts, content.ts).\n *\n * Path resolution (bundled with tsup):\n * - From dist/chunk-*.js: __dirname = packages/cli/dist/ → ../templates\n */\nexport function getTemplatesDirectory(): string {\n const knownTemplateFile = 'SAFEWORD.md';\n\n // Try different relative paths - the bundled code ends up in dist/ directly (flat)\n // while source is in src/utils/\n const candidates = [\n nodePath.join(__dirname, '..', 'templates'), // From dist/ (flat bundled)\n nodePath.join(__dirname, '..', '..', 'templates'), // From src/utils/ or dist/utils/\n nodePath.join(__dirname, 'templates'), // Direct sibling (unlikely but safe)\n ];\n\n for (const candidate of candidates) {\n if (existsSync(nodePath.join(candidate, knownTemplateFile))) {\n return candidate;\n }\n }\n\n throw new Error('Templates directory not found');\n}\n\n/**\n * Check if a path exists\n * @param path\n */\n/** True when `path` exists and is a directory (a file is not). */\nexport function isDirectory(path: string): boolean {\n try {\n return statSync(path).isDirectory();\n } catch {\n return false;\n }\n}\n\n/** Resolve an existing directory to a normalized canonical path. */\nexport function canonicalDirectory(path: unknown): string | undefined {\n if (typeof path !== 'string' || path.trim() === '') return undefined;\n try {\n if (!statSync(path).isDirectory()) return undefined;\n return nodePath.normalize(realpathSync(path));\n } catch {\n return undefined;\n }\n}\n\nexport function exists(path: string): boolean {\n return existsSync(path);\n}\n\n/**\n * Directories to exclude when scanning subdirectories for language manifests.\n * These contain vendored/generated files that would cause false positives.\n */\nconst SUBDIRECTORY_EXCLUDE = new Set([\n 'node_modules',\n '.git',\n '.safeword',\n 'vendor',\n // Go toolchains ignore testdata/ entirely; repos keep fixture go.mod files there\n // (with real module directives) that must not surface as discovered packages.\n 'testdata',\n 'dist',\n 'build',\n 'target',\n 'coverage',\n 'dbt_packages',\n 'out',\n '.next',\n '.nuxt',\n '.output',\n '__pycache__',\n '.venv',\n 'venv',\n]);\n\n/**\n * Check if a file exists anywhere in the project tree.\n * Recursively scans subdirectories, skipping excluded directories.\n *\n * @param cwd - Project root directory\n * @param filename - File to search for (e.g., 'pyproject.toml')\n * @returns true if found anywhere in the project tree\n */\nexport function existsInTree(cwd: string, filename: string): boolean {\n return findInTree(cwd, filename) !== undefined;\n}\n\n/**\n * Find a file anywhere in the project tree.\n * Recursively scans subdirectories, skipping excluded directories.\n * Root is checked first; deeper matches use depth-first traversal.\n *\n * @param cwd - Project root directory\n * @param filename - File to search for (e.g., 'pyproject.toml')\n * @param maxDepth - Maximum directory depth to scan (default: 10)\n * @returns Directory path where file was found, or undefined\n */\nexport function findInTree(cwd: string, filename: string, maxDepth = 10): string | undefined {\n // Check root first\n if (existsSync(nodePath.join(cwd, filename))) {\n return cwd;\n }\n\n return scanTreeForFile(cwd, filename, 1, maxDepth);\n}\n\n/** Whether a dirent is a subdirectory safe to recurse into (not hidden, not excluded). */\nfunction isScannableSubdirectory(entry: Dirent): boolean {\n return (\n entry.isDirectory() && !entry.name.startsWith('.') && !SUBDIRECTORY_EXCLUDE.has(entry.name)\n );\n}\n\n/** Return scannable subdirectory paths (excludes hidden dirs and SUBDIRECTORY_EXCLUDE). */\nfunction getScannableSubdirectories(directory: string): string[] {\n try {\n return readdirSync(directory, { withFileTypes: true })\n .filter(entry => isScannableSubdirectory(entry))\n .map(entry => nodePath.join(directory, entry.name));\n } catch {\n return [];\n }\n}\n\nfunction scanTreeForFile(\n directory: string,\n filename: string,\n depth: number,\n maxDepth: number,\n): string | undefined {\n if (depth > maxDepth) return undefined;\n\n const subdirectories = getScannableSubdirectories(directory);\n\n // Check all children at this level first\n for (const subdirectory of subdirectories) {\n if (existsSync(nodePath.join(subdirectory, filename))) {\n return subdirectory;\n }\n }\n\n // Then recurse deeper\n for (const subdirectory of subdirectories) {\n const result = scanTreeForFile(subdirectory, filename, depth + 1, maxDepth);\n if (result !== undefined) return result;\n }\n\n return undefined;\n}\n\n/**\n * Find the first directory (root first, then descending) that contains a file\n * whose name matches `predicate`. Skips hidden dirs + SUBDIRECTORY_EXCLUDE. The\n * predicate analogue of {@link findInTree} — share this instead of re-walking\n * the tree (and re-declaring the exclude set) in callers.\n * @param cwd - Project root directory\n * @param predicate - Matches a filename (e.g. a Python test-file name)\n * @param maxDepth - Maximum directory depth to scan (default: 10)\n * @returns Directory containing a match, or undefined\n */\nexport function findFileMatchingInTree(\n cwd: string,\n predicate: (filename: string) => boolean,\n maxDepth = 10,\n excludedDirectories: ReadonlySet<string> = new Set(),\n): string | undefined {\n return scanTreeForMatch(cwd, predicate, 0, maxDepth, excludedDirectories);\n}\n\nfunction scanTreeForMatch(\n directory: string,\n predicate: (filename: string) => boolean,\n depth: number,\n maxDepth: number,\n excludedDirectories: ReadonlySet<string>,\n): string | undefined {\n if (depth > maxDepth) return undefined;\n\n let entries;\n try {\n entries = readdirSync(directory, { withFileTypes: true });\n } catch {\n return undefined;\n }\n\n if (entries.some(entry => entry.isFile() && predicate(entry.name))) {\n return directory;\n }\n\n for (const entry of entries) {\n if (!isScannableSubdirectory(entry)) continue;\n const child = nodePath.join(directory, entry.name);\n if (excludedDirectories.has(child)) continue;\n const result = scanTreeForMatch(child, predicate, depth + 1, maxDepth, excludedDirectories);\n if (result !== undefined) return result;\n }\n\n return undefined;\n}\n\n/**\n * Index multiple files in a single tree walk — the multi-file analogue of\n * `findInTree`. Returns a Map from each requested filename to the directory of\n * its **shallowest** occurrence (root-first, level-order — same ordering as\n * `findInTree`), omitting names not found. One traversal regardless of how many\n * filenames are requested, so callers that probe many manifests stay O(one walk)\n * even on large/deep monorepos. Stops early once every name is located.\n */\nexport function indexFilesInTree(\n cwd: string,\n filenames: Iterable<string>,\n maxDepth = 10,\n): Map<string, string> {\n const wanted = [...filenames];\n const found = new Map<string, string>();\n // Cursor instead of Array.shift() so the BFS stays O(dirs), not O(dirs²) on\n // wide/deep monorepos — the very repos this single-walk index exists to serve.\n const queue: { directory: string; depth: number }[] = [{ directory: cwd, depth: 0 }];\n\n let head = 0;\n while (head < queue.length) {\n const item = queue[head];\n head += 1;\n if (item === undefined) break;\n for (const name of wanted) {\n if (!found.has(name) && existsSync(nodePath.join(item.directory, name))) {\n found.set(name, item.directory);\n }\n }\n if (found.size === wanted.length) break;\n if (item.depth >= maxDepth) continue;\n for (const subdirectory of getScannableSubdirectories(item.directory)) {\n queue.push({ directory: subdirectory, depth: item.depth + 1 });\n }\n }\n return found;\n}\n\n/**\n * Every directory (root included) that DIRECTLY contains `filename`, in root-first\n * BFS order, skipping hidden dirs and SUBDIRECTORY_EXCLUDE. The collect-all analogue\n * of {@link findInTree} (which stops at the first match) — used to enumerate orphan\n * language manifests (a `go.mod` / `pyproject.toml` / `Cargo.toml` that is not enrolled\n * in any declared workspace) across a polyglot monorepo in one bounded walk.\n */\nexport function findAllInTree(cwd: string, filename: string, maxDepth = 10): string[] {\n const found: string[] = [];\n // Cursor-based BFS (not Array.shift()) so the walk stays O(dirs) on wide/deep trees,\n // mirroring indexFilesInTree.\n const queue: { directory: string; depth: number }[] = [{ directory: cwd, depth: 0 }];\n let head = 0;\n while (head < queue.length) {\n const item = queue[head];\n head += 1;\n if (item === undefined) break;\n if (existsSync(nodePath.join(item.directory, filename))) found.push(item.directory);\n if (item.depth >= maxDepth) continue;\n for (const subdirectory of getScannableSubdirectories(item.directory)) {\n queue.push({ directory: subdirectory, depth: item.depth + 1 });\n }\n }\n return found;\n}\n\n/**\n * Create directory recursively\n * @param path\n */\nexport function ensureDirectory(path: string): void {\n if (!existsSync(path)) {\n mkdirSync(path, { recursive: true });\n }\n}\n\n/**\n * Read file as string\n * @param path\n */\nexport function readFile(path: string): string {\n return readFileSync(path, 'utf8');\n}\n\n/**\n * Read file as string, return null if not exists\n * @param path\n */\nexport function readFileSafe(path: string): string | undefined {\n if (!existsSync(path)) return undefined;\n return readFileSync(path, 'utf8');\n}\n\n/**\n * Write file, creating parent directories if needed\n * @param path\n * @param content\n */\nexport function writeFile(path: string, content: string): void {\n ensureDirectory(nodePath.dirname(path));\n writeFileSync(path, content);\n}\n\n/**\n * Remove file or directory recursively\n * @param path\n */\nexport function remove(path: string): void {\n if (existsSync(path)) {\n rmSync(path, { recursive: true, force: true });\n }\n}\n\n/**\n * Remove directory only if empty, returns true if removed\n * @param path\n */\nexport function removeIfEmpty(path: string): boolean {\n if (!existsSync(path)) return false;\n try {\n rmdirSync(path); // Non-recursive, throws if not empty\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Make all shell scripts in a directory executable\n * @param dirPath\n */\nexport function makeScriptsExecutable(dirPath: string): void {\n if (!existsSync(dirPath)) return;\n for (const file of readdirSync(dirPath)) {\n if (file.endsWith('.sh')) {\n chmodSync(nodePath.join(dirPath, file), 0o755);\n }\n }\n}\n\n/**\n * Read and parse a JSON file, best-effort: returns `undefined` when no JSON\n * value can be obtained from the path — whether it is absent, empty, unparseable\n * (e.g. JSONC comments or malformed), or unreadable as a file at all. The read is\n * inside the `try` so a directory at the path (`EISDIR`) or a permission error\n * (`EACCES`) degrades to `undefined` like a parse failure, instead of throwing\n * uncaught and crashing callers (e.g. reconcile's jsonMerge). All callers already\n * treat the result as `T | undefined`.\n * @param path\n */\nexport function readJson(path: string): unknown {\n try {\n const content = readFileSafe(path);\n if (!content) return undefined;\n return JSON.parse(content) as unknown;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Write JSON file with formatting\n * @param path\n * @param data\n */\nexport function writeJson(path: string, data: unknown): void {\n writeFile(path, `${JSON.stringify(data, undefined, 2)}\\n`);\n}\n"],"mappings":"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","names":[]}
@@ -75,31 +75,31 @@ var CLAUDE_HISTORICAL_CATALOGUE = {
75
75
  ".claude/skills/bdd/DISCOVERY.md": "c229895c53030b8f44ff563dd3728d8f4a4e4e593d8c29ae9349283ea25b5d91",
76
76
  ".claude/skills/bdd/DONE.md": "e9f22430341cf225eaf58ef6335720c5033cb8f6779425d5740adc0ff80a5f60",
77
77
  ".claude/skills/bdd/PLAN_IMPLEMENTATION.md": "8ed89fca82f6e71b77351674f12d5d7c1380574e326e05ca1912caf84ff4ff9b",
78
- ".claude/skills/bdd/SCENARIOS.md": "a2cf8937e4c555c6e42f3f33a45cd5355dc61337a75ea220561bc0b6d9be3ad8",
78
+ ".claude/skills/bdd/SCENARIOS.md": "c808d4c8455f8f6248f85e2d2403e881bdab83e0ac19e8c3dd9c28fb9c2ee8f4",
79
79
  ".claude/skills/bdd/SKILL.md": "df9f7927be3289f3ffb0444d49ae25fe601ce33b9661f7a809f74dff8da40ff0",
80
80
  ".claude/skills/bdd/SPLITTING.md": "e232a37a4d76f0dfc51e65965c1e1b7f1572e0dedce0fb8c031e75bd6544a708",
81
- ".claude/skills/bdd/TDD.md": "fc596bf79b97eae1545a62b799b5ea29543c33eae65e722155125875421efa91",
81
+ ".claude/skills/bdd/TDD.md": "2865c9efa57682eb211cc101f9db15f529bd605c3a2bebb57131e2a6d3aec79d",
82
82
  ".claude/skills/bdd/VERIFY.md": "85abadfe756a3f391779fe500cd5c66597a33e0cab7fcef55f6b633b30818f31",
83
83
  ".claude/skills/brainstorm/SKILL.md": "fe99638bd1621cbd5fe3780a8d39023d4b175e3be2aef2e60d0ebe7558848f2e",
84
84
  ".claude/skills/cleanup-zombies/SKILL.md": "e0af9635774767cf36eb69726e11c642ec1dad42839c11407ea8ef60f89fc289",
85
- ".claude/skills/closeout/SKILL.md": "15beb617c742d2d3f7f330f1e7ccb0545e00df1c3b6e3b0c3cedae4535651a2d",
85
+ ".claude/skills/closeout/SKILL.md": "3eda7ae5172eb795da38c31a6e96d07a40154fd7a3ae4d9824c9c044b286f571",
86
86
  ".claude/skills/debug/SKILL.md": "ae56c4c9287f76a2250d13fa9908f5726ed4edbe4080ece10d1559507e242bd0",
87
87
  ".claude/skills/elicit/SKILL.md": "2638c773ce241a886563d1db8abbee70d72edefa780f762c0ed095df0f65cee5",
88
88
  ".claude/skills/explain/SKILL.md": "6673eccef3a9e68659c4e4b81b1e63bf9da03b1ae802dc7d22f419cb7c65472d",
89
89
  ".claude/skills/figure-it-out/SKILL.md": "18e2b44e9a91562079b3e1f52fcd9f952b5f57a0f0e7647b0273809848a75c0d",
90
90
  ".claude/skills/finish-review/REVIEWER.md": "0ecee21a63f91e09d3136f62cf8f7590ba9a640b85cad7b7b35c1ae334ff43c2",
91
- ".claude/skills/finish-review/SKILL.md": "8ff3fbeb2ba9eb6f9278bea718aacd747682dae4245bdb87356988c53611a69b",
92
- ".claude/skills/lint/SKILL.md": "208ec54032cabdcb532d1070e5ef5f1fcd6f0f0bfe8daf08e4ecf007aa285f66",
91
+ ".claude/skills/finish-review/SKILL.md": "e9ed5d198994b6cca12c62b1a4c13a1db2d82d65fc8a9173a41c5b5cf312cd52",
92
+ ".claude/skills/lint/SKILL.md": "fdde749fe9ce764f6f9325f963c092d457960494fe78dd04d65d53a50e7cfd19",
93
93
  ".claude/skills/quality-review/SKILL.md": "b940905d31cd4931665e023a65824a923a2bd5d7f590358355576cff8f3bc42a",
94
94
  ".claude/skills/refactor/SKILL.md": "a51a858fb13b50cbc86789edbde8a39e364b5cdd7d5d3b025d555d90b221760e",
95
95
  ".claude/skills/retro-filer/SKILL.md": "ea126f3805a2befefb4db2011439f075ebfd6eca31b78bd5f284ac11d667b4f0",
96
96
  ".claude/skills/retro/SKILL.md": "166e5109193bad4c26e060f6841d71c03f9155c7e74e1853c43b99b01c25d379",
97
- ".claude/skills/review-spec/SKILL.md": "5c707432234f2d6893b6b3ebd340820990040d81006e1550b419f032ce8fda08",
98
- ".claude/skills/self-review/SKILL.md": "dcc667823790f18e1fa8cf35aaf10c40464664929c7bbb093531fd684f673e15",
97
+ ".claude/skills/review-spec/SKILL.md": "ee3d3cf1c548e67dfcfb58f55c2d667fee7466b1f0ab6cad050c67c1b940fe33",
98
+ ".claude/skills/self-review/SKILL.md": "e5ff994ec84573e6f129127bad89617f0a67b67c5cf792cedac558b6e419ac3b",
99
99
  ".claude/skills/spike/SKILL.md": "905aab56037ad5a258bafa91cb2ebf05cff1acffbc9e1fd6f7a1f27230672f37",
100
100
  ".claude/skills/tdd-review/SKILL.md": "4b945f122a90d23462845d7bdbbd0b736aa69d423a2d7e99ebf646bf118faa4f",
101
101
  ".claude/skills/testing/SKILL.md": "697a4b090935989e0c8a53462d2b44087afafa50adc69e9a98da14bed23dbde9",
102
- ".claude/skills/ticket-system/SKILL.md": "1edd1d2c546dae594124c8846c43f78f81d4579370589eaf1476b894c0a9750e",
102
+ ".claude/skills/ticket-system/SKILL.md": "97595a9875cdca30ea26c809a26e5be7df338a42034d6122b559e70275f2477e",
103
103
  ".claude/skills/verify/SKILL.md": "26763f7fbc900b6994938db93e3b1e1e601ccded59d74c45f528a73fe71ec92f"
104
104
  },
105
105
  hook_files: {
@@ -1571,4 +1571,4 @@ export {
1571
1571
  acceptedHistoricalHookEntries,
1572
1572
  cataloguedClaudeLegacyPaths
1573
1573
  };
1574
- //# sourceMappingURL=chunk-NSZGI7HK.js.map
1574
+ //# sourceMappingURL=chunk-HNCUIXDY.js.map