@opengsd/gsd-core 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-debug-session-manager.md +11 -0
  4. package/agents/gsd-doc-synthesizer.md +2 -4
  5. package/agents/gsd-executor.md +5 -5
  6. package/agents/gsd-mempalace-curator.md +5 -2
  7. package/agents/gsd-phase-researcher.md +20 -1
  8. package/agents/gsd-plan-checker.md +37 -0
  9. package/agents/gsd-planner.md +44 -46
  10. package/agents/gsd-user-profiler.md +3 -0
  11. package/agents/gsd-verifier.md +12 -3
  12. package/bin/install.js +841 -971
  13. package/bin/lib/ui-safety-gate.cjs +2 -0
  14. package/commands/gsd/code-review.md +1 -1
  15. package/commands/gsd/execute-phase.md +1 -1
  16. package/commands/gsd/map-codebase.md +1 -1
  17. package/commands/gsd/mempalace-capture.md +1 -1
  18. package/commands/gsd/mempalace-recall.md +1 -1
  19. package/commands/gsd/new-milestone.md +1 -1
  20. package/commands/gsd/quick.md +1 -1
  21. package/commands/gsd/review-backlog.md +2 -1
  22. package/commands/gsd/verify-work.md +1 -1
  23. package/gsd-core/bin/gsd-tools.cjs +469 -88
  24. package/gsd-core/bin/lib/active-workstream-store.cjs +138 -22
  25. package/gsd-core/bin/lib/agent-install-check.cjs +230 -32
  26. package/gsd-core/bin/lib/api-coverage.cjs +3 -5
  27. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  28. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  29. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  30. package/gsd-core/bin/lib/audit.cjs +876 -240
  31. package/gsd-core/bin/lib/broken-windows.cjs +1 -1
  32. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  33. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  34. package/gsd-core/bin/lib/capability-registry.cjs +575 -101
  35. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  36. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  37. package/gsd-core/bin/lib/capability-validator.cjs +495 -22
  38. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  39. package/gsd-core/bin/lib/check-command-router.cjs +71 -37
  40. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  41. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  42. package/gsd-core/bin/lib/command-aliases.cjs +22 -0
  43. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  44. package/gsd-core/bin/lib/commands.cjs +651 -86
  45. package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
  46. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  47. package/gsd-core/bin/lib/config-loader.cjs +75 -0
  48. package/gsd-core/bin/lib/config.cjs +10 -1
  49. package/gsd-core/bin/lib/core-utils.cjs +127 -29
  50. package/gsd-core/bin/lib/decisions.cjs +23 -0
  51. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  52. package/gsd-core/bin/lib/frontmatter.cjs +155 -20
  53. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  54. package/gsd-core/bin/lib/git-base-branch.cjs +102 -0
  55. package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
  56. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  57. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  58. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  59. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  60. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  61. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  62. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  63. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  64. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  65. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  66. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  67. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  68. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  69. package/gsd-core/bin/lib/init.cjs +321 -129
  70. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  71. package/gsd-core/bin/lib/install-engine.cjs +745 -258
  72. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  73. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  74. package/gsd-core/bin/lib/install-profiles.cjs +134 -57
  75. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  76. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  77. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  78. package/gsd-core/bin/lib/installer-migrations.cjs +138 -31
  79. package/gsd-core/bin/lib/io.cjs +10 -0
  80. package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
  81. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  82. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  83. package/gsd-core/bin/lib/milestone.cjs +754 -70
  84. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  85. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  86. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  87. package/gsd-core/bin/lib/pattern.cjs +122 -0
  88. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  89. package/gsd-core/bin/lib/phase-id.cjs +444 -36
  90. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  91. package/gsd-core/bin/lib/phase-locator.cjs +125 -18
  92. package/gsd-core/bin/lib/phase.cjs +646 -143
  93. package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
  94. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  95. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  96. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  97. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  98. package/gsd-core/bin/lib/planning-workspace.cjs +56 -6
  99. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  100. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  101. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  102. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
  103. package/gsd-core/bin/lib/review-lane-descriptor.cjs +13 -4
  104. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  105. package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
  106. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  107. package/gsd-core/bin/lib/roadmap-command-router.cjs +34 -0
  108. package/gsd-core/bin/lib/roadmap-parser.cjs +943 -184
  109. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  110. package/gsd-core/bin/lib/roadmap.cjs +385 -94
  111. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +608 -46
  112. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  113. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +426 -55
  114. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  115. package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
  116. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +115 -3
  117. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  118. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  119. package/gsd-core/bin/lib/security.cjs +104 -5
  120. package/gsd-core/bin/lib/shell-command-projection.cjs +275 -3
  121. package/gsd-core/bin/lib/smart-entry.cjs +142 -22
  122. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  123. package/gsd-core/bin/lib/state-document.cjs +152 -8
  124. package/gsd-core/bin/lib/state-transition.cjs +371 -117
  125. package/gsd-core/bin/lib/state.cjs +1794 -357
  126. package/gsd-core/bin/lib/surface.cjs +23 -9
  127. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  128. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  129. package/gsd-core/bin/lib/uat-predicate.cjs +9 -3
  130. package/gsd-core/bin/lib/uat.cjs +399 -56
  131. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  132. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  133. package/gsd-core/bin/lib/unusable-input.cjs +24 -0
  134. package/gsd-core/bin/lib/update-context.cjs +8 -2
  135. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  136. package/gsd-core/bin/lib/validate.cjs +20 -6
  137. package/gsd-core/bin/lib/vendor/README.md +37 -0
  138. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  139. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  140. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  141. package/gsd-core/bin/lib/verification.cjs +258 -8
  142. package/gsd-core/bin/lib/verify.cjs +368 -888
  143. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
  144. package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
  145. package/gsd-core/bin/lib/workstream.cjs +2 -2
  146. package/gsd-core/bin/lib/worktree-safety.cjs +176 -9
  147. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  148. package/gsd-core/bin/shared/config-schema.manifest.json +7 -1
  149. package/gsd-core/references/agent-contracts.md +43 -26
  150. package/gsd-core/references/checkpoints.md +2 -2
  151. package/gsd-core/references/context-budget.md +1 -1
  152. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  153. package/gsd-core/references/doc-conflict-engine.md +1 -1
  154. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  155. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  156. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  157. package/gsd-core/references/execute-phase-response-language.md +1 -1
  158. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  159. package/gsd-core/references/gate-prompts.md +1 -1
  160. package/gsd-core/references/git-planning-commit.md +2 -1
  161. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  162. package/gsd-core/references/model-profiles.md +12 -4
  163. package/gsd-core/references/mvp-concepts.md +9 -9
  164. package/gsd-core/references/planner-guidance.md +3 -9
  165. package/gsd-core/references/planner-preconditions.md +1 -1
  166. package/gsd-core/references/planner-reviews.md +1 -1
  167. package/gsd-core/references/planning-config.md +8 -6
  168. package/gsd-core/references/revision-loop.md +1 -1
  169. package/gsd-core/references/specless-probe-fallback.md +1 -1
  170. package/gsd-core/references/universal-anti-patterns.md +3 -3
  171. package/gsd-core/references/verifier-phase-gates.md +192 -0
  172. package/gsd-core/references/verify-mvp-mode.md +1 -1
  173. package/gsd-core/references/workstream-flag.md +22 -6
  174. package/gsd-core/templates/discussion-log.md +1 -1
  175. package/gsd-core/templates/phase-prompt.md +2 -4
  176. package/gsd-core/templates/state.md +4 -4
  177. package/gsd-core/templates/verification-report.md +9 -1
  178. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  179. package/gsd-core/workflows/autonomous.md +1 -1
  180. package/gsd-core/workflows/cleanup.md +62 -3
  181. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +13 -3
  182. package/gsd-core/workflows/code-review-fix.md +37 -10
  183. package/gsd-core/workflows/code-review.md +38 -12
  184. package/gsd-core/workflows/complete-milestone.md +141 -18
  185. package/gsd-core/workflows/debug.md +7 -5
  186. package/gsd-core/workflows/diagnose-issues.md +35 -9
  187. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  188. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  189. package/gsd-core/workflows/discuss-phase-assumptions.md +2 -1
  190. package/gsd-core/workflows/edit-phase.md +26 -1
  191. package/gsd-core/workflows/eval-review.md +3 -5
  192. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +31 -6
  193. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  194. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +2 -0
  195. package/gsd-core/workflows/execute-phase.md +38 -50
  196. package/gsd-core/workflows/execute-plan.md +36 -4
  197. package/gsd-core/workflows/explore.md +131 -4
  198. package/gsd-core/workflows/fast.md +10 -2
  199. package/gsd-core/workflows/health.md +73 -4
  200. package/gsd-core/workflows/import.md +4 -4
  201. package/gsd-core/workflows/ingest-docs.md +5 -5
  202. package/gsd-core/workflows/mvp-phase.md +6 -3
  203. package/gsd-core/workflows/new-milestone.md +14 -9
  204. package/gsd-core/workflows/new-project.md +14 -14
  205. package/gsd-core/workflows/next.md +12 -0
  206. package/gsd-core/workflows/plan-phase.md +41 -17
  207. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  208. package/gsd-core/workflows/progress.md +34 -6
  209. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -4
  210. package/gsd-core/workflows/quick/steps/quick-verification.md +27 -6
  211. package/gsd-core/workflows/quick/steps/research-phase.md +2 -2
  212. package/gsd-core/workflows/quick.md +35 -15
  213. package/gsd-core/workflows/review.md +26 -5
  214. package/gsd-core/workflows/secure-phase.md +1 -1
  215. package/gsd-core/workflows/session-report.md +2 -1
  216. package/gsd-core/workflows/settings.md +66 -2
  217. package/gsd-core/workflows/ship.md +104 -44
  218. package/gsd-core/workflows/spec-phase.md +30 -12
  219. package/gsd-core/workflows/sync-skills.md +63 -8
  220. package/gsd-core/workflows/transition.md +46 -11
  221. package/gsd-core/workflows/ui-phase.md +5 -5
  222. package/gsd-core/workflows/ui-review.md +2 -2
  223. package/gsd-core/workflows/update.md +1 -1
  224. package/gsd-core/workflows/validate-phase.md +1 -1
  225. package/gsd-core/workflows/verify-work.md +9 -7
  226. package/hooks/dist/gsd-agent-isolation-guard.js +103 -14
  227. package/hooks/dist/gsd-check-update-worker.js +56 -13
  228. package/hooks/dist/gsd-check-update.js +19 -1
  229. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  230. package/hooks/dist/gsd-cursor-subagent-start.js +77 -2
  231. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  232. package/hooks/dist/gsd-prompt-guard.js +21 -20
  233. package/hooks/dist/gsd-read-injection-scanner.js +38 -24
  234. package/hooks/dist/gsd-statusline.js +18 -0
  235. package/hooks/dist/gsd-update-banner.js +22 -1
  236. package/hooks/dist/gsd-workflow-guard.js +134 -36
  237. package/hooks/dist/lib/git-cmd.js +92 -59
  238. package/hooks/dist/lib/injection-patterns.js +45 -0
  239. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  240. package/hooks/dist/lib/isolation-sentinel.js +9 -0
  241. package/hooks/gsd-agent-isolation-guard.js +103 -14
  242. package/hooks/gsd-check-update-worker.js +56 -13
  243. package/hooks/gsd-check-update.js +19 -1
  244. package/hooks/gsd-cursor-pre-tool.js +0 -3
  245. package/hooks/gsd-cursor-subagent-start.js +77 -2
  246. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  247. package/hooks/gsd-prompt-guard.js +21 -20
  248. package/hooks/gsd-read-injection-scanner.js +38 -24
  249. package/hooks/gsd-statusline.js +18 -0
  250. package/hooks/gsd-update-banner.js +22 -1
  251. package/hooks/gsd-workflow-guard.js +134 -36
  252. package/hooks/lib/git-cmd.js +92 -59
  253. package/hooks/lib/injection-patterns.js +45 -0
  254. package/hooks/lib/isolation-deny-reason.js +39 -0
  255. package/hooks/lib/isolation-sentinel.js +9 -0
  256. package/package.json +21 -9
  257. package/pi/gsd.cjs +19 -5
  258. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  259. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  260. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  261. package/scripts/changeset/lint.cjs +60 -5
  262. package/scripts/check-alias-drift.cjs +7 -43
  263. package/scripts/check-contract-drift.cjs +297 -0
  264. package/scripts/ci-test-scope.cjs +19 -2
  265. package/scripts/command-contract-helpers.cjs +903 -1
  266. package/scripts/gen-adr-index.cjs +728 -38
  267. package/scripts/gen-capability-registry.cjs +3 -15
  268. package/scripts/gen-context-index.cjs +2 -11
  269. package/scripts/gen-health-docs.cjs +390 -0
  270. package/scripts/gen-inventory-manifest.cjs +50 -4
  271. package/scripts/gen-loop-host-contract.cjs +4 -24
  272. package/scripts/gen-registry.cjs +3 -14
  273. package/scripts/lib/alias-drift-families.cjs +46 -0
  274. package/scripts/lib/drift-scan.cjs +278 -0
  275. package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
  276. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  277. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  278. package/scripts/lint-canary-version-leak.cjs +73 -0
  279. package/scripts/lint-command-contract.cjs +96 -13
  280. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  281. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  282. package/scripts/lint-default-flip-documentation.cjs +193 -0
  283. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  284. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  285. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  286. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  287. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  288. package/scripts/lint-milestone-window-drift.cjs +468 -0
  289. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  290. package/scripts/lint-plan-count-drift.cjs +318 -0
  291. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  292. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  293. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  294. package/scripts/lint-regression-test-names.cjs +15 -13
  295. package/scripts/lint-removed-but-needed.cjs +320 -0
  296. package/scripts/lint-state-field-drift.cjs +805 -0
  297. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  298. package/scripts/lint-test-file-count.allowlist.json +21 -10
  299. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  300. package/scripts/lint-vendored-deps.cjs +124 -0
  301. package/scripts/pr-changed-files.cjs +63 -0
  302. package/scripts/pr-template-policy.cjs +14 -4
  303. package/scripts/prompt-injection-scan.sh +25 -0
  304. package/scripts/require-issue-link-policy.cjs +192 -0
  305. package/scripts/state-write-path-drift-baseline.json +19 -0
  306. package/scripts/sync-runtime-launcher.cjs +2 -4
  307. package/skills/gsd-autonomous/SKILL.md +0 -1
  308. package/skills/gsd-code-review/SKILL.md +1 -1
  309. package/skills/gsd-execute-phase/SKILL.md +1 -2
  310. package/skills/gsd-map-codebase/SKILL.md +1 -1
  311. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  312. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  313. package/skills/gsd-new-milestone/SKILL.md +1 -1
  314. package/skills/gsd-next/SKILL.md +0 -1
  315. package/skills/gsd-plan-phase/SKILL.md +0 -1
  316. package/skills/gsd-progress/SKILL.md +0 -1
  317. package/skills/gsd-quick/SKILL.md +1 -1
  318. package/skills/gsd-review-backlog/SKILL.md +2 -1
  319. package/skills/gsd-stats/SKILL.md +0 -1
  320. package/skills/gsd-verify-work/SKILL.md +1 -1
  321. package/vscode/package.json +1 -1
  322. package/gsd-core/workflows/discovery-phase.md +0 -298
  323. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  324. package/gsd-core/workflows/verify-phase.md +0 -574
  325. package/scripts/affected-tests-lib.cjs +0 -554
  326. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  327. package/scripts/run-affected-tests.cjs +0 -7
  328. package/scripts/run-tests.cjs +0 -1051
