@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
@@ -34,7 +34,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
34
34
  return (mod && mod.__esModule) ? mod : { "default": mod };
35
35
  };
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
- exports.LEGACY_NON_REGISTRY_RUNTIME_IDS = void 0;
37
+ exports.GSD_LOCATION_ENV_KEYS = exports.NON_REGISTRY_CONFIG_HOME_DESCRIPTORS = exports.KIMI_CODE_HOOKS_TOML_DESCRIPTOR = exports.KIMI_HOOKS_TOML_DESCRIPTOR = exports.LEGACY_NON_REGISTRY_RUNTIME_IDS = void 0;
38
38
  exports.isRegisteredRuntimeId = isRegisteredRuntimeId;
39
39
  exports.resolveConfigHomeFromDescriptor = resolveConfigHomeFromDescriptor;
40
40
  exports.resolveAntigravityGlobalDir = resolveAntigravityGlobalDir;
@@ -338,6 +338,72 @@ function resolveKimiGlobalDir(opts = {}) {
338
338
  probeExists: 'skills',
339
339
  }, { env, home, existsSync: existsSyncFn });
340
340
  }
341
+ /**
342
+ * Kimi CLI's own native config.toml home. Hoisted out of resolveKimiHooksTomlDir
343
+ * so it is ENUMERABLE, not merely resolvable.
344
+ *
345
+ * #2665 round 3: a config-location var that lives only inside a function body is
346
+ * invisible to every consumer that needs the SET rather than the path — the test
347
+ * scrub list and the hermeticity guard both derive from descriptors, and this one
348
+ * reached neither. `kimi` is the sharp case precisely because it owns TWO config
349
+ * homes: KIMI_CONFIG_DIR (registry-visible, already covered) and KIMI_SHARE_DIR
350
+ * (this one), so a derivation keyed only on the registry looks complete and is not.
351
+ */
352
+ exports.KIMI_HOOKS_TOML_DESCRIPTOR = {
353
+ kind: 'dot-home',
354
+ name: '.kimi',
355
+ env: ['KIMI_SHARE_DIR'],
356
+ };
357
+ /**
358
+ * Kimi Code's native config.toml home — the `kimi-code` counterpart of the
359
+ * descriptor above, hoisted for exactly the same reason.
360
+ *
361
+ * #2755 landed kimi-code hooks support on `next` while this PR was open, and
362
+ * declared this descriptor as an inline object literal inside
363
+ * resolveKimiHooksTomlDir's body — the same resolvable-but-not-enumerable shape
364
+ * round 3 hoisted KIMI_SHARE_DIR out of. Hoisting it puts `KIMI_CODE_HOME` into
365
+ * the derived scrub set and the hermeticity guard's watch roots in the SAME
366
+ * commit, which is the property NON_REGISTRY_CONFIG_HOME_DESCRIPTORS exists to
367
+ * guarantee. Each product's env var stays scoped to that product (#2755).
368
+ */
369
+ exports.KIMI_CODE_HOOKS_TOML_DESCRIPTOR = {
370
+ kind: 'dot-home',
371
+ name: '.kimi-code',
372
+ env: ['KIMI_CODE_HOME'],
373
+ };
374
+ /**
375
+ * Config-home descriptors resolved OUTSIDE the capability registry.
376
+ *
377
+ * Anything added here is picked up by every derived consumer in the same commit —
378
+ * which is the property that makes the derivation structurally incapable of being
379
+ * narrower than the surface it guards. Adding a hardcoded resolver WITHOUT adding
380
+ * its descriptor here is the defect this array exists to make hard.
381
+ */
382
+ exports.NON_REGISTRY_CONFIG_HOME_DESCRIPTORS = [
383
+ exports.KIMI_HOOKS_TOML_DESCRIPTOR,
384
+ exports.KIMI_CODE_HOOKS_TOML_DESCRIPTOR,
385
+ ];
386
+ /**
387
+ * GSD's OWN location vars — a second family, not runtime configHomes.
388
+ *
389
+ * #2665 round 3: the registry describes where each *third-party runtime* keeps its
390
+ * config. It says nothing about where GSD keeps its own user-owned state, and that
391
+ * is a separate env-first surface:
392
+ *
393
+ * GSD_HOME — `process.env['GSD_HOME'] || os.homedir()`, the root of
394
+ * `$GSD_HOME/.gsd/` (consent.json, defaults.json, capability
395
+ * overlays). Read env-first by capability-loader, capability-consent,
396
+ * capability-state, capability-writer, config-loader, install-profiles
397
+ * and bin/install.js. A WRITE surface.
398
+ * GSD_AGENTS_DIR — `if (process.env['GSD_AGENTS_DIR']) return it`, priority 1 in
399
+ * getAgentsDir. Misdirects a READ rather than a write, hence lower
400
+ * severity — but it is env-first and unconditional, so it belongs
401
+ * to the same class.
402
+ *
403
+ * Deliberately NOT folded into the descriptor array above: these do not resolve
404
+ * through resolveConfigHomeFromDescriptor and have no `kind`/`name` shape.
405
+ */
406
+ exports.GSD_LOCATION_ENV_KEYS = ['GSD_HOME', 'GSD_AGENTS_DIR'];
341
407
  /**
342
408
  * Resolve the directory holding the Kimi product's OWN native config.toml —
343
409
  * the file that product itself reads for providers/models/hooks/etc, and the
@@ -375,8 +441,8 @@ function resolveKimiHooksTomlDir(opts = {}) {
375
441
  // value originates from argv, and an index would resolve inherited keys
376
442
  // (`constructor`, `__proto__`) to something that is not a descriptor.
377
443
  const descriptor = opts.runtime === 'kimi-code'
378
- ? { kind: 'dot-home', name: '.kimi-code', env: ['KIMI_CODE_HOME'] }
379
- : { kind: 'dot-home', name: '.kimi', env: ['KIMI_SHARE_DIR'] };
444
+ ? exports.KIMI_CODE_HOOKS_TOML_DESCRIPTOR
445
+ : exports.KIMI_HOOKS_TOML_DESCRIPTOR;
380
446
  return resolveConfigHomeFromDescriptor(descriptor, { env, home });
381
447
  }
382
448
  /**
@@ -21,9 +21,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
21
21
  * BEHAVIOR-PRESERVING RELOCATION: all logic is copied verbatim from
22
22
  * bin/install.js. No behavior change, no descriptor reads, no new IO.
23
23
  *
24
- * bin/install.js re-exports every symbol from this module so existing
25
- * consumers that do require('../bin/install.js').writeCursorHooksJson
26
- * (etc.) continue to work unchanged.
24
+ * #2876 (epic #2866 Phase 7): bin/install.js previously re-exported every
25
+ * symbol from this module, but a repo-wide audit found zero production
26
+ * consumers of those re-exports — no `require('../bin/install.js').
27
+ * writeCursorHooksJson` (or any sibling) exists outside a doc comment
28
+ * anywhere in the tree. The re-exports were test-suite-only pass-throughs;
29
+ * tests now require this module directly instead.
27
30
  */
