@opengsd/gsd-core 1.7.0 → 1.9.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 (261) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +45 -1
  4. package/README.md +2 -0
  5. package/agents/gsd-code-fixer.md +1 -1
  6. package/agents/gsd-codebase-mapper.md +1 -1
  7. package/agents/gsd-debug-session-manager.md +78 -4
  8. package/agents/gsd-debugger.md +87 -29
  9. package/agents/gsd-executor.md +49 -9
  10. package/agents/gsd-intel-updater.md +3 -3
  11. package/agents/gsd-phase-researcher.md +4 -2
  12. package/agents/gsd-plan-checker.md +20 -0
  13. package/agents/gsd-planner.md +44 -59
  14. package/agents/gsd-project-researcher.md +2 -2
  15. package/agents/gsd-ui-auditor.md +0 -40
  16. package/agents/gsd-verifier.md +2 -2
  17. package/bin/install.js +1338 -135
  18. package/commands/gsd/ai-integration-phase.md +1 -1
  19. package/commands/gsd/mempalace-capture.md +9 -5
  20. package/commands/gsd/new-milestone.md +1 -1
  21. package/commands/gsd/plan-phase.md +5 -3
  22. package/commands/gsd/plan-review-convergence.md +7 -2
  23. package/gsd-core/bin/gsd-tools.cjs +2690 -2472
  24. package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
  25. package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
  26. package/gsd-core/bin/lib/api-coverage.cjs +360 -53
  27. package/gsd-core/bin/lib/audit.cjs +8 -8
  28. package/gsd-core/bin/lib/broken-windows.cjs +716 -0
  29. package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
  30. package/gsd-core/bin/lib/capability-consent.cjs +40 -1
  31. package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
  32. package/gsd-core/bin/lib/capability-loader.cjs +23 -1
  33. package/gsd-core/bin/lib/capability-registry.cjs +1450 -160
  34. package/gsd-core/bin/lib/capability-trust.cjs +468 -33
  35. package/gsd-core/bin/lib/capability-validator.cjs +882 -6
  36. package/gsd-core/bin/lib/capability-writer.cjs +6 -1
  37. package/gsd-core/bin/lib/check-command-router.cjs +140 -27
  38. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
  39. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +209 -31
  40. package/gsd-core/bin/lib/claude-orchestration.cjs +203 -25
  41. package/gsd-core/bin/lib/command-aliases.cjs +14 -0
  42. package/gsd-core/bin/lib/commands.cjs +326 -21
  43. package/gsd-core/bin/lib/config-loader.cjs +214 -30
  44. package/gsd-core/bin/lib/config.cjs +158 -22
  45. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  46. package/gsd-core/bin/lib/decisions.cjs +32 -8
  47. package/gsd-core/bin/lib/docs.cjs +6 -0
  48. package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
  49. package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
  50. package/gsd-core/bin/lib/frontmatter.cjs +125 -15
  51. package/gsd-core/bin/lib/gap-checker.cjs +17 -2
  52. package/gsd-core/bin/lib/host-integration.cjs +215 -8
  53. package/gsd-core/bin/lib/init.cjs +155 -66
  54. package/gsd-core/bin/lib/install-engine.cjs +299 -23
  55. package/gsd-core/bin/lib/install-profiles.cjs +239 -1
  56. package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
  57. package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
  58. package/gsd-core/bin/lib/installer-migrations.cjs +44 -5
  59. package/gsd-core/bin/lib/markdown-sectionizer.cjs +107 -0
  60. package/gsd-core/bin/lib/milestone.cjs +248 -14
  61. package/gsd-core/bin/lib/model-catalog.cjs +69 -4
  62. package/gsd-core/bin/lib/model-resolver.cjs +189 -7
  63. package/gsd-core/bin/lib/observability/logger.cjs +7 -2
  64. package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
  65. package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
  66. package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
  67. package/gsd-core/bin/lib/phase-id.cjs +304 -9
  68. package/gsd-core/bin/lib/phase.cjs +258 -17
  69. package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
  70. package/gsd-core/bin/lib/plan-scan.cjs +70 -2
  71. package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
  72. package/gsd-core/bin/lib/profile-output.cjs +34 -8
  73. package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
  74. package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
  75. package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
  76. package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
  77. package/gsd-core/bin/lib/roadmap-parser.cjs +61 -10
  78. package/gsd-core/bin/lib/roadmap.cjs +23 -7
  79. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +38 -5
  80. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +23 -9
  81. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +156 -0
  82. package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
  83. package/gsd-core/bin/lib/smart-entry.cjs +70 -5
  84. package/gsd-core/bin/lib/state-document.cjs +171 -24
  85. package/gsd-core/bin/lib/state-transition.cjs +50 -11
  86. package/gsd-core/bin/lib/state.cjs +206 -32
  87. package/gsd-core/bin/lib/surface.cjs +51 -9
  88. package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
  89. package/gsd-core/bin/lib/uat.cjs +428 -11
  90. package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
  91. package/gsd-core/bin/lib/unusable-input.cjs +216 -0
  92. package/gsd-core/bin/lib/validate.cjs +44 -8
  93. package/gsd-core/bin/lib/verification.cjs +163 -31
  94. package/gsd-core/bin/lib/verify.cjs +348 -42
  95. package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
  96. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  97. package/gsd-core/bin/shared/config-schema.manifest.json +4 -15
  98. package/gsd-core/bin/shared/model-catalog.json +5 -0
  99. package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
  100. package/gsd-core/references/api-coverage.md +37 -7
  101. package/gsd-core/references/checkpoints.md +1 -1
  102. package/gsd-core/references/common-bug-patterns.md +13 -0
  103. package/gsd-core/references/context-budget.md +40 -0
  104. package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
  105. package/gsd-core/references/debugger-fix-acceptance.md +157 -0
  106. package/gsd-core/references/debugger-philosophy.md +1 -0
  107. package/gsd-core/references/debugger-prevention.md +98 -0
  108. package/gsd-core/references/debugger-rca-branching.md +98 -0
  109. package/gsd-core/references/debugger-repro-hardening.md +130 -0
  110. package/gsd-core/references/debugger-sbfl.md +110 -0
  111. package/gsd-core/references/debugger-semantic-recall.md +81 -0
  112. package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
  113. package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
  114. package/gsd-core/references/execute-phase-response-language.md +7 -0
  115. package/gsd-core/references/gate-prompts.md +6 -3
  116. package/gsd-core/references/model-profile-resolution.md +64 -13
  117. package/gsd-core/references/offer-next.md +88 -0
  118. package/gsd-core/references/planner-antipatterns.md +6 -0
  119. package/gsd-core/references/planner-mvp-mode.md +12 -13
  120. package/gsd-core/references/planner-preconditions.md +156 -0
  121. package/gsd-core/references/planner-reversibility.md +132 -0
  122. package/gsd-core/references/planning-config.md +2 -1
  123. package/gsd-core/references/reviewer-instances.md +28 -19
  124. package/gsd-core/references/runtime-aware-dispatch.md +42 -0
  125. package/gsd-core/references/skeleton-template.md +1 -1
  126. package/gsd-core/references/thinking-models-planning.md +3 -1
  127. package/gsd-core/references/ui-consideration-probe.md +2 -2
  128. package/gsd-core/references/worktree-branch-check.md +4 -4
  129. package/gsd-core/templates/DEBUG.md +5 -3
  130. package/gsd-core/templates/summary-minimal.md +4 -0
  131. package/gsd-core/templates/summary-standard.md +4 -0
  132. package/gsd-core/templates/summary.md +7 -0
  133. package/gsd-core/workflows/add-phase.md +2 -0
  134. package/gsd-core/workflows/add-tests.md +3 -1
  135. package/gsd-core/workflows/add-todo.md +32 -1
  136. package/gsd-core/workflows/ai-integration-phase.md +8 -6
  137. package/gsd-core/workflows/audit-fix.md +6 -2
  138. package/gsd-core/workflows/audit-milestone.md +8 -0
  139. package/gsd-core/workflows/autonomous.md +19 -15
  140. package/gsd-core/workflows/check-todos.md +5 -3
  141. package/gsd-core/workflows/cleanup.md +7 -1
  142. package/gsd-core/workflows/code-review-fix.md +14 -6
  143. package/gsd-core/workflows/code-review.md +93 -24
  144. package/gsd-core/workflows/complete-milestone.md +3 -0
  145. package/gsd-core/workflows/debug.md +35 -7
  146. package/gsd-core/workflows/diagnose-issues.md +5 -1
  147. package/gsd-core/workflows/discovery-phase.md +7 -0
  148. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
  149. package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
  150. package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
  151. package/gsd-core/workflows/discuss-phase-assumptions.md +18 -9
  152. package/gsd-core/workflows/discuss-phase.md +2 -2
  153. package/gsd-core/workflows/do.md +7 -1
  154. package/gsd-core/workflows/docs-update.md +9 -0
  155. package/gsd-core/workflows/eval-review.md +4 -1
  156. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
  157. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
  158. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
  159. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
  160. package/gsd-core/workflows/execute-phase.md +110 -149
  161. package/gsd-core/workflows/execute-plan.md +20 -8
  162. package/gsd-core/workflows/explore.md +4 -0
  163. package/gsd-core/workflows/extract-learnings.md +21 -0
  164. package/gsd-core/workflows/graduation.md +3 -0
  165. package/gsd-core/workflows/health.md +7 -1
  166. package/gsd-core/workflows/help/modes/full.md +9 -5
  167. package/gsd-core/workflows/import.md +11 -2
  168. package/gsd-core/workflows/inbox.md +7 -0
  169. package/gsd-core/workflows/ingest-docs.md +19 -10
  170. package/gsd-core/workflows/manager.md +3 -1
  171. package/gsd-core/workflows/map-codebase.md +17 -10
  172. package/gsd-core/workflows/mvp-phase.md +3 -0
  173. package/gsd-core/workflows/new-milestone.md +79 -23
  174. package/gsd-core/workflows/new-project.md +28 -19
  175. package/gsd-core/workflows/new-workspace.md +3 -1
  176. package/gsd-core/workflows/next.md +5 -2
  177. package/gsd-core/workflows/onboard.md +3 -0
  178. package/gsd-core/workflows/plan-phase.md +56 -51
  179. package/gsd-core/workflows/plan-review-convergence.md +61 -12
  180. package/gsd-core/workflows/plant-seed.md +3 -0
  181. package/gsd-core/workflows/profile-user.md +7 -1
  182. package/gsd-core/workflows/progress.md +31 -3
  183. package/gsd-core/workflows/quick.md +33 -10
  184. package/gsd-core/workflows/remove-workspace.md +3 -0
  185. package/gsd-core/workflows/review.md +172 -585
  186. package/gsd-core/workflows/scan.md +10 -2
  187. package/gsd-core/workflows/secure-phase.md +13 -2
  188. package/gsd-core/workflows/settings-integrations.md +3 -0
  189. package/gsd-core/workflows/settings.md +3 -0
  190. package/gsd-core/workflows/ship.md +88 -11
  191. package/gsd-core/workflows/sketch.md +3 -0
  192. package/gsd-core/workflows/smart-entry.md +4 -1
  193. package/gsd-core/workflows/spike.md +7 -1
  194. package/gsd-core/workflows/ui-phase.md +11 -2
  195. package/gsd-core/workflows/ui-review.md +11 -1
  196. package/gsd-core/workflows/undo.md +7 -0
  197. package/gsd-core/workflows/update.md +106 -5
  198. package/gsd-core/workflows/validate-phase.md +13 -2
  199. package/gsd-core/workflows/verify-phase.md +2 -2
  200. package/gsd-core/workflows/verify-work.md +15 -4
  201. package/hooks/dist/gsd-context-monitor.js +27 -9
  202. package/hooks/dist/gsd-cursor-session-start.js +6 -2
  203. package/hooks/dist/gsd-cursor-stop.js +6 -2
  204. package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
  205. package/hooks/dist/gsd-graphify-update.sh +9 -0
  206. package/hooks/dist/gsd-phase-boundary.sh +14 -2
  207. package/hooks/dist/gsd-prompt-guard.js +101 -2
  208. package/hooks/dist/gsd-read-guard.js +100 -2
  209. package/hooks/dist/gsd-read-injection-scanner.js +109 -2
  210. package/hooks/dist/gsd-statusline.js +97 -9
  211. package/hooks/dist/gsd-workflow-guard.js +110 -6
  212. package/hooks/dist/gsd-worktree-path-guard.js +132 -8
  213. package/hooks/dist/lib/cursor-workspace.js +74 -0
  214. package/hooks/gsd-context-monitor.js +27 -9
  215. package/hooks/gsd-cursor-session-start.js +6 -2
  216. package/hooks/gsd-cursor-stop.js +6 -2
  217. package/hooks/gsd-cursor-subagent-start.js +6 -2
  218. package/hooks/gsd-graphify-update.sh +9 -0
  219. package/hooks/gsd-phase-boundary.sh +14 -2
  220. package/hooks/gsd-prompt-guard.js +101 -2
  221. package/hooks/gsd-read-guard.js +100 -2
  222. package/hooks/gsd-read-injection-scanner.js +109 -2
  223. package/hooks/gsd-statusline.js +97 -9
  224. package/hooks/gsd-workflow-guard.js +110 -6
  225. package/hooks/gsd-worktree-path-guard.js +132 -8
  226. package/hooks/lib/cursor-workspace.js +74 -0
  227. package/package.json +10 -8
  228. package/pi/gsd.cjs +34 -3
  229. package/scripts/changeset/lint.cjs +1 -0
  230. package/scripts/changeset/parse.cjs +26 -0
  231. package/scripts/check-coverage-gate.cjs +51 -0
  232. package/scripts/check-glossary-refs.cjs +244 -0
  233. package/scripts/ci-rebase-check.cjs +48 -4
  234. package/scripts/ci-test-scope.cjs +67 -17
  235. package/scripts/gen-adr-index.cjs +528 -0
  236. package/scripts/gen-capability-matrix.cjs +26 -2
  237. package/scripts/gen-capability-registry.cjs +132 -34
  238. package/scripts/gen-emitted-baseline.cjs +145 -0
  239. package/scripts/gen-test-timings.cjs +201 -0
  240. package/scripts/lint-compiled-artifact-sync.cjs +146 -0
  241. package/scripts/lint-emitted-drift-ack.cjs +149 -0
  242. package/scripts/lint-fix-has-regression-test.cjs +131 -0
  243. package/scripts/lint-portable-timeout.cjs +140 -0
  244. package/scripts/lint-resolution-provenance.cjs +9 -0
  245. package/scripts/lint-test-file-count.allowlist.json +1 -0
  246. package/scripts/mutation-matrix.cjs +4 -0
  247. package/scripts/prompt-injection-scan.sh +6 -0
  248. package/scripts/registry-schema.cjs +57 -8
  249. package/scripts/release-notes/conventional-title.cjs +19 -1
  250. package/scripts/release-notes/format-github-release-notes.cjs +7 -3
  251. package/scripts/release-tarball-smoke.cjs +18 -11
  252. package/scripts/run-tests.cjs +420 -58
  253. package/scripts/workflow-size.cjs +16 -8
  254. package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
  255. package/skills/gsd-mempalace-capture/SKILL.md +9 -5
  256. package/skills/gsd-new-milestone/SKILL.md +1 -1
  257. package/skills/gsd-plan-phase/SKILL.md +5 -3
  258. package/skills/gsd-plan-review-convergence/SKILL.md +7 -2
  259. package/vscode/package.json +1 -1
  260. package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
  261. package/scripts/update-size-baseline.cjs +0 -68