@@ -0,0 +1,740 @@
1
+ 'use strict';
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ /**
6
+ * Refactor-trigger command router — CLI subcommand dispatcher for
7
+ * `gsd-tools refactor` (issue #1953).
8
+ *
9
+ * Follows `src/intel-command-router.cts` exactly: `routeHubCommandFamily`,
10
+ * `makeInvalidArgs` for validation failures, `output(value, raw)` for
11
+ * success, `_`-prefixed injection seams, lazy `require` of the heavy leaf
12
+ * module (`complexity-trigger.cjs`) and the adjacent modules
13
+ * (`git-base-branch.cjs`, `broken-windows.cjs`) inside the route function.
14
+ *
15
+ * Authoritative surfaces:
16
+ * .gsd/phase/feat-1953-complexity-triggered-refactor/42-router-contract.md
17
+ * .gsd/phase/feat-1953-complexity-triggered-refactor/41-api-contract.md
18
+ *
19
+ * `src/complexity-trigger.cts` stays a pure leaf (analyzer, evaluator,
20
+ * baseline persistence) — this module owns capability-activation gating,
21
+ * git invocation (via the `src/git-base-branch.cts` adapters), config
22
+ * reads, CLI arg parsing, phase-directory resolution, and the optional
23
+ * broken-windows ledger integration. It never duplicates the leaf's logic.
24
+ *
25
+ * Arg indexing:
26
+ * args[0] = 'refactor' (family — matched by dispatchCapabilityCommand)
27
+ * args[1] = subcommand (accept | decline | evaluate | status)
28
+ * args[2..] = flags (--phase, --since, --reason, --raw)
29
+ *
30
+ * Seams: `_complexity` (complexity-trigger.cjs), `_git` (git-base-branch.cjs),
31
+ * `_windows` (broken-windows.cjs — OPTIONAL; absent or a throwing `require`
32
+ * is the documented degrade path, never an error), `_core` (output capture).
33
+ * Production callers omit all four.
34
+ */
35
+ const node_path_1 = __importDefault(require("node:path"));
36
+ const node_fs_1 = __importDefault(require("node:fs"));
37
+ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
38
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
39
+ const io = require("./io.cjs");
40
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
41
+ const commandRoutingHub = require("./command-routing-hub.cjs");
42
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
43
+ const cjsCommandRouterAdapter = require("./cjs-command-router-adapter.cjs");
44
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
45
+ const capabilityStateMod = require("./capability-state.cjs");
46
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
47
+ const capabilityActivationMod = require("./capability-activation.cjs");
48
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
49
+ const configLoaderMod = require("./config-loader.cjs");
50
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
51
+ const phaseLocatorMod = require("./phase-locator.cjs");
52
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
53
+ const planningWorkspaceMod = require("./planning-workspace.cjs");
54
+ const { output } = io;
55
+ const { makeInvalidArgs } = commandRoutingHub;
56
+ const { routeHubCommandFamily } = cjsCommandRouterAdapter;
57
+ const { isCapabilityActive } = capabilityStateMod;
58
+ const { resolveConfigKey } = capabilityActivationMod;
59
+ const { loadConfig } = configLoaderMod;
60
+ const { findPhaseInternal, listMilestonePhaseDirs } = phaseLocatorMod;
61
+ const { planningDir } = planningWorkspaceMod;
62
+ const CAPABILITY_ID = 'refactor-trigger';
63
+ // Default CoreModule implementation. `_core` seam overrides this entirely
64
+ // for test injection (captures output calls without writing to real stdout).
65
+ const _defaultCore = { output };
66
+ // ─── Disabled response ──────────────────────────────────────────────────────
67
+ function disabledResponse() {
68
+ return {
69
+ disabled: true,
70
+ message: 'refactor-trigger is not enabled. Enable with: gsd config-set refactor.trigger_enabled true',
71
+ };
72
+ }
73
+ // ─── Arg parsing ────────────────────────────────────────────────────────────
74
+ // Positive integer, optionally zero-padded, optionally with dotted decimal
75
+ // sub-phase segments (1, 01, 12, 3.1). Rejects 0, negative, letters, and
76
+ // whitespace — matches the router contract's "positive integer or decimal
77
+ // phase id" rule.
78
+ const PHASE_VALUE_RE = /^0*[1-9]\d*(?:\.\d+)*$/;
79
+ /**
80
+ * Reads a `--flag value` or `--flag=value` occurrence from `args`. Repeated
81
+ * occurrences are reported via `duplicated` (the last one wins in `value`,
82
+ * but callers should treat `duplicated` as invalid input). A token
83
+ * immediately following `--flag` that itself starts with `--` (or the flag
84
+ * being the last token) is NOT consumed as a value — `flagShaped` is set and
85
+ * `value` stays `''`, so `--phase --raw` never silently swallows `--raw`.
86
+ */
87
+ function readFlag(args, flag) {
88
+ let present = false;
89
+ let value = '';
90
+ let count = 0;
91
+ let flagShaped = false;
92
+ const eqPrefix = `${flag}=`;
93
+ for (let i = 0; i < args.length; i++) {
94
+ const a = args[i];
95
+ if (a === flag) {
96
+ count++;
97
+ present = true;
98
+ const next = args[i + 1];
99
+ if (next === undefined) {
100
+ value = '';
101
+ }
102
+ else if (next.startsWith('--')) {
103
+ value = '';
104
+ flagShaped = true;
105
+ }
106
+ else {
107
+ value = next;
108
+ flagShaped = false;
109
+ }
110
+ }
111
+ else if (a.startsWith(eqPrefix)) {
112
+ count++;
113
+ present = true;
114
+ value = a.slice(eqPrefix.length);
115
+ flagShaped = false;
116
+ }
117
+ }
118
+ return { present, value, duplicated: count > 1, flagShaped };
119
+ }
120
+ /**
121
+ * Validate `--phase`: required, and a positive integer or decimal phase id.
122
+ * Empty, whitespace-only, duplicated, `--phase=`, `--phase==N`, or a
123
+ * flag-shaped value all fail. Missing entirely -> REFACTOR_USAGE (no value
124
+ * was ever offered); present but invalid -> REFACTOR_INVALID_PHASE (a value
125
+ * was given but rejected). Never throws.
126
+ */
127
+ function requirePhaseArg(args, complexity, usage) {
128
+ const flag = readFlag(args, '--phase');
129
+ if (!flag.present) {
130
+ return { ok: false, result: makeInvalidArgs('phase', usage, complexity.REASON.REFACTOR_USAGE) };
131
+ }
132
+ const trimmed = flag.value.trim();
133
+ if (flag.duplicated || flag.flagShaped || trimmed === '' || !PHASE_VALUE_RE.test(trimmed)) {
134
+ return {
135
+ ok: false,
136
+ result: makeInvalidArgs('phase', `Invalid --phase value: ${JSON.stringify(flag.value)}. ${usage}`, complexity.REASON.REFACTOR_INVALID_PHASE),
137
+ };
138
+ }
139
+ return { ok: true, phase: trimmed };
140
+ }
141
+ /**
142
+ * Resolve the on-disk phase directory for a validated `--phase` value. A
143
+ * phase that cannot be located on disk (never planned, wrong number, an
144
+ * archived milestone this call isn't scoped to) returns `null` — this is a
145
+ * distinct fact from a git failure (git is fine; the phase just doesn't
146
+ * exist), so callers report REFACTOR_INVALID_PHASE, never
147
+ * REFACTOR_GIT_UNAVAILABLE, for this case.
148
+ */
149
+ function resolvePhaseDirForArg(cwd, phaseArg) {
150
+ const search = findPhaseInternal(cwd, phaseArg);
151
+ if (!search || search.found !== true)
152
+ return null;
153
+ return { phaseDir: search.directory, padded: search.phase_number };
154
+ }
155
+ /**
156
+ * Resolve `relFile` (a repo-relative path, typically from `git diff
157
+ * --name-only`) against `cwd` and refuse a path that escapes the project
158
+ * root. Returns the absolute path, or `null` when it escapes.
159
+ *
160
+ * Two layers: the string-level `startsWith` check is a cheap first gate but
161
+ * confines only the SYMLINK's own path, not its target — a symlink
162
+ * committed in the repo (e.g. `src/evil.cts -> /etc/passwd`) has an
163
+ * in-tree path that passes the string check, and `fs.readFileSync` on it
164
+ * would follow the link and read outside the root. `lstatSync` (which does
165
+ * NOT follow symlinks, unlike `statSync`) is the second gate: anything that
166
+ * is not a regular file — a symlink most of all — is refused outright.
167
+ * Refusing rather than resolving-and-re-confining is deliberate: it is
168
+ * simpler and strictly stricter (a symlink whose target legitimately lives
169
+ * inside the root is still refused, which is an acceptable false positive
170
+ * for this analyzer). An `lstatSync` failure (ENOENT on a broken symlink,
171
+ * EACCES, a race) is treated the same as "not a regular file" — never
172
+ * propagated — so callers can keep using their existing null-means-skip
173
+ * convention (`REASON.REFACTOR_FILE_UNREADABLE`) uniformly.
174
+ */
175
+ function resolveConfinedPath(cwd, relFile) {
176
+ const root = node_path_1.default.resolve(cwd);
177
+ const resolved = node_path_1.default.resolve(root, relFile);
178
+ if (resolved !== root && !resolved.startsWith(root + node_path_1.default.sep))
179
+ return null;
180
+ try {
181
+ if (!node_fs_1.default.lstatSync(resolved).isFile())
182
+ return null;
183
+ }
184
+ catch {
185
+ return null;
186
+ }
187
+ return resolved;
188
+ }
189
+ function artifactFileName(padded, complexity) {
190
+ return `${padded}${complexity.PROPOSAL_SUFFIX}`;
191
+ }
192
+ function artifactAbsPath(cwd, phaseDir, padded, complexity) {
193
+ return node_path_1.default.join(cwd, phaseDir, artifactFileName(padded, complexity));
194
+ }
195
+ function candidateKey(file, name) {
196
+ return `${file}::${name}`;
197
+ }
198
+ // ─── Atomic write (proposal + ledger) ───────────────────────────────────────
199
+ /**
200
+ * Atomic publish: write a sibling `.tmp.<pid>` file, then rename over the
201
+ * target via `retryRenameSync` (transient-Windows-lock-tolerant — matches
202
+ * `local/require-fs-op-fallback`, ADR-1703 Phase 6). On any failure, best-
203
+ * effort unlinks the temp file and rethrows.
204
+ */
205
+ function writeTextAtomic(filePath, content) {
206
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
207
+ const tmp = `${filePath}.${process.pid}.tmp`;
208
+ node_fs_1.default.writeFileSync(tmp, content, 'utf8');
209
+ try {
210
+ (0, shell_command_projection_cjs_1.retryRenameSync)(tmp, filePath);
211
+ }
212
+ catch (err) {
213
+ try {
214
+ node_fs_1.default.unlinkSync(tmp);
215
+ }
216
+ catch { /* best-effort cleanup */ }
217
+ throw err;
218
+ }
219
+ }
220
+ // ─── Config reads ───────────────────────────────────────────────────────────
221
+ /**
222
+ * Read `refactor.complexity_threshold` / `refactor.complexity_jump_delta` /
223
+ * `refactor.trigger_strict` via the same four-level precedence walk
224
+ * (`resolveConfigKey`) the capability-activation gate uses: loadConfig
225
+ * result -> workstream config.json -> root config.json -> registry
226
+ * `configSchema` default. Reading the frozen first-party registry directly
227
+ * is deliberate (not `capability-loader.cjs`'s `loadRegistry`): with
228
+ * `includeInstalled` omitted, `loadRegistry()` returns the exact same frozen
229
+ * object — refactor-trigger is first-party, so the overlay/consent machinery
230
+ * has nothing to add here and this avoids the extra indirection.
231
+ */
232
+ function readEvalConfig(cwd) {
233
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
234
+ const registry = require('./capability-registry.cjs');
235
+ const config = loadConfig(cwd);
236
+ const threshold = resolveConfigKey('refactor.complexity_threshold', { config, cwd, registry }).value;
237
+ const jumpDelta = resolveConfigKey('refactor.complexity_jump_delta', { config, cwd, registry }).value;
238
+ const strict = resolveConfigKey('refactor.trigger_strict', { config, cwd, registry }).value;
239
+ // Read via the SAME four-level precedence walk as the `refactor.*` keys
240
+ // above — reusing `resolveConfigKey`/`loadConfig`/the frozen registry
241
+ // rather than a second config reader. A missing key resolves to the
242
+ // registry default (false), matching "treat a missing key as falsy".
243
+ const windowsEnforce = resolveConfigKey('workflow.windows_enforce', { config, cwd, registry }).value;
244
+ return { threshold, jumpDelta, strict: Boolean(strict), windowsEnforce: Boolean(windowsEnforce) };
245
+ }
246
+ // ─── Strict-mode enforcement-gap warning (#1953) ────────────────────────────
247
+ const WINDOWS_ENFORCE_REMEDIATION = 'gsd config-set workflow.windows_enforce true';
248
+ /**
249
+ * `refactor.trigger_strict` only ever APPENDS a deviation window to the
250
+ * broken-windows ledger — a ship only actually stops when the separate
251
+ * `workflow.windows_enforce` toggle is also on. A user who enables only
252
+ * `refactor.trigger_strict` gets tracking with no enforcement and, absent
253
+ * this warning, no signal that this is the case. Fires strictly on TRIGGERED
254
+ * evaluates with strict mode on, mirroring the existing ledger-recording
255
+ * gate: either the broken-windows capability could not record the window at
256
+ * all (`ledgerRecorded === false` — the existing degrade path), or it did,
257
+ * but `workflow.windows_enforce` is falsy. Never fires with strict off.
258
+ */
259
+ function strictNotEnforcingWarning(complexity, ledgerRecorded, windowsEnforce) {
260
+ if (!ledgerRecorded) {
261
+ return {
262
+ reason: complexity.REASON.REFACTOR_STRICT_NOT_ENFORCING,
263
+ message: 'refactor.trigger_strict is on, but the broken-windows capability is unavailable, so ship will not '
264
+ + `actually be blocked. Install the broken-windows capability, then run: ${WINDOWS_ENFORCE_REMEDIATION}`,
265
+ };
266
+ }
267
+ if (!windowsEnforce) {
268
+ return {
269
+ reason: complexity.REASON.REFACTOR_STRICT_NOT_ENFORCING,
270
+ message: 'refactor.trigger_strict is on, but workflow.windows_enforce is off, so ship will not actually be '
271
+ + `blocked. Run: ${WINDOWS_ENFORCE_REMEDIATION}`,
272
+ };
273
+ }
274
+ return null;
275
+ }
276
+ // ─── Broken-windows integration (strict mode; OPTIONAL) ─────────────────────
277
+ function windowsLedgerPath(cwd, windows) {
278
+ return node_path_1.default.join(cwd, '.planning', windows.LEDGER_FILE_NAME);
279
+ }
280
+ function readLedgerOrEmpty(cwd, windows) {
281
+ const ledgerPath = windowsLedgerPath(cwd, windows);
282
+ try {
283
+ const raw = node_fs_1.default.readFileSync(ledgerPath, 'utf8');
284
+ return windows.parseLedger(raw);
285
+ }
286
+ catch (e) {
287
+ const code = (e && typeof e === 'object' && 'code' in e) ? String(e.code) : '';
288
+ if (code === 'ENOENT')
289
+ return windows.emptyLedger(new Date().toISOString());
290
+ return null; // unreadable/malformed — degrade rather than throw
291
+ }
292
+ }
293
+ /**
294
+ * Structural identity match (#1953 defect 2): a deviation window's identity
295
+ * is the typed `phase`/`file`/`line` triple `appendWindow` already persists
296
+ * — never the human-readable `description` prose. A reworded description or
297
+ * a user editing `WINDOWS.md` prose can never break dedup.
298
+ */
299
+ function findOpenDeviationEntry(ledger, phase, file, line) {
300
+ return ledger.entries.find((e) => e.status === 'open' && e.kind === 'deviation' && e.phase === phase && e.file === file && e.line === line) ?? null;
301
+ }
302
+ /**
303
+ * Shared require-or-degrade + ledger-read boilerplate for
304
+ * `recordStrictWindow` / `resolveLedgerWindow` (#1953 defect 5a). `_windows`
305
+ * unavailable (module absent, or its `require` throws) or the ledger
306
+ * unreadable both degrade to a `{ ok: false, note }` result — never an
307
+ * error, never throws.
308
+ */
309
+ function loadWindowsOrDegrade(cwd, windowsOverride) {
310
+ let windows;
311
+ try {
312
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
313
+ windows = windowsOverride ?? require('./broken-windows.cjs');
314
+ }
315
+ catch {
316
+ return { ok: false, note: 'broken-windows capability unavailable — proposal recorded locally only' };
317
+ }
318
+ const ledger = readLedgerOrEmpty(cwd, windows);
319
+ if (ledger === null) {
320
+ return { ok: false, note: 'broken-windows ledger unreadable — proposal recorded locally only' };
321
+ }
322
+ return { ok: true, windows, ledger };
323
+ }
324
+ /**
325
+ * Strict-mode window append (step 9 of `evaluate`). Degrades to
326
+ * `{ recorded: false, note }` per `loadWindowsOrDegrade` — never an error,
327
+ * never throws. Idempotent: re-evaluating the same still-untriaged phase
328
+ * finds the existing OPEN entry (matched structurally on phase + target
329
+ * file/line) and does not append a second one.
330
+ */
331
+ function recordStrictWindow(cwd, padded, target, windowsOverride) {
332
+ const loaded = loadWindowsOrDegrade(cwd, windowsOverride);
333
+ if (!loaded.ok)
334
+ return { recorded: false, note: loaded.note };
335
+ const { windows, ledger } = loaded;
336
+ if (findOpenDeviationEntry(ledger, padded, target.file, target.startLine)) {
337
+ return { recorded: true, note: 'already recorded for this phase (idempotent)' };
338
+ }
339
+ const key = candidateKey(target.file, target.name);
340
+ const description = `${key} — complexity ${target.score}` +
341
+ (target.baseline !== null ? ` (baseline ${target.baseline}, delta ${target.delta})` : '');
342
+ const now = new Date().toISOString();
343
+ try {
344
+ const result = windows.appendWindow(ledger, { kind: 'deviation', phase: padded, file: target.file, line: target.startLine, description }, { now });
345
+ writeTextAtomic(windowsLedgerPath(cwd, windows), windows.renderLedger(result.ledger));
346
+ return { recorded: true };
347
+ }
348
+ catch (e) {
349
+ return { recorded: false, note: `failed to record broken-windows entry: ${e instanceof Error ? e.message : String(e)}` };
350
+ }
351
+ }
352
+ /**
353
+ * Resolve (mark fixed/waived) the ledger window matching phase + target
354
+ * file/line, if any. Never throws. Mirrors `recordStrictWindow`'s
355
+ * `{ recorded, note }` degrade shape (#1953 defect 2): every
356
+ * `resolved: false` path carries a `note` naming the real reason, so
357
+ * `accept`/`decline` never tell a user "it failed" without saying why.
358
+ */
359
+ function resolveLedgerWindow(cwd, padded, file, line, kind, reasonText, windowsOverride) {
360
+ const loaded = loadWindowsOrDegrade(cwd, windowsOverride);
361
+ if (!loaded.ok)
362
+ return { resolved: false, note: loaded.note };
363
+ const { windows, ledger } = loaded;
364
+ const entry = findOpenDeviationEntry(ledger, padded, file, line);
365
+ if (!entry) {
366
+ return { resolved: false, note: 'no open broken-windows entry found for this phase/target — nothing to resolve' };
367
+ }
368
+ const now = new Date().toISOString();
369
+ try {
370
+ const updated = kind === 'accept'
371
+ ? windows.markFixed(ledger, entry.id, { now })
372
+ : windows.markWaived(ledger, entry.id, reasonText || 'declined', { now });
373
+ writeTextAtomic(windowsLedgerPath(cwd, windows), windows.renderLedger(updated));
374
+ return { resolved: true };
375
+ }
376
+ catch (e) {
377
+ return { resolved: false, note: `failed to resolve broken-windows entry: ${e instanceof Error ? e.message : String(e)}` };
378
+ }
379
+ }
380
+ // ─── evaluate ────────────────────────────────────────────────────────────────
381
+ const EVALUATE_USAGE = 'Usage: gsd-tools refactor evaluate --phase <N> [--since <ref>] [--raw]';
382
+ function handleEvaluate(args, cwd, raw, c, complexity, git, windowsOverride) {
383
+ const rest = args.slice(2);
384
+ const phaseCheck = requirePhaseArg(rest, complexity, EVALUATE_USAGE);
385
+ if (!phaseCheck.ok)
386
+ return phaseCheck.result;
387
+ // Step 3: resolve PHASE_DIR + anchor (phaseStartCommit, or --since override).
388
+ const resolved = resolvePhaseDirForArg(cwd, phaseCheck.phase);
389
+ if (resolved === null) {
390
+ c.output({ verdict: complexity.VERDICT.SKIPPED, reason: complexity.REASON.REFACTOR_INVALID_PHASE, phase: phaseCheck.phase }, raw);
391
+ return undefined;
392
+ }
393
+ const { phaseDir, padded } = resolved;
394
+ const sinceFlag = readFlag(rest, '--since');
395
+ const sinceOverride = sinceFlag.present && !sinceFlag.flagShaped ? sinceFlag.value.trim() : '';
396
+ const sinceRef = sinceOverride !== '' ? sinceOverride : git.phaseStartCommit(cwd, phaseDir);
397
+ if (sinceRef === null) {
398
+ c.output({ verdict: complexity.VERDICT.SKIPPED, reason: complexity.REASON.REFACTOR_GIT_UNAVAILABLE, phase: padded }, raw);
399
+ return undefined;
400
+ }
401
+ const touched = git.changedFilesSince(cwd, sinceRef);
402
+ if (touched === null) {
403
+ c.output({ verdict: complexity.VERDICT.SKIPPED, reason: complexity.REASON.REFACTOR_GIT_UNAVAILABLE, phase: padded }, raw);
404
+ return undefined;
405
+ }
406
+ // Step 4: empty touched set.
407
+ if (touched.length === 0) {
408
+ c.output({ verdict: complexity.VERDICT.BELOW_THRESHOLD, reason: complexity.REASON.REFACTOR_NO_TOUCHED_FILES, phase: padded }, raw);
409
+ return undefined;
410
+ }
411
+ // Step 5: filter with isAnalyzablePath; read each survivor. A read failure
412
+ // (or a path that escapes the project root) skips that one file with a
413
+ // reason and the run continues — never aborts the evaluation.
414
+ const analyzed = [];
415
+ // Only files that were SUCCESSFULLY analyzed feed nextBaseline's prune set
416
+ // — an unreadable file must never wipe that file's baseline history via
417
+ // the "file in analyzedFiles but function missing" prune rule.
418
+ const successfullyAnalyzedFiles = [];
419
+ for (const relFile of touched) {
420
+ if (!complexity.isAnalyzablePath(relFile))
421
+ continue;
422
+ const confined = resolveConfinedPath(cwd, relFile);
423
+ if (confined === null) {
424
+ analyzed.push({ file: relFile, ok: false, reason: complexity.REASON.REFACTOR_FILE_UNREADABLE });
425
+ continue;
426
+ }
427
+ let source;
428
+ try {
429
+ source = node_fs_1.default.readFileSync(confined, 'utf8');
430
+ }
431
+ catch {
432
+ analyzed.push({ file: relFile, ok: false, reason: complexity.REASON.REFACTOR_FILE_UNREADABLE });
433
+ continue;
434
+ }
435
+ const result = complexity.analyzeSource(source);
436
+ if (!result.ok) {
437
+ analyzed.push({ file: relFile, ok: false, reason: result.reason });
438
+ }
439
+ else {
440
+ analyzed.push({ file: relFile, ok: true, method: result.method, functions: result.functions });
441
+ successfullyAnalyzedFiles.push(relFile);
442
+ }
443
+ }
444
+ // Step 6.
445
+ const planningDirPath = planningDir(cwd);
446
+ const baselineRead = complexity.readBaseline(planningDirPath);
447
+ const evalConfig = readEvalConfig(cwd);
448
+ const evaluation = complexity.evaluateCandidates({
449
+ analyzed,
450
+ baseline: baselineRead.baseline,
451
+ threshold: evalConfig.threshold,
452
+ jumpDelta: evalConfig.jumpDelta,
453
+ });
454
+ // Step 7.
455
+ let artifactWritten = false;
456
+ let artifactPath = null;
457
+ let artifactWriteError = null;
458
+ if (evaluation.verdict === complexity.VERDICT.TRIGGERED && evaluation.target) {
459
+ const target = evaluation.target;
460
+ const proposal = {
461
+ schema_version: complexity.SCHEMA_VERSION,
462
+ status: 'proposed',
463
+ phase: padded,
464
+ target_file: target.file,
465
+ target_function: target.name,
466
+ score: target.score,
467
+ baseline: target.baseline,
468
+ delta: target.delta,
469
+ metric: 'decision-points',
470
+ recorded_at: new Date().toISOString(),
471
+ resolved_at: null,
472
+ reason: target.reasons.join(','),
473
+ candidates: evaluation.candidates,
474
+ };
475
+ artifactPath = artifactAbsPath(cwd, phaseDir, padded, complexity);
476
+ try {
477
+ writeTextAtomic(artifactPath, complexity.renderProposal(proposal));
478
+ artifactWritten = true;
479
+ }
480
+ catch (e) {
481
+ artifactWriteError = e instanceof Error ? e.message : String(e);
482
+ }
483
+ }
484
+ // Step 8: baseline write failure is reported but does not fail the command.
485
+ const nextBaselineValue = complexity.nextBaseline(baselineRead.baseline, analyzed, { analyzedFiles: successfullyAnalyzedFiles, phase: padded });
486
+ const baselineWrite = complexity.writeBaseline(planningDirPath, nextBaselineValue);
487
+ // Step 9: strict mode, TRIGGERED only.
488
+ let ledgerRecorded;
489
+ let ledgerNote;
490
+ const warnings = [];
491
+ if (evalConfig.strict && evaluation.verdict === complexity.VERDICT.TRIGGERED && evaluation.target) {
492
+ const strictResult = recordStrictWindow(cwd, padded, evaluation.target, windowsOverride);
493
+ ledgerRecorded = strictResult.recorded;
494
+ ledgerNote = strictResult.note;
495
+ const warning = strictNotEnforcingWarning(complexity, ledgerRecorded, evalConfig.windowsEnforce);
496
+ if (warning)
497
+ warnings.push(warning);
498
+ }
499
+ const result = {
500
+ verdict: evaluation.verdict,
501
+ phase: padded,
502
+ candidates: evaluation.candidates,
503
+ target: evaluation.target,
504
+ skipped: evaluation.skipped,
505
+ threshold_used: evaluation.thresholdUsed,
506
+ jump_delta_used: evaluation.jumpDeltaUsed,
507
+ artifact_written: artifactWritten,
508
+ artifact_path: artifactPath,
509
+ baseline_write: baselineWrite.ok,
510
+ };
511
+ if (artifactWriteError !== null)
512
+ result.artifact_write_error = artifactWriteError;
513
+ if (!baselineWrite.ok && baselineWrite.reason)
514
+ result.baseline_write_reason = baselineWrite.reason;
515
+ if (ledgerRecorded !== undefined)
516
+ result.ledger_recorded = ledgerRecorded;
517
+ if (ledgerNote !== undefined)
518
+ result.ledger_note = ledgerNote;
519
+ if (warnings.length > 0)
520
+ result.warnings = warnings;
521
+ c.output(result, raw);
522
+ return undefined;
523
+ }
524
+ // ─── status ──────────────────────────────────────────────────────────────────
525
+ const STATUS_USAGE = 'Usage: gsd-tools refactor status [--phase <N>] [--raw]';
526
+ function scanProposals(cwd, complexity) {
527
+ const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
528
+ const results = [];
529
+ // #1953 (phase-enumeration-drift guard): phase-directory enumeration has one
530
+ // owner (`listMilestonePhaseDirs`, src/phase-locator.cts). Called unscoped
531
+ // (no `cwd` in opts) to preserve this scan's prior all-phases-directory
532
+ // reach — the only behavior change is that sentinel directories (backlog
533
+ // `0-*` / icebox `999-*`, per the canonical `isSentinelPhaseId`) are now
534
+ // excluded, and the enumeration order is `comparePhaseNum`-sorted rather
535
+ // than raw filesystem order.
536
+ const { value: dirs } = listMilestonePhaseDirs(phasesDir);
537
+ for (const dirName of dirs) {
538
+ const full = node_path_1.default.join(phasesDir, dirName);
539
+ let files;
540
+ try {
541
+ files = node_fs_1.default.readdirSync(full);
542
+ }
543
+ catch {
544
+ continue;
545
+ }
546
+ for (const fileName of files) {
547
+ if (!fileName.endsWith(complexity.PROPOSAL_SUFFIX))
548
+ continue;
549
+ try {
550
+ const text = node_fs_1.default.readFileSync(node_path_1.default.join(full, fileName), 'utf8');
551
+ const proposal = complexity.parseProposal(text);
552
+ if (proposal) {
553
+ results.push({
554
+ phase: proposal.phase,
555
+ target_file: proposal.target_file,
556
+ target_function: proposal.target_function,
557
+ status: proposal.status,
558
+ score: proposal.score,
559
+ });
560
+ }
561
+ }
562
+ catch {
563
+ continue;
564
+ }
565
+ }
566
+ }
567
+ return results;
568
+ }
569
+ function handleStatus(args, cwd, raw, c, complexity) {
570
+ const rest = args.slice(2);
571
+ const phaseFlag = readFlag(rest, '--phase');
572
+ if (!phaseFlag.present) {
573
+ c.output({ proposals: scanProposals(cwd, complexity) }, raw);
574
+ return undefined;
575
+ }
576
+ const trimmed = phaseFlag.value.trim();
577
+ if (phaseFlag.duplicated || phaseFlag.flagShaped || trimmed === '' || !PHASE_VALUE_RE.test(trimmed)) {
578
+ return makeInvalidArgs('phase', `Invalid --phase value: ${JSON.stringify(phaseFlag.value)}. ${STATUS_USAGE}`, complexity.REASON.REFACTOR_INVALID_PHASE);
579
+ }
580
+ const resolved = resolvePhaseDirForArg(cwd, trimmed);
581
+ if (resolved === null) {
582
+ c.output({ found: false, phase: trimmed }, raw);
583
+ return undefined;
584
+ }
585
+ const artifactPath = artifactAbsPath(cwd, resolved.phaseDir, resolved.padded, complexity);
586
+ let text;
587
+ try {
588
+ text = node_fs_1.default.readFileSync(artifactPath, 'utf8');
589
+ }
590
+ catch {
591
+ c.output({ found: false, phase: resolved.padded }, raw);
592
+ return undefined;
593
+ }
594
+ const proposal = complexity.parseProposal(text);
595
+ if (proposal === null) {
596
+ c.output({ found: false, phase: resolved.padded }, raw);
597
+ return undefined;
598
+ }
599
+ c.output({ found: true, phase: resolved.padded, proposal }, raw);
600
+ return undefined;
601
+ }
602
+ // ─── accept / decline ──────────────────────────────────────────────────────
603
+ /**
604
+ * Re-measure the target function's CURRENT complexity score by re-reading
605
+ * and re-analyzing `proposal.target_file` — `accept` re-anchors to the
606
+ * post-refactor score and `decline` to the current (unchanged) score, and
607
+ * both need the LIVE value, not the frozen `proposal.score` snapshot from
608
+ * evaluate-time. Falls back to `proposal.score` when the file is gone,
609
+ * unreadable, unparseable, or the function can no longer be found (e.g.
610
+ * renamed) — reanchorBaseline always needs *a* number, and the proposal's
611
+ * last-known score is the least-surprising fallback.
612
+ */
613
+ function measureCurrentScore(cwd, proposal, complexity) {
614
+ const confined = resolveConfinedPath(cwd, proposal.target_file);
615
+ if (confined === null)
616
+ return proposal.score;
617
+ let source;
618
+ try {
619
+ source = node_fs_1.default.readFileSync(confined, 'utf8');
620
+ }
621
+ catch {
622
+ return proposal.score;
623
+ }
624
+ const result = complexity.analyzeSource(source);
625
+ if (!result.ok)
626
+ return proposal.score;
627
+ const fn = result.functions.find((f) => f.name === proposal.target_function);
628
+ return fn ? fn.score : proposal.score;
629
+ }
630
+ function dispositionUsage(kind) {
631
+ return kind === 'accept'
632
+ ? 'Usage: gsd-tools refactor accept --phase <N> [--raw]'
633
+ : 'Usage: gsd-tools refactor decline --phase <N> --reason "<text>" [--raw]';
634
+ }
635
+ function handleDisposition(kind, args, cwd, raw, c, complexity, windowsOverride) {
636
+ const rest = args.slice(2);
637
+ const usage = dispositionUsage(kind);
638
+ const phaseCheck = requirePhaseArg(rest, complexity, usage);
639
+ if (!phaseCheck.ok)
640
+ return phaseCheck.result;
641
+ let reasonText = '';
642
+ if (kind === 'decline') {
643
+ const reasonFlag = readFlag(rest, '--reason');
644
+ const trimmedReason = reasonFlag.value.trim();
645
+ if (!reasonFlag.present || reasonFlag.flagShaped || trimmedReason === '') {
646
+ return makeInvalidArgs('reason', usage, complexity.REASON.REFACTOR_DECLINE_REASON_EMPTY);
647
+ }
648
+ reasonText = reasonFlag.value;
649
+ }
650
+ const resolved = resolvePhaseDirForArg(cwd, phaseCheck.phase);
651
+ if (resolved === null) {
652
+ return makeInvalidArgs('phase', `No refactor proposal found for phase ${phaseCheck.phase}.`, complexity.REASON.REFACTOR_ARTIFACT_NOT_FOUND);
653
+ }
654
+ const artifactPath = artifactAbsPath(cwd, resolved.phaseDir, resolved.padded, complexity);
655
+ let text;
656
+ try {
657
+ text = node_fs_1.default.readFileSync(artifactPath, 'utf8');
658
+ }
659
+ catch {
660
+ return makeInvalidArgs('phase', `No refactor proposal found for phase ${resolved.padded}.`, complexity.REASON.REFACTOR_ARTIFACT_NOT_FOUND);
661
+ }
662
+ const proposal = complexity.parseProposal(text);
663
+ if (proposal === null) {
664
+ return makeInvalidArgs('phase', `Refactor proposal for phase ${resolved.padded} is unreadable.`, complexity.REASON.REFACTOR_ARTIFACT_NOT_FOUND);
665
+ }
666
+ if (proposal.status !== 'proposed') {
667
+ return makeInvalidArgs('phase', `Refactor proposal for phase ${resolved.padded} is already ${proposal.status}.`, complexity.REASON.REFACTOR_ALREADY_DISPOSITIONED);
668
+ }
669
+ const key = candidateKey(proposal.target_file, proposal.target_function);
670
+ const now = new Date().toISOString();
671
+ const liveScore = measureCurrentScore(cwd, proposal, complexity);
672
+ const targetCandidate = proposal.candidates.find((cand) => cand.file === proposal.target_file && cand.name === proposal.target_function) ?? proposal.candidates[0] ?? null;
673
+ const updatedProposal = {
674
+ ...proposal,
675
+ status: kind === 'accept' ? 'accepted' : 'declined',
676
+ resolved_at: now,
677
+ reason: kind === 'accept' ? proposal.reason : reasonText,
678
+ };
679
+ writeTextAtomic(artifactPath, complexity.renderProposal(updatedProposal));
680
+ const planningDirPath = planningDir(cwd);
681
+ const baselineRead = complexity.readBaseline(planningDirPath);
682
+ const nextBaselineValue = complexity.reanchorBaseline(baselineRead.baseline, key, liveScore, { phase: resolved.padded });
683
+ const baselineWrite = complexity.writeBaseline(planningDirPath, nextBaselineValue);
684
+ const ledgerResult = resolveLedgerWindow(cwd, resolved.padded, proposal.target_file, targetCandidate ? targetCandidate.startLine : -1, kind, reasonText, windowsOverride);
685
+ const dispositionResult = {
686
+ status: updatedProposal.status,
687
+ phase: resolved.padded,
688
+ target: key,
689
+ reanchored_to: liveScore,
690
+ baseline_write: baselineWrite.ok,
691
+ ledger_resolved: ledgerResult.resolved,
692
+ };
693
+ if (ledgerResult.note !== undefined)
694
+ dispositionResult.ledger_note = ledgerResult.note;
695
+ c.output(dispositionResult, raw);
696
+ return undefined;
697
+ }
698
+ // ─── Dispatch ────────────────────────────────────────────────────────────────
699
+ /**
700
+ * Shared capability-active gate + lazy `complexity-trigger.cjs` require
701
+ * (#1953 defect 5b) — identical across all four subcommand handlers. Emits
702
+ * the disabled response and returns `undefined` without invoking `run` when
703
+ * the capability is off; otherwise resolves the module (respecting the
704
+ * `_complexity` test seam) and hands it to `run`.
705
+ */
706
+ function withActiveComplexity(cwd, raw, c, complexityOverride, run) {
707
+ if (!isCapabilityActive(CAPABILITY_ID, cwd)) {
708
+ c.output(disabledResponse(), raw);
709
+ return undefined;
710
+ }
711
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
712
+ const complexity = complexityOverride ?? require('./complexity-trigger.cjs');
713
+ return run(complexity);
714
+ }
715
+ function routeRefactorTriggerCommand({ args, cwd, raw, error, _complexity, _git, _windows, _core }) {
716
+ const c = _core ?? _defaultCore;
717
+ routeHubCommandFamily({
718
+ family: 'refactor',
719
+ args,
720
+ // Alphabetical for a stable unknown-subcommand message, matching intel/graphify.
721
+ subcommands: ['accept', 'decline', 'evaluate', 'status'],
722
+ handlers: {
723
+ evaluate: () => withActiveComplexity(cwd, raw, c, _complexity, (complexity) => {
724
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
725
+ const git = _git ?? require('./git-base-branch.cjs');
726
+ return handleEvaluate(args, cwd, raw, c, complexity, git, _windows);
727
+ }),
728
+ status: () => withActiveComplexity(cwd, raw, c, _complexity, (complexity) => handleStatus(args, cwd, raw, c, complexity)),
729
+ accept: () => withActiveComplexity(cwd, raw, c, _complexity, (complexity) => handleDisposition('accept', args, cwd, raw, c, complexity, _windows)),
730
+ decline: () => withActiveComplexity(cwd, raw, c, _complexity, (complexity) => handleDisposition('decline', args, cwd, raw, c, complexity, _windows)),
731
+ },
732
+ unknownMessage: (subcommand, available) => `Unknown refactor subcommand. Available: ${available.join(', ')}`,
733
+ error,
734
+ cwd,
735
+ raw,
736
+ });
737
+ }
738
+ module.exports = {
739
+ routeRefactorTriggerCommand,
740
+ };