28
31
  const node_fs_1 = __importDefault(require("node:fs"));
29
32
  const node_path_1 = __importDefault(require("node:path"));
@@ -488,6 +491,98 @@ function rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts) {
488
491
  }
489
492
  return changed;
490
493
  }
494
+ // ---------------------------------------------------------------------------
495
+ // Shared: reconcileManagedShellHookCommands (#3329)
496
+ // ---------------------------------------------------------------------------
497
+ /**
498
+ * Rewrite already-registered managed `.sh` hook `command` strings to the shape
499
+ * the current installer would generate (#3329).
500
+ *
501
+ * applySettingsJsonHooks registers the four `.sh` managed hooks only-if-absent,
502
+ * so an entry registered by an older installer keeps its old command forever —
503
+ * `/gsd-update` (which re-invokes the installer) never re-derived it. On
504
+ * Claude/win32 that left the pre-#580/#3393 bash-runner-prefixed commands
505
+ * (`bash "<script>.sh"`, `"<git>/bash.exe" "<script>.sh"`) in place, spawning a
506
+ * nested bash on every hook fire. The #2979 rewriter above cannot help: it is
507
+ * Node-only by design (its basename gate contains only `.js` filenames).
508
+ *
509
+ * Scoping / safety:
510
+ * - Inert unless `shellHookOmitsBashRunner({ platform, runtime, isShellHook:
511
+ * true })` — the exact combination whose correct command shape changed. Where
512
+ * the bash runner is still correct (non-Windows, non-claude), nothing is
513
+ * rewritten, so the reconcile cannot churn unrelated installs.
514
+ * - Only entries whose parsed script token's basename exactly equals one of the
515
+ * expected managed `.sh` filenames are touched. A user hook would have to
516
+ * live at a path ending in exactly `gsd-session-state.sh` etc. — i.e. the
517
+ * GSD-installed file — to match. Extra-token commands (env prefixes, extra
518
+ * args) and args-form launcher entries (#976) never match the strict
519
+ * `[runner ]<script>` two-token shape and are left alone.
520
+ * - A null/empty expected command disables rewriting for that hook (a
521
+ * bash-runner-unavailable install must not have its entry nulled).
522
+ *
523
+ * @param settings settings.json-shaped object; mutated in place
524
+ * @param expected map of managed `.sh` filename → the command this install
525
+ * would register today (from buildHookCommand / buildLocalShellHookCommand)
526
+ * @param opts platform/runtime override (default: current process)
527
+ * @returns true when any command was rewritten
528
+ */
529
+ function reconcileManagedShellHookCommands(settings, expected, opts) {
530
+ if (!settings || !settings.hooks || !expected)
531
+ return false;
532
+ if (!opts)
533
+ opts = {};
534
+ const platform = opts.platform || process.platform;
535
+ const runtime = opts.runtime || 'generic';
536
+ if (!shellHookOmitsBashRunner({ platform, runtime, isShellHook: true }))
537
+ return false;
538
+ const expectedByBasename = new Map();
539
+ for (const [hookFile, command] of Object.entries(expected)) {
540
+ if (typeof command === 'string' && command.length > 0) {
541
+ expectedByBasename.set(hookFile, command);
542
+ }
543
+ }
544
+ if (expectedByBasename.size === 0)
545
+ return false;
546
+ let changed = false;
547
+ for (const entries of Object.values(settings.hooks)) {
548
+ if (!Array.isArray(entries))
549
+ continue;
550
+ for (const entry of entries) {
551
+ if (!entry || !Array.isArray(entry.hooks))
552
+ continue;
553
+ for (const h of entry.hooks) {
554
+ if (!h || typeof h.command !== 'string')
555
+ continue;
556
+ if (Array.isArray(h.args) && h.args.length > 0)
557
+ continue;
558
+ let trimmed = h.command.trim();
559
+ const hadPowerShellCallOperator = platform === 'win32' && /^&\s+/.test(trimmed);
560
+ if (hadPowerShellCallOperator) {
561
+ trimmed = trimmed.replace(/^&\s+/, '').trim();
562
+ }
563
+ // Strict `[runner ]<script>` shape: an optional single runner token
564
+ // (bare, 'single-quoted', or "double-quoted") followed by the script
565
+ // token. Anything else (env prefixes, extra flags, pipelines) does not
566
+ // match and is left untouched.
567
+ const m = trimmed.match(/^(?:(?:"([^"]+)"|'([^']+)'|(\S+))\s+)?(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/);
568
+ if (!m)
569
+ continue;
570
+ const scriptToken = m[4] || m[5] || m[6] || '';
571
+ if (!scriptToken)
572
+ continue;
573
+ const basename = shellCmdProjection.posixNormalize(scriptToken).split('/').pop() || '';
574
+ const expectedCommand = expectedByBasename.get(basename);
575
+ if (!expectedCommand)
576
+ continue;
577
+ if (h.command === expectedCommand)
578
+ continue;
579
+ h.command = expectedCommand;
580
+ changed = true;
581
+ }
582
+ }
583
+ }
584
+ return changed;
585
+ }
491
586
  function buildCodexHookBlock(targetDir, opts) {
492
587
  const absoluteRunner = opts && opts.absoluteRunner;
493
588
  if (!absoluteRunner)
@@ -1764,6 +1859,22 @@ function applySettingsJsonHooks(settings, opts) {
1764
1859
  else if (!hasPhaseBoundaryHook && !phaseBoundaryCommand) {
1765
1860
  console.warn(` ${yellow}⚠${reset} Skipped phase boundary hook — Bash executable path unavailable (#3393)`);
1766
1861
  }
1862
+ // #3329: the four `.sh` sites above register only-if-absent, so an entry
1863
+ // registered by an older installer keeps its old command forever —
1864
+ // /gsd-update (which re-invokes the installer) never re-derived it. On
1865
+ // Claude/win32 that left the pre-#580/#3393 bash-runner-prefixed commands
1866
+ // in settings.json indefinitely. Reconcile existing managed `.sh` entries
1867
+ // to the command this install would generate today. Inert wherever the
1868
+ // bash runner is still the correct shape; scoped to exact managed
1869
+ // basenames so user-authored hooks are never touched.
1870
+ if (reconcileManagedShellHookCommands(settings, {
1871
+ 'gsd-validate-commit.sh': validateCommitCommand,
1872
+ 'gsd-graphify-update.sh': graphifyUpdateCommand,
1873
+ 'gsd-session-state.sh': sessionStateCommand,
1874
+ 'gsd-phase-boundary.sh': phaseBoundaryCommand,
1875
+ }, { platform: hookOpts.platform, runtime })) {
1876
+ console.log(` ${green}✓${reset} Reconciled managed .sh hook commands to current format (#3329)`);
1877
+ }
1767
1878
  // ── Extended hook events: SubagentStop / Stop / PreCompact / SubagentStart
1768
1879
  // (#788 + #770 + #2092) ────────────────────────────────────────────────
1769
1880
  // Claude Code (since #770) and Qwen Code (since #788) both support the
@@ -2195,6 +2306,7 @@ module.exports = {
2195
2306
  applySettingsJsonHooks,
2196
2307
  referencesHook,
2197
2308
  rewriteLegacyManagedNodeHookCommands,
2309
+ reconcileManagedShellHookCommands,
2198
2310
  normalizeNodePath,
2199
2311
  resolveNodeRunner,
2200
2312
  resolveBashRunner,
@@ -193,7 +193,9 @@ exports.NO_LOCAL_CONFIG_DIR_SENTINEL = '(no-local-config-dir)';
193
193
  * Pure runtime-identity projection. Relocated from `bin/install.js` per
194
194
  * ADR-1508 (epic #1507, #1510 Phase 1) so the Runtime Artifact Conversion
195
195
  * Module's rewrite engine can consume it without importing the installer.
196
- * `bin/install.js` re-exports this same function for back-compat.
196
+ * Consumers import it from here. `bin/install.js` used to re-export it for
197
+ * back-compat; that re-export was retired in #2876, once an audit found the
198
+ * compatibility spine had no production consumer at all.
197
199
  */
198
200
  function getDirName(runtime) {
199
201
  if (!runtime)
@@ -29,6 +29,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
29
29
  };
30
30
  Object.defineProperty(exports, "__esModule", { value: true });
31
31
  exports.formatGsdSlash = formatGsdSlash;
32
+ exports.resolveExplicitRuntime = resolveExplicitRuntime;
32
33
  exports.resolveRuntime = resolveRuntime;
33
34
  exports.formatGsdSlashFor = formatGsdSlashFor;
34
35
  const node_fs_1 = __importDefault(require("node:fs"));
@@ -75,19 +76,21 @@ function formatGsdSlash(commandName, runtime) {
75
76
  return `/gsd-${token}${tail}`;
76
77
  }
77
78
  /**
78
- * Resolve the effective runtime for a project directory.
79
+ * Resolve the explicit runtime for a project directory, from the two
80
+ * explicit sources only — no default is applied.
79
81
  *
80
- * process.env.GSD_RUNTIME > config.runtime > 'claude'
82
+ * env.GSD_RUNTIME > config.runtime > null
81
83
  *
82
- * Mirrors the precedence already used by profile-output.cjs and the rest of
83
- * the runtime resolution chain. Returns a lowercased string so downstream
84
- * comparisons can be case-blind.
84
+ * Returns null when neither explicit source is set, so a caller can
85
+ * distinguish "config said claude" from "nothing was set" (needed by
86
+ * #3245's host-detection rung, which must only run when this returns null).
85
87
  *
86
88
  * @param projectDir - path to the project directory, or null/undefined
87
- * @returns the resolved runtime name
89
+ * @param env - environment variables to read GSD_RUNTIME from; defaults to process.env
90
+ * @returns the resolved runtime name, or null if neither source is set
88
91
  */
89
- function resolveRuntime(projectDir) {
90
- const envRuntime = (0, runtime_name_policy_cjs_1.resolveRuntimeNameFromCandidates)(process.env['GSD_RUNTIME']);
92
+ function resolveExplicitRuntime(projectDir, env = process.env) {
93
+ const envRuntime = (0, runtime_name_policy_cjs_1.resolveRuntimeNameFromCandidates)(env['GSD_RUNTIME']);
91
94
  if (envRuntime)
92
95
  return envRuntime;
93
96
  if (projectDir) {
@@ -113,7 +116,22 @@ function resolveRuntime(projectDir) {
113
116
  // runtime output formatting.
114
117
  }
115
118
  }
116
- return 'claude';
119
+ return null;
120
+ }
121
+ /**
122
+ * Resolve the effective runtime for a project directory.
123
+ *
124
+ * process.env.GSD_RUNTIME > config.runtime > 'claude'
125
+ *
126
+ * Mirrors the precedence already used by profile-output.cjs and the rest of
127
+ * the runtime resolution chain. Returns a lowercased string so downstream
128
+ * comparisons can be case-blind.
129
+ *
130
+ * @param projectDir - path to the project directory, or null/undefined
131
+ * @returns the resolved runtime name
132
+ */
133
+ function resolveRuntime(projectDir) {
134
+ return resolveExplicitRuntime(projectDir) ?? 'claude';
117
135
  }
118
136
  /**
119
137
  * Convenience: format using the runtime resolved from a project directory.
@@ -29,6 +29,7 @@ exports.requireSafePath = requireSafePath;
29
29
  exports.scanForInjection = scanForInjection;
30
30
  exports.sanitizeForPrompt = sanitizeForPrompt;
31
31
  exports.sanitizeForDisplay = sanitizeForDisplay;
32
+ exports.sanitizeLabel = sanitizeLabel;
32
33
  exports.validateShellArg = validateShellArg;
33
34
  exports.safeJsonParse = safeJsonParse;
34
35
  exports.validatePhaseNumber = validatePhaseNumber;
@@ -73,13 +74,52 @@ function validatePath(filePath, baseDir, opts = {}) {
73
74
  resolvedPath = node_fs_1.default.realpathSync(resolvedPath);
74
75
  }
75
76
  catch {
76
- const parentDir = node_path_1.default.dirname(resolvedPath);
77
+ // realpathSync failed — either resolvedPath doesn't exist at all, or it's
78
+ // a dangling symlink (the link itself exists but its target doesn't).
79
+ // lstat (unlike stat/realpath) stats the link itself and does NOT follow
80
+ // it, so it succeeds for a dangling symlink and throws ENOENT for a
81
+ // genuinely absent path. That's the discriminator: without it, a dangling
82
+ // symlink to a non-existent OUTSIDE path would fall through to the
83
+ // parent-resolution fallback below and be re-accepted as an in-project
84
+ // path, while a symlink to an EXISTING outside path is correctly
85
+ // rejected via the realpathSync success branch above — a state
86
+ // difference an attacker can use as an existence oracle for arbitrary
87
+ // absolute paths.
77
88
  try {
78
- const realParent = node_fs_1.default.realpathSync(parentDir);
79
- resolvedPath = node_path_1.default.join(realParent, node_path_1.default.basename(resolvedPath));
89
+ if (node_fs_1.default.lstatSync(resolvedPath).isSymbolicLink()) {
90
+ return { safe: false, resolved: '', error: 'Path is an unresolvable symbolic link' };
91
+ }
80
92
  }
81
93
  catch {
82
- // Parent doesn't exist either — keep the resolved path as-is
94
+ // lstat also threw — resolvedPath (and its would-be link) genuinely
95
+ // doesn't exist. Fall through to ancestor resolution below.
96
+ }
97
+ // Walk up to the nearest ancestor that exists and realpath THAT, then
98
+ // re-append the remaining (not-yet-created) segments. This canonicalizes
99
+ // resolvedPath the same way resolvedBase was canonicalized above,
100
+ // regardless of how many leading directories are missing — a single
101
+ // parent-only check would leave resolvedPath un-canonicalized whenever
102
+ // the parent is also missing, which breaks the startsWith comparison
103
+ // below on any non-canonical cwd (e.g. macOS /var/... vs
104
+ // /private/var/...).
105
+ let ancestor = node_path_1.default.dirname(resolvedPath);
106
+ const remainder = [node_path_1.default.basename(resolvedPath)];
107
+ for (;;) {
108
+ try {
109
+ const realAncestor = node_fs_1.default.realpathSync(ancestor);
110
+ resolvedPath = node_path_1.default.join(realAncestor, ...remainder);
111
+ break;
112
+ }
113
+ catch {
114
+ const parent = node_path_1.default.dirname(ancestor);
115
+ if (parent === ancestor) {
116
+ // Reached filesystem root without finding an existing ancestor —
117
+ // keep resolvedPath as-is.
118
+ break;
119
+ }
120
+ remainder.unshift(node_path_1.default.basename(ancestor));
121
+ ancestor = parent;
122
+ }
83
123
  }
84
124
  }
85
125
  const normalizedBase = resolvedBase + node_path_1.default.sep;
@@ -193,7 +233,7 @@ exports.INJECTION_PATTERNS = [
193
233
  /override\s+(system|previous)\s+(prompt|instructions)/i,
194
234
  // Role/identity manipulation
195
235
  /you\s+are\s+now\s+(?:a|an|the)\s+/i,
196
- /act\s+as\s+(?:a|an|the)\s+(?!plan|phase|wave)/i,
236
+ /\bact\s+as\s+(?:a|an|the)\s+(?!plan|phase|wave)/i,
197
237
  /pretend\s+(?:you(?:'re| are)\s+|to\s+be\s+)/i,
198
238
  /from\s+now\s+on,?\s+you\s+(?:are|will|should|must)/i,
199
239
  // System prompt extraction
@@ -349,6 +389,65 @@ function sanitizeForDisplay(text) {
349
389
  .join('\n');
350
390
  return sanitized;
351
391
  }
392
+ /**
393
+ * Sanitize a value that must render as a SINGLE LINE and is derived from a
394
+ * filesystem name (a phase directory's number/name token, an archived
395
+ * milestone label, a bare filename) — not from file/frontmatter CONTENT.
396
+ *
397
+ * Why this is NOT `sanitizeForDisplay`: that helper's job is multi-line
398
+ * prose — it strips whole protocol-leak LINES while deliberately preserving
399
+ * `\n` between legitimate ones (see its docstring and
400
+ * `tests/security.test.cjs`'s neighbouring describe). A filesystem name is
401
+ * the opposite shape: it is supposed to be one line, so a `\n`/`\r` inside
402
+ * one is never legitimate content to preserve — it is an attacker (or a
403
+ * doctored checkout) using the directory NAME itself as the injection
404
+ * vector. #3458's reproduction: a phase directory literally named
405
+ * `zz\n0 open items require decisions.\n\x1b[2K\x1b[1G FORGED`
406
+ * flows verbatim into `audit-open`'s human report (the phase-number
407
+ * fallback taken when the name doesn't match `PHASE_NUMBER_TOKEN_SOURCE`).
408
+ * `sanitizeForDisplay` would pass every one of those bytes straight through
409
+ * — by design, since it never touches control characters — so the embedded
410
+ * `\n` becomes a real newline in the report, printing a forged
411
+ * "0 open items require decisions." as its own line, and the raw ESC bytes
412
+ * reach the terminal.
413
+ *
414
+ * This helper closes that hole by ESCAPING (never silently stripping) the
415
+ * C0 control range (0x00–0x1F, including ESC 0x1B, CR, LF), DEL (0x7F), and
416
+ * the C1 range (0x80–0x9F) into a visible representation (`\n`, `\x1b`,
417
+ * ...). Escaping rather than stripping is deliberate: a reviewer reading the
418
+ * report should be able to SEE that a name was doctored, not have it quietly
419
+ * normalized away as if nothing happened. Every other character — including
420
+ * all ordinary printable and non-ASCII text — passes through byte-identical.
421
+ */
422
+ function sanitizeLabel(text) {
423
+ if (!text || typeof text !== 'string')
424
+ return text;
425
+ const NAMED_ESCAPES = {
426
+ 0x00: '\\0',
427
+ 0x07: '\\a',
428
+ 0x08: '\\b',
429
+ 0x09: '\\t',
430
+ 0x0a: '\\n',
431
+ 0x0b: '\\v',
432
+ 0x0c: '\\f',
433
+ 0x0d: '\\r',
434
+ 0x1b: '\\x1b',
435
+ };
436
+ let out = '';
437
+ for (const ch of text) {
438
+ const code = ch.codePointAt(0);
439
+ const isC0 = code <= 0x1f;
440
+ const isDel = code === 0x7f;
441
+ const isC1 = code >= 0x80 && code <= 0x9f;
442
+ if (isC0 || isDel || isC1) {
443
+ out += NAMED_ESCAPES[code] ?? `\\x${code.toString(16).padStart(2, '0')}`;
444
+ }
445
+ else {
446
+ out += ch;
447
+ }
448
+ }
449
+ return out;
450
+ }
352
451
  // ─── Shell Safety ───────────────────────────────────────────────────────────────────────
353
452
  /**
354
453
  * Validate that a string is safe to use as a shell argument when quoted.