@@ -37,6 +37,100 @@ function nearestExistingDir(start) {
37
37
  return null;
38
38
  }
39
39
 
40
+ // #2304: Kimi's native hook bus delivers Kimi's tool vocabulary in the payload
41
+ // (Write → WriteFile, Edit/MultiEdit → StrReplaceFile) while the [[hooks]]
42
+ // matcher is registered pre-translated (runtime-hooks-surface.cts
43
+ // buildKimiHooksTomlBlock) — so without normalizing the payload too, the
44
+ // matcher fires but the tool_name check below exits 0 and the guard is dormant
45
+ // on Kimi. The tool_input field names differ as well (kimi-cli
46
+ // src/kimi_cli/tools/file/{write,replace}.py): WriteFile takes `path`/`content`,
47
+ // StrReplaceFile takes `path` + `edit: Edit | list[Edit]` with `old`/`new` —
48
+ // kimi-cli's hooks/events.py forwards tool_input verbatim, so both layers need
49
+ // mapping. Accepts bare and module-qualified ('kimi_cli.tools.file:WriteFile')
50
+ // names; unknown names fall through untouched. Inlined per guard (not
51
+ // hooks/lib/): hook scripts are staged as standalone files, and a sibling
52
+ // require is a staging dependency that can fail silently.
53
+ // A Map, not an object literal: bare bracket lookup resolves prototype keys
54
+ // ('constructor', '__proto__', 'toString') to truthy functions/objects, so the
55
+ // !mapped fall-through never fires for them; Map.get returns undefined (same
56
+ // shape as canonicalizeRuntimeName in src/runtime-name-policy.cts).
57
+ const KIMI_TOOL_NAMES = new Map([['WriteFile', 'Write'], ['StrReplaceFile', 'Edit'], ['ReadFile', 'Read'], ['Shell', 'Bash']]);
58
+ function normalizeKimiPayload(data) {
59
+ // #2595 (review nit): `JSON.parse('null')` is null, and null/primitive
60
+ // payloads reached the `data.tool_name` read below and threw — falsifying
61
+ // this function's own "total over the inputs JSON can express" claim, which
62
+ // property (e) now tests directly. Harmless in practice (a null payload has
63
+ // nothing to guard, and the throw landed in the same fail-open catch as the
64
+ // exit-0 it now takes deliberately) but the claim should be true as stated.
65
+ if (data === null || typeof data !== 'object') return data;
66
+ const raw = data.tool_name;
67
+ if (typeof raw !== 'string') return data;
68
+ const mapped = KIMI_TOOL_NAMES.get(raw.slice(raw.lastIndexOf(':') + 1));
69
+ if (!mapped) return data;
70
+ data.tool_name = mapped;
71
+ if (data.tool_response === undefined && data.tool_output !== undefined) {
72
+ data.tool_response = data.tool_output;
73
+ }
74
+ const input = data.tool_input;
75
+ if (input && typeof input === 'object') {
76
+ // #2547 (review): Kimi's `path` is AUTHORITATIVE — it must win outright,
77
+ // not merely fill in when `file_path` happens to be absent. kimi-cli's file
78
+ // tools carry no `file_path` field at all (src/kimi_cli/tools/file/write.py,
79
+ // replace.py, @ 4a550ef — the SHA #2547 pins), and soul/toolset.py hands the
80
+ // model's raw json-parsed
81
+ // arguments to PreToolUse verbatim, doing typed validation only later inside
82
+ // tool.call() — after the hook has already decided. So a `file_path` in a
83
+ // Kimi payload is ALWAYS model-supplied, and under the old `=== undefined`
84
+ // condition it SHADOWED the field kimi-cli actually executes on. A payload
85
+ // pairing a cross-root `path` with a spurious `file_path: ""` left every
86
+ // guard reading an empty string and exiting 0, while the identical write
87
+ // without the extra key blocked — a bypass needing no crash at all. The same
88
+ // shadowing also preserved a NON-STRING `file_path` (`[]`), which threw
89
+ // inside gsd-worktree-path-guard's path.isAbsolute() and reached its outer
90
+ // `catch { process.exit(0) }`: the same crash-to-allow this fix closes
91
+ // elsewhere, reached through the guard's own read rather than through
92
+ // normalization. Overwriting can only ever narrow what a guard inspects to
93
+ // the path that will actually be written, so it cannot under-block.
94
+ if (typeof input.path === 'string') {
95
+ input.file_path = input.path;
96
+ }
97
+ const edits = Array.isArray(input.edit) ? input.edit
98
+ : (input.edit && typeof input.edit === 'object') ? [input.edit] : [];
99
+ if (edits.length) {
100
+ // #2547: `e?.old`, not `e.old` — `??` guards the value, not the
101
+ // dereference, so a NULLISH entry (`edit: [null]`) threw a TypeError
102
+ // here. normalizeKimiPayload runs before any tool dispatch, so that throw
103
+ // reached each guard's outer `catch { process.exit(0) }` and silently
104
+ // downgraded a should-BLOCK call into an allow. (A string/number entry
105
+ // never threw — `('x').old` is a legal read yielding undefined.)
106
+ //
107
+ // The String() coercion is guarded for the same reason: `{"toString":
108
+ // null}` is valid JSON that throws "Cannot convert object to primitive
109
+ // value", which is the identical crash-to-allow with a different
110
+ // trigger. Degrading only the non-coercible entry to '' keeps
111
+ // stringification intact for every value that CAN coerce (numbers,
112
+ // arrays, plain objects), so nothing downstream — including
113
+ // gsd-prompt-guard's scan of new_string — loses content it saw before.
114
+ const editText = (v) => { try { return String(v ?? ''); } catch { return ''; } };
115
+ // #2595 (review Major 2): reconstruct UNCONDITIONALLY, mirroring the
116
+ // `path` decision above rather than merely filling in when the field
117
+ // happens to be absent. kimi-cli's StrReplaceFile schema is `path` +
118
+ // `edit` only (src/kimi_cli/tools/file/replace.py @ 4a550ef) — it carries
119
+ // no `old_string`/`new_string` at all, so either field appearing in a
120
+ // Kimi payload is ALWAYS model-supplied, exactly like `file_path`. Under
121
+ // the old `=== undefined` condition a model-supplied `new_string: ""`
122
+ // SHADOWED the reconstruction, leaving gsd-prompt-guard's injection scan
123
+ // reading '' and exiting at its `if (!content)` before it ever saw the
124
+ // real `edit[].new` — a one-key bypass of the very scan this fix's
125
+ // guarded coercion exists to keep fed. A `typeof` test would NOT close
126
+ // it: a benign non-empty string shadows just as effectively as ''.
127
+ input.old_string = edits.map((e) => editText(e?.old)).join('\n');
128
+ input.new_string = edits.map((e) => editText(e?.new)).join('\n');
129
+ }
130
+ }
131
+ return data;
132
+ }
133
+
40
134
  let input = '';
41
135
  const stdinTimeout = setTimeout(() => process.exit(0), 3000);
42
136
  process.stdin.setEncoding('utf8');
@@ -44,7 +138,7 @@ process.stdin.on('data', chunk => input += chunk);
44
138
  process.stdin.on('end', () => {
45
139
  clearTimeout(stdinTimeout);
46
140
  try {
47
- const data = JSON.parse(input);
141
+ const data = normalizeKimiPayload(JSON.parse(input));
48
142
  const toolName = data.tool_name;
49
143
 
50
144
  // Only guard Edit, Write, and MultiEdit tool calls
@@ -72,13 +166,14 @@ process.stdin.on('end', () => {
72
166
  }
73
167
 
74
168
  // #1342: Only enforce inside a GSD-managed isolated executor worktree. Those
75
- // are always on a `worktree-agent-*` branch (the positive allow-list enforced
76
- // by worktree-branch-check.md, #2924). A manually-created linked worktree (plain
77
- // non-GSD work, e.g. Claude Code plan-mode) is on the user's own branch, so the
78
- // guard must be a no-op there. Detached HEAD / error → not GSD-managed → no-op.
169
+ // are always on an `agent-*` or legacy `worktree-agent-*` branch (the positive
170
+ // allow-list enforced by worktree-branch-check.md, #2924, #1995). A manually-
171
+ // created linked worktree (plain non-GSD work, e.g. Claude Code plan-mode) is
172
+ // on the user's own branch, so the guard must be a no-op there. Detached HEAD
173
+ // / error → not GSD-managed → no-op.
79
174
  const branchResult = git(['symbolic-ref', '--short', 'HEAD'], cwd);
80
175
  const branch = branchResult.status === 0 && branchResult.stdout ? branchResult.stdout.trim() : '';
81
- if (!/^worktree-agent-[A-Za-z0-9._/-]+$/.test(branch)) {
176
+ if (!/^(worktree-)?agent-[A-Za-z0-9._/-]+$/.test(branch)) {
82
177
  process.exit(0); // not a GSD-managed executor worktree — no-op
83
178
  }
84
179
 
@@ -91,12 +186,37 @@ process.stdin.on('end', () => {
91
186
  }
92
187
  const wtTopRaw = wtTopResult.stdout.trim();
93
188
 
94
- const rawFilePath = data.tool_input?.file_path || '';
189
+ // #2595 (review Major 3): read the field TYPED. `?.file_path || ''` let a
190
+ // non-string through — `[]` and `{}` are truthy, so they survived the
191
+ // `!rawFilePath` check and threw inside path.isAbsolute() below, landing in
192
+ // this script's outer `catch { process.exit(0) }`. That is the same
193
+ // crash-to-allow #2547 closes elsewhere, reached through the guard's own
194
+ // read rather than through normalization, and it is NOT closed by making
195
+ // `path` authoritative: normalization returns early for native Claude Code
196
+ // payloads (KIMI_TOOL_NAMES has no 'Edit' entry), so `{"tool_name":"Edit",
197
+ // "tool_input":{"file_path":[]}}` reached it untouched — this guard's
198
+ // original #260 surface. Same shape as hooks/gsd-windsurf-pre-write.js:75.
199
+ const rawFilePath = typeof data.tool_input?.file_path === 'string'
200
+ ? data.tool_input.file_path
201
+ : '';
95
202
  if (!rawFilePath) {
96
203
  process.exit(0);
97
204
  }
98
205
 
99
- // Relative paths are always safe — they resolve relative to CWD inside the worktree
206
+ // Relative paths resolve against the tool's CWD, which is inside the worktree
207
+ // — so under the runtime this guard was written for they cannot leave it.
208
+ //
209
+ // #2595 (review Minor 5) — state the premise rather than leave it implicit,
210
+ // because THIS PR is what widened the guard's reach to Kimi. "Always safe"
211
+ // holds only while every runtime reaching here either rejects relative paths
212
+ // or resolves them against the worktree CWD. Claude Code's Edit/Write require
213
+ // an absolute file_path, so the original #260 surface satisfies it by
214
+ // construction. kimi-cli's StrReplaceFile takes `path` with no documented
215
+ // absoluteness guarantee, and its resolution behaviour is NOT verified here
216
+ // (no source available to this repo at 4a550ef beyond the schema). If it
217
+ // resolves relative paths against anything other than the tool CWD, a
218
+ // `../`-laden path exits 0 at this line and escapes the worktree. Stating a
219
+ // mechanism and an unverified premise — not asserting a live bypass.
100
220
  if (!path.isAbsolute(rawFilePath)) {
101
221
  process.exit(0);
102
222
  }
@@ -151,6 +271,8 @@ process.stdin.on('end', () => {
151
271
  `absolute path is not permitted from an isolated executor worktree. Use a relative path.`,
152
272
  };
153
273
  process.stdout.write(JSON.stringify(output));
274
+ // Kimi feeds stderr (not stdout) back to the model on exit 2.
275
+ process.stderr.write(output.reason);
154
276
  process.exit(2);
155
277
  }
156
278
  // Outside all git repositories — fail open (#1342).
@@ -177,6 +299,8 @@ process.stdin.on('end', () => {
177
299
  };
178
300
 
179
301
  process.stdout.write(JSON.stringify(output));
302
+ // Kimi feeds stderr (not stdout) back to the model on exit 2.
303
+ process.stderr.write(output.reason);
180
304
  process.exit(2);
181
305
  } catch {
182
306
  // Silent fail — never block valid tool calls due to hook errors
@@ -0,0 +1,74 @@
1
+ // cursor-workspace.js — shared workspace resolution for Cursor lifecycle hooks (#2587).
2
+ //
3
+ // No `gsd-hook-version:` marker, deliberately — matching hooks/lib/git-cmd.js.
4
+ // Neither copy path that stages hooks/lib/*.js substitutes the version
5
+ // placeholder (copyLibDir stamps .sh files only), and the managed-hooks
6
+ // staleness scan covers top-level hooks/ names, not hooks/lib/. A marker here
7
+ // would ship to users as an unsubstituted literal.
8
+ //
9
+ // Cursor invokes hooks with cwd set to the Cursor config dir (~/.cursor) under
10
+ // the cursor-agent CLI, NOT the workspace. Every hook that resolves a project
11
+ // path from process.cwd() therefore missed: sessionStart could only emit its
12
+ // "no .planning/ workflow found" nudge, stop's verify-work reminder never fired,
13
+ // and subagentStart left every subagent without phase context.
14
+ //
15
+ // The hook payload carries the real path in `workspace_roots`. This module is
16
+ // the single place that turns that payload into a project root, so the three
17
+ // hooks cannot drift apart (they previously carried three copies of it).
18
+ //
19
+ // Consumers: gsd-cursor-session-start.js, gsd-cursor-stop.js,
20
+ // gsd-cursor-subagent-start.js. Staged into <config>/hooks/lib/ by
21
+ // writeCursorHooksJson (src/runtime-hooks-surface.cts) alongside the scripts
22
+ // that require it, and registered in the installer's GSD_HOOK_LIB_FILES so
23
+ // uninstall and the manifest manage it.
24
+
25
+ 'use strict';
26
+
27
+ const fs = require('fs');
28
+ const path = require('path');
29
+
30
+ /**
31
+ * Resolve the workspace root a Cursor hook should report on.
32
+ *
33
+ * Search order:
34
+ * 1. Each entry of `workspace_roots` that actually carries .planning/STATE.md
35
+ * — so a multi-root workspace whose GSD project is not the first root
36
+ * still resolves.
37
+ * 2. process.cwd(), if IT carries .planning/STATE.md. cwd is a CANDIDATE, not
38
+ * merely the empty-roots fallback: an IDE invocation can supply
39
+ * workspace_roots AND run from the project, and searching roots alone
40
+ * would report "absent" for a project sitting right at cwd — narrower than
41
+ * the pre-fix always-cwd behavior this replaces.
42
+ * 3. The first declared root, else cwd — so the "absent" message still names
43
+ * a sensible directory when there is genuinely no project.
44
+ *
45
+ * Never throws: a malformed payload degrades to cwd rather than wedging the
46
+ * session. (fs.existsSync itself does not throw — it returns false for NUL
47
+ * bytes, over-long components, and non-directory ancestors.)
48
+ *
49
+ * @param {string} rawInput Raw stdin payload as received by the hook.
50
+ * @returns {string} Absolute-or-relative directory to resolve .planning/ against.
51
+ */
52
+ function resolveWorkspaceRoot(rawInput) {
53
+ let input = {};
54
+ try { input = JSON.parse(rawInput || '{}'); } catch { /* fall back to cwd */ }
55
+ const roots = Array.isArray(input.workspace_roots)
56
+ ? input.workspace_roots.filter((r) => typeof r === 'string' && r.length > 0)
57
+ : [];
58
+ for (const root of [...roots, process.cwd()]) {
59
+ if (fs.existsSync(path.join(root, '.planning', 'STATE.md'))) return root;
60
+ }
61
+ return roots[0] || process.cwd();
62
+ }
63
+
64
+ /**
65
+ * Convenience: the .planning/STATE.md path for the resolved workspace.
66
+ *
67
+ * @param {string} rawInput Raw stdin payload as received by the hook.
68
+ * @returns {string}
69
+ */
70
+ function resolveStatePath(rawInput) {
71
+ return path.join(resolveWorkspaceRoot(rawInput), '.planning', 'STATE.md');
72
+ }
73
+
74
+ module.exports = { resolveWorkspaceRoot, resolveStatePath };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengsd/gsd-core",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
5
5
  "main": ".opencode/plugins/gsd-core.js",
6
6
  "bin": {
@@ -65,12 +65,14 @@
65
65
  "eslint-plugin-no-only-tests": "^3.4.0",
66
66
  "fast-check": "^4.8.0",
67
67
  "globals": "^16.5.0",
68
- "js-yaml": "^4.2.0",
68
+ "js-yaml": "^4.2.1",
69
69
  "typescript": "^6.0.3",
70
70
  "typescript-eslint": "^8.60.0"
71
71
  },
72
72
  "overrides": {
73
- "qs": ">=6.15.2"
73
+ "qs": ">=6.15.2",
74
+ "body-parser": ">=2.3.0",
75
+ "@hono/node-server": ">=2.0.5"
74
76
  },
75
77
  "optionalDependencies": {
76
78
  "fallow": "^2.70.0"
@@ -90,7 +92,8 @@
90
92
  "gen:plugin-skills": "node scripts/gen-plugin-skills.cjs --write",
91
93
  "gen:capability-registry": "node scripts/gen-capability-registry.cjs --write",
92
94
  "gen:registry": "node scripts/gen-registry.cjs --write",
93
- "gen:golden": "node scripts/gen-golden-install-parity-zcode.cjs && node scripts/gen-install-tree-fixtures.cjs",
95
+ "gen:install-tree": "node scripts/gen-install-tree-fixtures.cjs",
96
+ "regen:derived": "npm run build && npm run gen:registry && node scripts/gen-adr-index.cjs --write && node scripts/gen-capability-matrix.cjs --write && node scripts/gen-inventory-manifest.cjs --write && node scripts/sync-manifest-versions.cjs && npm run gen:install-tree",
94
97
  "validate:registry": "node scripts/validate-registry.cjs",
95
98
  "prepack": "npm run build:lib",
96
99
  "prepare": "npm run build:lib",
@@ -101,7 +104,7 @@
101
104
  "lint": "eslint . --cache --cache-location node_modules/.cache/eslint/",
102
105
  "lint:fix": "eslint . --fix",
103
106
  "lint:table-schema-drift": "node scripts/lint-table-schema-drift.cjs",
104
- "lint:ci": "npm run lint && npm run lint:skill-deps && npm run lint:generated-sync && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-allow-test-rule-refs.cjs && node scripts/lint-resolution-provenance.cjs && node scripts/validate-registry.cjs && node scripts/lint-table-schema-drift.cjs",
107
+ "lint:ci": "npm run lint && npm run lint:skill-deps && npm run lint:generated-sync && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-allow-test-rule-refs.cjs && node scripts/lint-resolution-provenance.cjs && node scripts/lint-emitted-drift-ack.cjs && node scripts/lint-portable-timeout.cjs && node scripts/validate-registry.cjs && node scripts/lint-table-schema-drift.cjs && node scripts/lint-fix-has-regression-test.cjs",
105
108
  "lint:allow-test-rule-refs": "node scripts/lint-allow-test-rule-refs.cjs",
106
109
  "lint:regression-names": "node scripts/lint-regression-test-names.cjs",
107
110
  "lint:descriptions": "node scripts/lint-descriptions.cjs",
@@ -109,11 +112,10 @@
109
112
  "lint:test-file-count": "node scripts/lint-test-file-count.cjs",
110
113
  "lint:pr-checks": "node scripts/lint-pr-check-project-dir.cjs",
111
114
  "lint:changeset": "node scripts/changeset/lint.cjs",
112
- "lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check && node scripts/gen-registry.cjs --check",
115
+ "lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check && node scripts/gen-registry.cjs --check && node scripts/gen-adr-index.cjs --check && node scripts/check-glossary-refs.cjs --check && node scripts/lint-compiled-artifact-sync.cjs --check",
113
116
  "lint:docs": "node scripts/lint-docs-required.cjs",
114
117
  "lint:legacy-name": "node scripts/lint-legacy-dir-name.cjs",
115
118
  "ci:test-scope": "node scripts/ci-test-scope.cjs",
116
- "size:baseline": "node scripts/update-size-baseline.cjs",
117
119
  "changeset": "node scripts/changeset/new.cjs",
118
120
  "changelog:render": "node scripts/changeset/cli.cjs render",
119
121
  "test": "node scripts/run-tests.cjs",
@@ -125,7 +127,7 @@
125
127
  "test:affected": "node scripts/run-affected-tests.cjs",
126
128
  "test:coverage": "c8 --check-coverage --lines 70 --branches 60 --reporter text --include 'gsd-core/bin/lib/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs",
127
129
  "test:coverage:scripts-floor": "c8 check-coverage --lines 55 --include 'scripts/**/*.cjs' --exclude 'tests/**' --all",
128
- "test:coverage:unit": "c8 --check-coverage --lines 70 --branches 60 --reporter text --include 'gsd-core/bin/lib/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs --suite unit && c8 check-coverage --per-file --lines 0 --functions 0 --statements 0 --branches 70 --include 'gsd-core/bin/lib/state.cjs' --include 'gsd-core/bin/lib/phase.cjs' --include 'gsd-core/bin/lib/verify.cjs' --include 'gsd-core/bin/lib/init.cjs' --exclude 'tests/**'",
130
+ "test:coverage:unit": "c8 --reporter text --reporter json-summary --include 'gsd-core/bin/lib/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs --suite unit && node scripts/check-coverage-gate.cjs",
129
131
  "test:coverage:all": "npm run test:coverage",
130
132
  "test:mutation": "stryker run",
131
133
  "test:mutation:since": "stryker run --incremental --since origin/next"
package/pi/gsd.cjs CHANGED
@@ -9,8 +9,14 @@
9
9
  * This extension binds GSD's command surface to pi via the imperative adapter
10
10
  * path — the programmatic-CLI peer of the OpenCode worked binding.
11
11
  *
12
- * Installation: copy this file to ~/.pi/agent/extensions/gsd.cjs (pi loads
13
- * extensions via jiti from that dir). The engine is resolved from the installed
12
+ * Installation: copy this file to ~/.pi/agent/extensions/gsd.js — note the
13
+ * `.js` DEST suffix, not `.cjs`. pi auto-discovers extensions/ entries through
14
+ * `isExtensionFile()`, which accepts only `.ts`/`.js` and skips anything else
15
+ * SILENTLY (no error, no log line), so a `.cjs` dest installs fine and is then
16
+ * never loaded (#2470). This source file keeps its `.cjs` suffix on purpose —
17
+ * tests `require()` it directly and `.cjs` is unambiguous CommonJS — and pi
18
+ * loads the copied file via jiti, which handles CommonJS and ESM alike, so the
19
+ * suffix gates discovery, not parsing. The engine is resolved from the installed
14
20
  * GSD tree (walk-up like the OpenCode plugin). pi's shared hooks/ bundle
15
21
  * (hooks/*.js + hooks/lib/git-cmd.js) is installed alongside the extension —
16
22
  * capabilities/pi/capability.json does NOT set
@@ -191,10 +197,35 @@ function buildBeforeProviderRequestHandler({ tier = 'sonnet' } = {}) {
191
197
  return async function onBeforeProviderRequest(event, ctx) {
192
198
  try {
193
199
  const effectiveCwd = (ctx && ctx.cwd) || process.cwd();
194
- const { resolveTierEntry } = require(path.join(GSD_CORE, 'bin', 'lib', 'model-resolver.cjs'));
195
200
  const { loadConfig } = require(path.join(GSD_CORE, 'bin', 'lib', 'config-loader.cjs'));
196
201
  const config = loadConfig(effectiveCwd);
197
202
  const overrides = (config && config.model_profile_overrides) || undefined;
203
+
204
+ // #2460: FAIL-OPEN when the user has not explicitly configured
205
+ // model_profile_overrides[runtime][tier]. pi is provider-agnostic
206
+ // (kimi-coding, zai, openrouter, openai-codex, minimax, anthropic, …);
207
+ // the built-in RUNTIME_PROFILE_MAP.pi.sonnet default is `claude-sonnet-5`,
208
+ // an Anthropic-ecosystem assumption that is wrong for every non-Anthropic
209
+ // provider. Returning the built-in default unconditionally rewrote every
210
+ // outgoing request to a model the active provider did not know.
211
+ //
212
+ // resolveTierEntry falls back to the built-in catalog when no override is
213
+ // present, so we cannot call it to detect "did the user opt in?" — we
214
+ // inspect the override map directly. Only when the user has explicitly
215
+ // set model_profile_overrides[runtime][tier] do we steer; otherwise we
216
+ // return undefined and pi's chosen model flows through untouched.
217
+ const userRaw = overrides && overrides.pi ? overrides.pi[tier] : undefined;
218
+ // `''` is treated the same as null/undefined: an explicit empty-string
219
+ // override is the user clearing a previously-set value, NOT opting in to
220
+ // steering. Without this guard, `resolveTierEntry`'s falsy `if (userRaw)`
221
+ // check would silently fall back to the built-in catalog and rewrite
222
+ // payload.model to claude-sonnet-5 — re-introducing the exact bug this
223
+ // handler exists to prevent.
224
+ if (userRaw === undefined || userRaw === null || userRaw === '') {
225
+ return undefined; // fail-open — leave pi's model untouched
226
+ }
227
+
228
+ const { resolveTierEntry } = require(path.join(GSD_CORE, 'bin', 'lib', 'model-resolver.cjs'));
198
229
  const entry = resolveTierEntry({ runtime: 'pi', tier, overrides });
199
230
  const modelId = entry && typeof entry.model === 'string' && entry.model.length > 0 ? entry.model : null;
200
231
  if (!modelId) return undefined; // fail-open — leave pi's model untouched
@@ -27,6 +27,7 @@ const OPT_OUT_LABEL = 'no-changelog';
27
27
  const USER_FACING_PREFIXES = [
28
28
  'bin/',
29
29
  'gsd-core/',
30
+ 'src/',
30
31
  'agents/',
31
32
  'commands/',
32
33
  'hooks/',
@@ -65,10 +65,23 @@ function extractDocsExempt(body) {
65
65
  // is CRLF-aware so Windows-authored fragments don't leave residual `\r`
66
66
  // characters that would shift the `(#NNNN)` PR suffix to a blank line in
67
67
  // the rendered CHANGELOG.md / GitHub release-notes bullet.
68
+ //
69
+ // Both leading AND trailing line terminators are stripped. `DOCS_EXEMPT_RE`
70
+ // removes the marker's own text but its `$` anchor (multiline mode) does
71
+ // not consume the `\n` that terminates the marker's line. When the marker
72
+ // is the FIRST line of the body, that leftover `\n` becomes the new first
73
+ // character of `body` — serializeChangelog then emits an empty `- ` bullet
74
+ // followed by an orphaned continuation paragraph, and parseChangelog's
75
+ // bullet-continuation check (which requires a leading `\s`) treats that
76
+ // non-indented paragraph as terminating the bullet, silently dropping the
77
+ // entry's content on re-parse. Stripping leading terminators here closes
78
+ // that gap the same way the trailing strip already does for the opposite
79
+ // (marker-last) position.
68
80
  const cleaned = body
69
81
  .replace(DOCS_EXEMPT_RE, '')
70
82
  .replace(/[ \t\r]+$/gm, '') // strip trailing \r/spaces on each line
71
83
  .replace(/(?:\r?\n){3,}/g, '\n\n') // collapse 3+ blank lines (CRLF-aware)
84
+ .replace(/^[\r\n]+/, '') // strip terminators left by a first-line marker
72
85
  .replace(/[\r\n]+$/, ''); // strip every trailing line terminator
73
86
  return { docsExempt: reason, body: cleaned };
74
87
  }
@@ -105,6 +118,19 @@ function parseFragment(src) {
105
118
  if (body.endsWith('\r\n')) verbatimBody = body.slice(0, -2);
106
119
  else if (body.endsWith('\n')) verbatimBody = body.slice(0, -1);
107
120
  else verbatimBody = body;
121
+ // Some fragments have a blank line between the closing frontmatter `---`
122
+ // and the first line of actual content (purely a stylistic authoring
123
+ // choice — the blank line carries no significant content, unlike
124
+ // indentation inside a code block). Strip any such leading blank line(s)
125
+ // here, mirroring the trailing-terminator strip above. Without this,
126
+ // `body` starts with `\n`/`\r\n`, serializeChangelog emits an empty `- `
127
+ // bullet followed by an orphaned paragraph, and parseChangelog's
128
+ // continuation check (requires a leading `\s` on the line) treats that
129
+ // non-indented paragraph as terminating the bullet — silently dropping
130
+ // the fragment's content on re-parse. This is the same downstream failure
131
+ // mode as a first-line docs-exempt marker (see extractDocsExempt below);
132
+ // it just arises from plain authoring whitespace instead of a marker.
133
+ verbatimBody = verbatimBody.replace(/^(?:[ \t]*\r?\n)+/, '');
108
134
  const { docsExempt, body: visibleBody } = extractDocsExempt(verbatimBody);
109
135
  if (!visibleBody.trim()) return { ok: false, reason: FRAGMENT_ERROR.EMPTY_BODY };
110
136
 
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const summaryPath = path.join(process.cwd(), 'coverage', 'coverage-summary.json');
8
+ if (!fs.existsSync(summaryPath)) {
9
+ console.error('check-coverage-gate: coverage/coverage-summary.json not found.');
10
+ console.error('Ensure c8 runs with --reporter json-summary before this script.');
11
+ process.exitCode = 1;
12
+ return;
13
+ }
14
+
15
+ const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
16
+ const errors = [];
17
+
18
+ const OVERALL_LINES = 70;
19
+ const OVERALL_BRANCHES = 60;
20
+ const PER_FILE_BRANCHES = 70;
21
+ const PER_FILE_FILES = [
22
+ 'gsd-core/bin/lib/state.cjs',
23
+ 'gsd-core/bin/lib/phase.cjs',
24
+ 'gsd-core/bin/lib/verify.cjs',
25
+ 'gsd-core/bin/lib/init.cjs',
26
+ ];
27
+
28
+ const overall = summary.total;
29
+ if (overall.lines.pct < OVERALL_LINES) {
30
+ errors.push(`Overall lines ${overall.lines.pct}% < ${OVERALL_LINES}%`);
31
+ }
32
+ if (overall.branches.pct < OVERALL_BRANCHES) {
33
+ errors.push(`Overall branches ${overall.branches.pct}% < ${OVERALL_BRANCHES}%`);
34
+ }
35
+
36
+ for (const file of PER_FILE_FILES) {
37
+ const key = Object.keys(summary).find((k) => k.endsWith(file));
38
+ if (!key) {
39
+ errors.push(`${file}: not found in coverage summary`);
40
+ continue;
41
+ }
42
+ const pct = summary[key].branches.pct;
43
+ if (pct < PER_FILE_BRANCHES) {
44
+ errors.push(`${file}: branches ${pct}% < ${PER_FILE_BRANCHES}%`);
45
+ }
46
+ }
47
+
48
+ if (errors.length > 0) {
49
+ for (const e of errors) console.error(`ERROR: ${e}`);
50
+ process.exitCode = 1;
51
+ }