@opengsd/gsd-core 1.9.1 → 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 (426) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/.opencode/plugins/gsd-core.js +8 -1
  4. package/agents/gsd-code-fixer.md +27 -3
  5. package/agents/gsd-debug-session-manager.md +11 -0
  6. package/agents/gsd-debugger.md +12 -246
  7. package/agents/gsd-doc-synthesizer.md +2 -4
  8. package/agents/gsd-executor.md +12 -10
  9. package/agents/gsd-integration-checker.md +3 -0
  10. package/agents/gsd-mempalace-curator.md +5 -2
  11. package/agents/gsd-phase-researcher.md +20 -1
  12. package/agents/gsd-plan-checker.md +46 -0
  13. package/agents/gsd-planner.md +49 -54
  14. package/agents/gsd-roadmapper.md +21 -3
  15. package/agents/gsd-user-profiler.md +3 -0
  16. package/agents/gsd-verifier.md +26 -73
  17. package/bin/install.js +1272 -1238
  18. package/bin/lib/ui-safety-gate.cjs +2 -0
  19. package/commands/gsd/code-review.md +1 -1
  20. package/commands/gsd/execute-phase.md +1 -1
  21. package/commands/gsd/map-codebase.md +1 -1
  22. package/commands/gsd/mempalace-capture.md +2 -2
  23. package/commands/gsd/mempalace-recall.md +1 -1
  24. package/commands/gsd/new-milestone.md +2 -2
  25. package/commands/gsd/plan-phase.md +1 -1
  26. package/commands/gsd/quick.md +1 -1
  27. package/commands/gsd/review-backlog.md +2 -1
  28. package/commands/gsd/verify-work.md +1 -1
  29. package/gsd-core/bin/gsd-tools.cjs +1009 -115
  30. package/gsd-core/bin/lib/active-workstream-store.cjs +153 -12
  31. package/gsd-core/bin/lib/agent-install-check.cjs +268 -38
  32. package/gsd-core/bin/lib/api-coverage.cjs +123 -5
  33. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  34. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  35. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  36. package/gsd-core/bin/lib/audit.cjs +926 -202
  37. package/gsd-core/bin/lib/broken-windows.cjs +36 -6
  38. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  39. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  40. package/gsd-core/bin/lib/capability-registry.cjs +608 -148
  41. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  42. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  43. package/gsd-core/bin/lib/capability-validator.cjs +507 -24
  44. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  45. package/gsd-core/bin/lib/check-command-router.cjs +114 -38
  46. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  47. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  48. package/gsd-core/bin/lib/command-aliases.cjs +94 -0
  49. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  50. package/gsd-core/bin/lib/commands.cjs +665 -99
  51. package/gsd-core/bin/lib/commonjs-marker.cjs +142 -0
  52. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  53. package/gsd-core/bin/lib/config-loader.cjs +76 -0
  54. package/gsd-core/bin/lib/config.cjs +22 -2
  55. package/gsd-core/bin/lib/context-composer.cjs +278 -0
  56. package/gsd-core/bin/lib/context-predicates.cjs +506 -0
  57. package/gsd-core/bin/lib/core-utils.cjs +217 -40
  58. package/gsd-core/bin/lib/decisions.cjs +23 -0
  59. package/gsd-core/bin/lib/docs.cjs +3 -2
  60. package/gsd-core/bin/lib/external-job.cjs +19 -4
  61. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  62. package/gsd-core/bin/lib/frontmatter.cjs +239 -32
  63. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  64. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
  65. package/gsd-core/bin/lib/git-base-branch.cjs +160 -15
  66. package/gsd-core/bin/lib/graphify.cjs +142 -27
  67. package/gsd-core/bin/lib/gsd2-import.cjs +37 -5
  68. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  69. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  70. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  71. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  72. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  73. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  74. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  75. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  76. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  77. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  78. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  79. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  80. package/gsd-core/bin/lib/host-integration.cjs +13 -1
  81. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  82. package/gsd-core/bin/lib/init-command-router.cjs +83 -8
  83. package/gsd-core/bin/lib/init.cjs +1325 -169
  84. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  85. package/gsd-core/bin/lib/install-engine.cjs +805 -264
  86. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  87. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  88. package/gsd-core/bin/lib/install-profiles.cjs +160 -57
  89. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  90. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  91. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  92. package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
  93. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  94. package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
  95. package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
  96. package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
  97. package/gsd-core/bin/lib/installer-migrations.cjs +206 -13
  98. package/gsd-core/bin/lib/io.cjs +38 -3
  99. package/gsd-core/bin/lib/markdown-sectionizer.cjs +8 -1
  100. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  101. package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
  102. package/gsd-core/bin/lib/mcp-server.cjs +135 -3
  103. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  104. package/gsd-core/bin/lib/milestone.cjs +821 -109
  105. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  106. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  107. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  108. package/gsd-core/bin/lib/pattern.cjs +122 -0
  109. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  110. package/gsd-core/bin/lib/phase-id.cjs +507 -36
  111. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  112. package/gsd-core/bin/lib/phase-locator.cjs +258 -58
  113. package/gsd-core/bin/lib/phase.cjs +891 -156
  114. package/gsd-core/bin/lib/plan-dependency-graph.cjs +303 -0
  115. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  116. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  117. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  118. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  119. package/gsd-core/bin/lib/planning-workspace.cjs +60 -6
  120. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  121. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  122. package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
  123. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  124. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +85 -0
  125. package/gsd-core/bin/lib/review-lane-descriptor.cjs +108 -0
  126. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  127. package/gsd-core/bin/lib/review-lane-runner.cjs +447 -68
  128. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  129. package/gsd-core/bin/lib/roadmap-command-router.cjs +76 -9
  130. package/gsd-core/bin/lib/roadmap-parser.cjs +1035 -194
  131. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  132. package/gsd-core/bin/lib/roadmap.cjs +405 -84
  133. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +795 -100
  134. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  135. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +440 -57
  136. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  137. package/gsd-core/bin/lib/runtime-homes.cjs +220 -41
  138. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +220 -44
  139. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  140. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  141. package/gsd-core/bin/lib/section-manifest.cjs +209 -0
  142. package/gsd-core/bin/lib/security.cjs +104 -5
  143. package/gsd-core/bin/lib/shell-command-projection.cjs +388 -30
  144. package/gsd-core/bin/lib/smart-entry.cjs +154 -22
  145. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  146. package/gsd-core/bin/lib/state-document.cjs +152 -8
  147. package/gsd-core/bin/lib/state-transition.cjs +424 -105
  148. package/gsd-core/bin/lib/state.cjs +1927 -401
  149. package/gsd-core/bin/lib/surface.cjs +35 -10
  150. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  151. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  152. package/gsd-core/bin/lib/uat-predicate.cjs +20 -4
  153. package/gsd-core/bin/lib/uat.cjs +706 -64
  154. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  155. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  156. package/gsd-core/bin/lib/unusable-input.cjs +33 -0
  157. package/gsd-core/bin/lib/update-context.cjs +8 -2
  158. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  159. package/gsd-core/bin/lib/validate.cjs +20 -6
  160. package/gsd-core/bin/lib/vendor/README.md +37 -0
  161. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  162. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  163. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  164. package/gsd-core/bin/lib/verification.cjs +287 -20
  165. package/gsd-core/bin/lib/verify.cjs +368 -880
  166. package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
  167. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +203 -19
  168. package/gsd-core/bin/lib/workstream-inventory.cjs +576 -31
  169. package/gsd-core/bin/lib/workstream.cjs +8 -2
  170. package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
  171. package/gsd-core/bin/lib/worktree-safety.cjs +450 -125
  172. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  173. package/gsd-core/bin/shared/config-schema.manifest.json +9 -1
  174. package/gsd-core/references/agent-contracts.md +43 -26
  175. package/gsd-core/references/artifact-types.md +10 -3
  176. package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
  177. package/gsd-core/references/checkpoints.md +2 -2
  178. package/gsd-core/references/context-budget.md +1 -1
  179. package/gsd-core/references/debugger-techniques.md +255 -0
  180. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  181. package/gsd-core/references/doc-conflict-engine.md +1 -1
  182. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  183. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  184. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  185. package/gsd-core/references/execute-phase-response-language.md +1 -1
  186. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  187. package/gsd-core/references/gate-prompts.md +1 -1
  188. package/gsd-core/references/git-planning-commit.md +2 -1
  189. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  190. package/gsd-core/references/model-profiles.md +12 -4
  191. package/gsd-core/references/mvp-concepts.md +9 -9
  192. package/gsd-core/references/planner-guidance.md +3 -9
  193. package/gsd-core/references/planner-preconditions.md +1 -1
  194. package/gsd-core/references/planner-reviews.md +1 -1
  195. package/gsd-core/references/planning-config.md +8 -6
  196. package/gsd-core/references/research-documentation-lookup.md +5 -3
  197. package/gsd-core/references/revision-loop.md +1 -1
  198. package/gsd-core/references/specless-probe-fallback.md +8 -7
  199. package/gsd-core/references/universal-anti-patterns.md +3 -3
  200. package/gsd-core/references/verifier-phase-gates.md +192 -0
  201. package/gsd-core/references/verifier-wiring-patterns.md +100 -0
  202. package/gsd-core/references/verify-mvp-mode.md +1 -1
  203. package/gsd-core/references/workstream-flag.md +22 -6
  204. package/gsd-core/references/worktree-branch-check.md +2 -2
  205. package/gsd-core/templates/discussion-log.md +1 -1
  206. package/gsd-core/templates/phase-prompt.md +2 -4
  207. package/gsd-core/templates/state.md +4 -4
  208. package/gsd-core/templates/summary-complex.md +2 -0
  209. package/gsd-core/templates/summary-minimal.md +2 -0
  210. package/gsd-core/templates/summary-standard.md +2 -0
  211. package/gsd-core/templates/summary.md +2 -0
  212. package/gsd-core/templates/verification-report.md +9 -1
  213. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  214. package/gsd-core/workflows/audit-milestone.md +3 -0
  215. package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
  216. package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
  217. package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
  218. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
  219. package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
  220. package/gsd-core/workflows/autonomous.md +33 -70
  221. package/gsd-core/workflows/cleanup.md +62 -3
  222. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
  223. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +93 -0
  224. package/gsd-core/workflows/code-review-fix.md +37 -10
  225. package/gsd-core/workflows/code-review.md +74 -166
  226. package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
  227. package/gsd-core/workflows/complete-milestone.md +160 -95
  228. package/gsd-core/workflows/debug.md +16 -17
  229. package/gsd-core/workflows/diagnose-issues.md +56 -8
  230. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  231. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  232. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
  233. package/gsd-core/workflows/discuss-phase-assumptions.md +7 -17
  234. package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
  235. package/gsd-core/workflows/docs-update.md +8 -51
  236. package/gsd-core/workflows/edit-phase.md +26 -1
  237. package/gsd-core/workflows/eval-review.md +3 -5
  238. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +64 -7
  239. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
  240. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
  241. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  242. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +21 -0
  243. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
  244. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
  245. package/gsd-core/workflows/execute-phase.md +103 -187
  246. package/gsd-core/workflows/execute-plan.md +36 -4
  247. package/gsd-core/workflows/explore.md +131 -4
  248. package/gsd-core/workflows/fast.md +10 -2
  249. package/gsd-core/workflows/health.md +73 -4
  250. package/gsd-core/workflows/help/modes/full.md +6 -1
  251. package/gsd-core/workflows/import.md +4 -4
  252. package/gsd-core/workflows/ingest-docs.md +7 -6
  253. package/gsd-core/workflows/mvp-phase.md +6 -3
  254. package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
  255. package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
  256. package/gsd-core/workflows/new-milestone.md +35 -47
  257. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
  258. package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
  259. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
  260. package/gsd-core/workflows/new-project.md +27 -240
  261. package/gsd-core/workflows/next.md +12 -0
  262. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
  263. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
  264. package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
  265. package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
  266. package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
  267. package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
  268. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
  269. package/gsd-core/workflows/plan-phase.md +89 -209
  270. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  271. package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
  272. package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
  273. package/gsd-core/workflows/progress.md +45 -159
  274. package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
  275. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
  276. package/gsd-core/workflows/quick/steps/quick-verification.md +67 -0
  277. package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
  278. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
  279. package/gsd-core/workflows/quick.md +55 -405
  280. package/gsd-core/workflows/resume-project.md +3 -0
  281. package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
  282. package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
  283. package/gsd-core/workflows/review.md +41 -13
  284. package/gsd-core/workflows/section-manifest.json +219 -0
  285. package/gsd-core/workflows/secure-phase.md +1 -1
  286. package/gsd-core/workflows/session-report.md +2 -1
  287. package/gsd-core/workflows/settings.md +66 -2
  288. package/gsd-core/workflows/ship.md +104 -44
  289. package/gsd-core/workflows/sketch.md +1 -1
  290. package/gsd-core/workflows/spec-phase.md +41 -20
  291. package/gsd-core/workflows/spike-wrap-up.md +20 -5
  292. package/gsd-core/workflows/spike.md +50 -16
  293. package/gsd-core/workflows/sync-skills.md +106 -13
  294. package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
  295. package/gsd-core/workflows/transition.md +53 -31
  296. package/gsd-core/workflows/ui-phase.md +13 -12
  297. package/gsd-core/workflows/ui-review.md +2 -2
  298. package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
  299. package/gsd-core/workflows/update.md +19 -8
  300. package/gsd-core/workflows/validate-phase.md +1 -1
  301. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
  302. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
  303. package/gsd-core/workflows/verify-work.md +17 -65
  304. package/hooks/dist/gsd-agent-isolation-guard.js +517 -0
  305. package/hooks/dist/gsd-check-update-worker.js +64 -12
  306. package/hooks/dist/gsd-check-update.js +19 -1
  307. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  308. package/hooks/dist/gsd-cursor-subagent-start.js +607 -26
  309. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  310. package/hooks/dist/gsd-prompt-guard.js +21 -20
  311. package/hooks/dist/gsd-read-injection-scanner.js +45 -24
  312. package/hooks/dist/gsd-statusline.js +90 -6
  313. package/hooks/dist/gsd-update-banner.js +22 -1
  314. package/hooks/dist/gsd-workflow-guard.js +134 -36
  315. package/hooks/dist/gsd-worktree-path-guard.js +2 -1
  316. package/hooks/dist/gsd-write-guard.js +359 -0
  317. package/hooks/dist/lib/git-cmd.js +92 -59
  318. package/hooks/dist/lib/injection-patterns.js +45 -0
  319. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  320. package/hooks/dist/lib/isolation-sentinel.js +277 -0
  321. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  322. package/hooks/gsd-agent-isolation-guard.js +517 -0
  323. package/hooks/gsd-check-update-worker.js +64 -12
  324. package/hooks/gsd-check-update.js +19 -1
  325. package/hooks/gsd-cursor-pre-tool.js +0 -3
  326. package/hooks/gsd-cursor-subagent-start.js +607 -26
  327. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  328. package/hooks/gsd-prompt-guard.js +21 -20
  329. package/hooks/gsd-read-injection-scanner.js +45 -24
  330. package/hooks/gsd-statusline.js +90 -6
  331. package/hooks/gsd-update-banner.js +22 -1
  332. package/hooks/gsd-workflow-guard.js +134 -36
  333. package/hooks/gsd-worktree-path-guard.js +2 -1
  334. package/hooks/gsd-write-guard.js +359 -0
  335. package/hooks/hooks.json +12 -0
  336. package/hooks/lib/git-cmd.js +92 -59
  337. package/hooks/lib/injection-patterns.js +45 -0
  338. package/hooks/lib/isolation-deny-reason.js +39 -0
  339. package/hooks/lib/isolation-sentinel.js +277 -0
  340. package/hooks/managed-hooks-registry.cjs +2 -0
  341. package/package.json +31 -10
  342. package/pi/gsd.cjs +71 -12
  343. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  344. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  345. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  346. package/scripts/build-hooks.js +9 -0
  347. package/scripts/changeset/lint.cjs +68 -6
  348. package/scripts/changeset/serialize.cjs +5 -1
  349. package/scripts/check-alias-drift.cjs +7 -43
  350. package/scripts/check-contract-drift.cjs +297 -0
  351. package/scripts/ci-test-scope.cjs +19 -2
  352. package/scripts/command-contract-helpers.cjs +903 -1
  353. package/scripts/gen-adr-index.cjs +728 -38
  354. package/scripts/gen-capability-matrix.cjs +1 -1
  355. package/scripts/gen-capability-registry.cjs +3 -15
  356. package/scripts/gen-context-index.cjs +439 -0
  357. package/scripts/gen-health-docs.cjs +390 -0
  358. package/scripts/gen-inventory-manifest.cjs +150 -4
  359. package/scripts/gen-loop-host-contract.cjs +4 -24
  360. package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
  361. package/scripts/gen-registry.cjs +3 -14
  362. package/scripts/gen-section-manifest.cjs +638 -0
  363. package/scripts/generate-package-identity.cjs +4 -2
  364. package/scripts/lib/alias-drift-families.cjs +46 -0
  365. package/scripts/lib/drift-scan.cjs +278 -0
  366. package/scripts/lint-allow-test-rule-refs.allowlist.json +15 -54
  367. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  368. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  369. package/scripts/lint-canary-version-leak.cjs +73 -0
  370. package/scripts/lint-command-contract.cjs +96 -13
  371. package/scripts/lint-compiled-artifact-sync.cjs +6 -1
  372. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  373. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  374. package/scripts/lint-default-flip-documentation.cjs +193 -0
  375. package/scripts/lint-docs-command-form.cjs +195 -0
  376. package/scripts/lint-docs-required.cjs +9 -1
  377. package/scripts/lint-emitted-drift-ack.cjs +215 -20
  378. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  379. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  380. package/scripts/lint-example-parser-parity.cjs +395 -0
  381. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  382. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  383. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  384. package/scripts/lint-milestone-window-drift.cjs +468 -0
  385. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  386. package/scripts/lint-plan-count-drift.cjs +318 -0
  387. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  388. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  389. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  390. package/scripts/lint-regression-test-names.cjs +15 -13
  391. package/scripts/lint-removed-but-needed.cjs +320 -0
  392. package/scripts/lint-state-field-drift.cjs +805 -0
  393. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  394. package/scripts/lint-test-file-count.allowlist.json +40 -3
  395. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  396. package/scripts/lint-vendored-deps.cjs +124 -0
  397. package/scripts/mutation-matrix.cjs +13 -0
  398. package/scripts/pr-changed-files.cjs +63 -0
  399. package/scripts/pr-template-policy.cjs +14 -4
  400. package/scripts/prompt-injection-scan.sh +52 -6
  401. package/scripts/require-issue-link-policy.cjs +192 -0
  402. package/scripts/state-write-path-drift-baseline.json +19 -0
  403. package/scripts/sync-runtime-launcher.cjs +2 -4
  404. package/skills/gsd-autonomous/SKILL.md +0 -1
  405. package/skills/gsd-code-review/SKILL.md +1 -1
  406. package/skills/gsd-execute-phase/SKILL.md +1 -2
  407. package/skills/gsd-map-codebase/SKILL.md +1 -1
  408. package/skills/gsd-mempalace-capture/SKILL.md +2 -2
  409. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  410. package/skills/gsd-new-milestone/SKILL.md +2 -2
  411. package/skills/gsd-next/SKILL.md +0 -1
  412. package/skills/gsd-plan-phase/SKILL.md +1 -2
  413. package/skills/gsd-progress/SKILL.md +0 -1
  414. package/skills/gsd-quick/SKILL.md +1 -1
  415. package/skills/gsd-review-backlog/SKILL.md +2 -1
  416. package/skills/gsd-stats/SKILL.md +0 -1
  417. package/skills/gsd-verify-work/SKILL.md +1 -1
  418. package/vscode/package.json +1 -1
  419. package/gsd-core/workflows/discovery-phase.md +0 -298
  420. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  421. package/gsd-core/workflows/verify-phase.md +0 -577
  422. package/scripts/affected-tests-lib.cjs +0 -554
  423. package/scripts/gen-emitted-baseline.cjs +0 -145
  424. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  425. package/scripts/run-affected-tests.cjs +0 -7
  426. package/scripts/run-tests.cjs +0 -1050
@@ -1,11 +1,11 @@
1
1
  "use strict";
2
2
  /**
3
- * Broken-windows ledger — enforced cross-phase defect register (issue #1950).
3
+ * Broken-windows ledger — optionally enforced cross-phase defect register (issue #1950).
4
4
  *
5
5
  * Manages `.planning/WINDOWS.md`: a cross-phase ledger of small defects (stubs,
6
6
  * TODOs, skipped tests, lint warnings, unrun verifies, unmet truths, deviations).
7
- * `/gsd-ship` blocks while any entry is `open`; an entry can be `waived` only
8
- * with a recorded reason or `fixed`.
7
+ * When `workflow.windows_enforce` is true, `/gsd-ship` blocks while any entry is
8
+ * `open`; an entry can be `waived` only with a recorded reason or `fixed`.
9
9
  *
10
10
  * LEAF MODULE — imports ONLY: node:fs, node:path. No other src/ imports.
11
11
  *
@@ -302,7 +302,12 @@ function parseFrontmatterStrict(raw) {
302
302
  }
303
303
  const yamlBody = raw.slice(headerEnd, closeIdx);
304
304
  const out = {};
305
- for (const line of yamlBody.split(/\r?\n/)) {
305
+ for (const rawLine of yamlBody.split(/\r?\n/)) {
306
+ // #3116: the `\n---` scan leaves the final line's CR attached on a CRLF
307
+ // ledger, and `.` never matches CR, so the key: value regex below fails on
308
+ // it. Strip the trailing CR per line so the rest of `raw` (which
309
+ // parseJsonBlock also slices by byte offset) is unaffected.
310
+ const line = rawLine.replace(/\r$/, '');
306
311
  if (line.trim() === '')
307
312
  continue;
308
313
  const m = line.match(/^([a-zA-Z0-9_]+):\s*(.*)$/);
@@ -447,7 +452,7 @@ function renderLedger(ledger) {
447
452
  const header = [
448
453
  '# Broken Windows Ledger',
449
454
  '',
450
- '> Cross-phase defect register. `/gsd-ship` blocks while `open_count > 0`.',
455
+ '> Cross-phase defect register. With `workflow.windows_enforce` enabled, `/gsd-ship` blocks while `open_count > 0`.',
451
456
  '> Waive with `gsd-tools windows waive <id> "<reason>"` (reason required).',
452
457
  '> Mark fixed with `gsd-tools windows fixed <id>`.',
453
458
  '',
@@ -559,7 +564,32 @@ function writeLedgerAtomic(cwd, ledger) {
559
564
  ensurePlanningDir(cwd);
560
565
  const p = ledgerPath(cwd);
561
566
  const tmp = `${p}.${process.pid}.tmp`;
562
- node_fs_1.default.writeFileSync(tmp, renderLedger(ledger), 'utf8');
567
+ // #2893: preserve any prose below the JSON ledger block. renderLedger
568
+ // reconstructs frontmatter + header + table + JSON — it does not include
569
+ // trailing prose that users may have written below the closing fence.
570
+ // Without this, every append/waive/fixed silently destroys that prose.
571
+ let trailingProse = '';
572
+ try {
573
+ const existing = node_fs_1.default.readFileSync(p, 'utf8');
574
+ // #2893: search for the CLOSING fence starting AFTER the opening fence,
575
+ // mirroring parseJsonBlock — indexOf(JSON_FENCE_CLOSE) alone would match
576
+ // the opening fence ('````json' starts with '````').
577
+ const openIdx = existing.indexOf(JSON_FENCE_OPEN);
578
+ if (openIdx !== -1) {
579
+ const fenceEnd = existing.indexOf(JSON_FENCE_CLOSE, openIdx + JSON_FENCE_OPEN.length);
580
+ if (fenceEnd !== -1) {
581
+ const afterFence = existing.slice(fenceEnd + JSON_FENCE_CLOSE.length);
582
+ // Drop leading newlines; keep the rest as prose.
583
+ trailingProse = afterFence.replace(/^(?:\r?\n)+/, '');
584
+ }
585
+ }
586
+ }
587
+ catch {
588
+ // File doesn't exist yet (first write) — no prose to preserve.
589
+ }
590
+ const rendered = renderLedger(ledger);
591
+ const content = trailingProse ? `${rendered}${trailingProse}` : rendered;
592
+ node_fs_1.default.writeFileSync(tmp, content, 'utf8');
563
593
  try {
564
594
  renameWithRetry(tmp, p);
565
595
  }
@@ -16,8 +16,9 @@
16
16
  * project ledger — `'' === ''` is no binding) NOR to the `disclosureSignature` alone (which covers
17
17
  * only executable surfaces, so a declarative-only cap has a constant signature and a repo-write
18
18
  * attacker could swap `capability.json` for a malicious gate/contribution while consent still
19
- * matched). `bundleContentHash` is recomputed by the loader at load over EVERY file in the bundle
20
- * (manifest AND artifacts AND identity), so any tamper — declarative-only swap, hook-script edit,
19
+ * matched). `bundleContentHash` is recomputed by the loader at load over the bundle (manifest AND
20
+ * artifacts AND identity), excluding only derived Python bytecode/cache noise (#3631 — see
21
+ * `bundleContentHash`'s own doc comment), so any tamper — declarative-only swap, hook-script edit,
21
22
  * empty-integrity local install — changes the hash and leaves the cap inactive. `integrity` and
22
23
  * `disclosureSignature` remain on the record for the human disclosure + re-consent-on-executable-
23
24
  * change UX (TRUST-2); they are NO LONGER the security binding.
@@ -182,10 +183,31 @@ function normalizeSepBytes(rel) {
182
183
  }
183
184
  /**
184
185
  * Recursively collect every REGULAR file AND every DIRECTORY under `absDir` as RAW-BYTE POSIX-relative
185
- * paths (`rel`, relative to the bundle root), refusing to follow symlinks out of the bundle. Bounded:
186
- * throws if the entry count or total byte size exceeds the caps (fail closed — a hostile/runaway tree
187
- * never hashes unbounded content). A non-regular entry encountered IN the tree (FIFO/device) is a
188
- * fail-closed throw — a bundle must be plain files and directories.
186
+ * paths (`rel`, relative to the bundle root), refusing to follow symlinks out of the bundle. Two
187
+ * NARROW, RECURSION-PRESERVING exclusions apply (#3631, amended after two orthogonal reviews found the
188
+ * original wholesale-skip UNSAFE — see the doc comment on `bundleContentHash` for the full rationale
189
+ * and the accepted residual risk):
190
+ *
191
+ * 1. A DIRECTORY whose basename is exactly `__pycache__` or `.pytest_cache`: its own TAG_DIR marker
192
+ * is SUPPRESSED (not pushed), but it is STILL RECURSED INTO — every child that is not itself
193
+ * excluded is still hashed. Suppressing only the marker is what keeps an empty/all-excluded such
194
+ * directory from moving the digest; recursing is what closes the original hole (H1): skipping
195
+ * recursion made an excluded directory an unbounded, permanently-unhashed region a manifest could
196
+ * point a hook `script` at (`__pycache__/run.js`) to install benign, then rewrite freely post-consent.
197
+ * 2. A FILE whose name ends `.pyc` or `.pyo` is excluded ONLY when its PARENT directory's basename is
198
+ * exactly `__pycache__` (checked byte-exact, never `.pytest_cache`) — a `.pyc`/`.pyo` anywhere else
199
+ * (bundle root, `scripts/`, a directory literally named `cache.pyc`, etc.) is hashed normally,
200
+ * because a sourceless legacy `.pyc` there IS importable and executable (H2).
201
+ *
202
+ * A regular FILE literally named `__pycache__` or `.pytest_cache` is NOT excluded (only a DIRECTORY of
203
+ * that basename gets marker-suppression) — it is hashed like any other file. A DIRECTORY literally
204
+ * named `x.pyc` is NOT excluded either — the suffix rule only ever applies to files. All exclusion
205
+ * checks run strictly AFTER the lstat-backed symlink/non-regular rejections below, so a symlink or
206
+ * device masquerading under an excluded name is still fail-closed rejected rather than silently
207
+ * skipped. Bounded: throws if the entry count or total byte size exceeds the caps (fail closed — a
208
+ * hostile/runaway tree never hashes unbounded content); an excluded entry still counts toward BOTH
209
+ * caps (the caps guard the walk itself, not the digest). A non-regular entry encountered IN the tree
210
+ * (FIFO/device) is a fail-closed throw — a bundle must be plain files and directories.
189
211
  *
190
212
  * #1459 finding 2 (MED/HIGH, ROUND 6): the enumeration ITSELF is bounded. Instead of
191
213
  * `fs.readdirSync` (which loads + sorts a WHOLE directory before the count cap — so a malicious bundle
@@ -204,11 +226,72 @@ function normalizeSepBytes(rel) {
204
226
  * abs/rel paths are concatenated at the BYTE level, so an invalid-UTF-8 filename is never lossily
205
227
  * decoded — two filenames that differ only in invalid bytes produce distinct rel byte strings.
206
228
  *
207
- * @param absDir the absolute directory to scan, as RAW BYTES (Buffer).
208
- * @param relDir the relpath of `absDir` from the bundle root, as RAW BYTES (Buffer; empty at the root).
209
- * @param count the CUMULATIVE entry counter shared across the whole recursive walk (fail-closed at the cap).
229
+ * @param absDir the absolute directory to scan, as RAW BYTES (Buffer).
230
+ * @param relDir the relpath of `absDir` from the bundle root, as RAW BYTES (Buffer; empty at the root).
231
+ * @param dirBasename the basename of `absDir` itself, as RAW BYTES (Buffer) — the PARENT basename every
232
+ * entry collected at this level shares. Threaded down so a `.pyc`/`.pyo` FILE can be
233
+ * excluded only when ITS parent is exactly `__pycache__` (empty at the bundle root).
234
+ * @param count the CUMULATIVE entry counter shared across the whole recursive walk (fail-closed at the cap).
235
+ */
236
+ // #3631 (amended — see bundleContentHash's doc comment for the H1/H2 rationale and accepted residual
237
+ // risk): basename rules for what is excluded FROM THE DIGEST. They are NOT excluded from the walk's
238
+ // resource caps — see the count.n / total.bytes accounting below, which still sees every excluded
239
+ // entry.
240
+ //
241
+ // Exact byte comparison, case-sensitive, deliberately: CPython always writes a lowercase
242
+ // `__pycache__` directory, so byte-exact matching keeps the digest identical across case-insensitive
243
+ // filesystems (e.g. default macOS/Windows) instead of varying with how a name happens to be spelled
244
+ // on disk.
245
+ //
246
+ // DELIBERATELY NOT on this list: node_modules, dist, build, or similar. Those hold code that is
247
+ // actually required/executed at runtime, so excluding them from the digest would stop consent from
248
+ // binding executable content — turning a usability bug (noisy re-consent prompts) into a
249
+ // supply-chain hole (a swapped dependency that never re-triggers consent).
250
+ /** Directory basenames whose TAG_DIR marker is suppressed — but the directory is STILL RECURSED INTO. */
251
+ const PYCACHE_DIR_BASENAMES = [
252
+ Buffer.from('__pycache__'),
253
+ Buffer.from('.pytest_cache'),
254
+ ];
255
+ /** FILE-name suffixes excluded ONLY when the file's parent directory basename is `__pycache__` (see below). */
256
+ const PYCACHE_FILE_SUFFIXES = [
257
+ Buffer.from('.pyc'),
258
+ Buffer.from('.pyo'),
259
+ ];
260
+ /** The one parent directory basename that gates the `.pyc`/`.pyo` FILE-suffix exclusion — never `.pytest_cache`. */
261
+ const PYCACHE_PARENT_BASENAME = Buffer.from('__pycache__');
262
+ /**
263
+ * True when `name` (a raw-byte dirent basename) is a DIRECTORY whose TAG_DIR marker must be
264
+ * suppressed — `__pycache__` or `.pytest_cache`, exact byte match. The caller still recurses into it;
265
+ * this predicate answers ONLY "skip the marker", never "skip the subtree".
266
+ */
267
+ function isPycacheDirBasename(name) {
268
+ for (const exact of PYCACHE_DIR_BASENAMES) {
269
+ if (Buffer.compare(name, exact) === 0)
270
+ return true;
271
+ }
272
+ return false;
273
+ }
274
+ /** True when raw-byte basename `name` ends in `.pyc` or `.pyo` (byte-suffix match, never utf8-decoded). */
275
+ function hasPycacheFileSuffix(name) {
276
+ for (const suffix of PYCACHE_FILE_SUFFIXES) {
277
+ if (name.length >= suffix.length && Buffer.compare(name.subarray(name.length - suffix.length), suffix) === 0) {
278
+ return true;
279
+ }
280
+ }
281
+ return false;
282
+ }
283
+ /**
284
+ * True when a FILE with basename `name`, inside a directory whose OWN basename is `dirBasename`, must
285
+ * be excluded from the digest: a `.pyc`/`.pyo` suffix whose parent directory basename is exactly
286
+ * `__pycache__` (byte-exact — never `.pytest_cache`, so a `.pyc` sitting directly inside a
287
+ * `.pytest_cache` dir is still hashed). A `.pyc`/`.pyo` file anywhere else — bundle root, `scripts/`,
288
+ * a directory literally named `cache.pyc`, etc. — is NOT excluded (H2): a sourceless legacy `.pyc`
289
+ * there is importable and executable, so it must stay bound to consent.
210
290
  */
211
- function collectBundleEntries(absDir, relDir, acc, total, count) {
291
+ function isExcludedFileBasename(name, dirBasename) {
292
+ return hasPycacheFileSuffix(name) && Buffer.compare(dirBasename, PYCACHE_PARENT_BASENAME) === 0;
293
+ }
294
+ function collectBundleEntries(absDir, relDir, dirBasename, acc, total, count) {
212
295
  let dir;
213
296
  try {
214
297
  // RAW-BYTE streaming open: dirent names are Buffers (encoding: 'buffer'), so an invalid-UTF-8
@@ -269,19 +352,35 @@ function collectBundleEntries(absDir, relDir, acc, total, count) {
269
352
  throw new Error(`bundleContentHash: refusing to hash a symlink in the bundle: "${abs.toString('utf8')}"`);
270
353
  }
271
354
  if (st.isDirectory()) {
355
+ // #3631 (amended, H1): exclusion is checked HERE — after the symlink fail-closed throw above —
356
+ // so a symlink named e.g. "__pycache__" or "x.pyc" is never silently skipped; only a REAL
357
+ // (lstat-confirmed) dir/file can be excluded. An excluded directory's own dirent was already
358
+ // counted toward count.n above (the cap guards the walk itself). Only the TAG_DIR MARKER is
359
+ // suppressed for `__pycache__`/`.pytest_cache` — the directory is ALWAYS recursed into so every
360
+ // non-excluded child underneath is still hashed (closing H1: no unbounded unhashed region).
361
+ if (isPycacheDirBasename(name)) {
362
+ collectBundleEntries(abs, rel, name, acc, total, count);
363
+ continue;
364
+ }
272
365
  // Emit a typed DIR marker for THIS directory (so an empty dir is bound), then recurse into it.
273
366
  acc.push({ abs, rel, kind: 'dir' });
274
- collectBundleEntries(abs, rel, acc, total, count);
367
+ collectBundleEntries(abs, rel, name, acc, total, count);
275
368
  continue;
276
369
  }
277
370
  if (!st.isFile()) {
278
371
  throw new Error(`bundleContentHash: refusing to hash a non-regular file in the bundle: "${abs.toString('utf8')}"`);
279
372
  }
280
- acc.push({ abs, rel, kind: 'file' });
373
+ // #3631: an excluded FILE (a __pycache__/*.pyc or *.pyo) still counts its bytes toward the
374
+ // total-size cap below — exclusion answers "does this bind the digest?", not "is this safe to
375
+ // read unbounded?" — so it must never become a way to smuggle unbounded bytes past
376
+ // BUNDLE_MAX_TOTAL_BYTES.
281
377
  total.bytes += st.size;
282
378
  if (total.bytes > BUNDLE_MAX_TOTAL_BYTES) {
283
379
  throw new Error(`bundleContentHash: bundle size exceeds ${BUNDLE_MAX_TOTAL_BYTES} bytes (refusing)`);
284
380
  }
381
+ if (isExcludedFileBasename(name, dirBasename))
382
+ continue;
383
+ acc.push({ abs, rel, kind: 'file' });
285
384
  }
286
385
  }
287
386
  /** Encode an unsigned 32-bit length as 4 big-endian bytes (the path-length frame). */
@@ -304,8 +403,39 @@ const TAG_FILE = Buffer.from([0x01]);
304
403
  const TAG_DIR = Buffer.from([0x02]);
305
404
  /**
306
405
  * The recomputed full-bundle content hash (#1459 CB-1/CB-2/TRUST2-5) — the SECURITY BINDING. A
307
- * `sha512-<base64>` over a DETERMINISTIC, INJECTIVE, LOSSLESS serialization of EVERY regular file
308
- * AND directory under `capDir` (recursively).
406
+ * `sha512-<base64>` over a DETERMINISTIC, INJECTIVE, LOSSLESS serialization of every regular file
407
+ * AND directory under `capDir` (recursively), with two NARROW exclusions (#3631, amended after two
408
+ * orthogonal reviews found the original wholesale directory-skip UNSAFE):
409
+ * - A `__pycache__` or `.pytest_cache` DIRECTORY's own TAG_DIR marker is suppressed, but it is
410
+ * ALWAYS recursed into — every non-excluded child underneath is still hashed.
411
+ * - A `.pyc`/`.pyo` FILE is excluded ONLY when its immediate parent directory's basename is exactly
412
+ * `__pycache__`; a `.pyc`/`.pyo` anywhere else (bundle root, `scripts/`, etc.) is hashed normally,
413
+ * since a sourceless legacy `.pyc` there is importable and executable.
414
+ * `node_modules`, `dist`, `build`, and similar are deliberately NOT excluded: their contents are
415
+ * executed/required at runtime, so dropping them from the digest would stop consent from binding
416
+ * executable content.
417
+ *
418
+ * ACCEPTED RESIDUAL RISK (documented, not a defect to re-litigate): CPython's default `.pyc`
419
+ * invalidation (timestamp-based) validates a cached bytecode file against its SOURCE's mtime and size
420
+ * only — NOT against the source's content — and both mtime and size are attacker-settable by whoever
421
+ * can already write to the bundle. So a forged `__pycache__/mod.cpython-3XX.pyc` whose header mtime/size
422
+ * were copied from an unmodified, still-hashed `mod.py` will execute without moving this digest. Before
423
+ * this exclusion existed that write was detected (any file under `__pycache__` moved the hash); after
424
+ * it, it is not, for files matching the narrow `__pycache__/*.pyc` shape above. This is accepted
425
+ * deliberately — the alternative (H1's original wholesale skip, or hashing every regenerated bytecode
426
+ * file) either reopens an unbounded unhashed region or makes consent fire on routine bytecode caching —
427
+ * and is bounded by: (1) the attacker must already have POST-CONSENT write access to the bundle; (2)
428
+ * everything outside `__pycache__/*.pyc` — including sourceless legacy `.pyc`/`.pyo` files anywhere
429
+ * else — remains hashed. NOT a bound, despite `isSafeHookScriptPath` (capability-lifecycle.cts /
430
+ * capability-validator.cjs) rejecting a manifest-declared hook `script` that names a
431
+ * `__pycache__`/`.pytest_cache` segment or ends `.pyc`/`.pyo`: the excluded region is still reachable
432
+ * by ONE HOP of indirection from any hashed, consent-covered script — a `require`/`import` of a
433
+ * `__pycache__/*.pyc` module path is not itself a declared script and the validator never sees it —
434
+ * so a hashed `hooks/run.js` can load a `__pycache__/mod.pyc` whose bytes are then free to change
435
+ * post-consent with the digest unmoved. The validator guard raises the bar for DECLARED surfaces; it
436
+ * does not contain the risk. KNOWN LIMITATION: `.pytest_cache`'s CONTENTS still change the digest as
437
+ * normal files — only its directory marker is suppressed, so this residual risk does NOT extend to
438
+ * `.pytest_cache`.
309
439
  *
310
440
  * Canonicalization (#1459 findings 1 + 4 — the prior `relpath + NUL + content + NUL` over utf8-decoded
311
441
  * STRINGS was non-injective, lossy in CONTENT, AND lossy in the PATH component):
@@ -333,8 +463,12 @@ const TAG_DIR = Buffer.from([0x02]);
333
463
  function bundleContentHash(capDir) {
334
464
  // Resolve to an absolute path, then carry it as RAW BYTES so the walk never lossily decodes a name.
335
465
  const rootBytes = Buffer.from(node_path_1.default.resolve(capDir));
466
+ // The root's own basename is threaded as the initial `dirBasename` so the parent-basename check for
467
+ // a `.pyc`/`.pyo` FILE applies even to a file placed DIRECTLY at the bundle root (root literally
468
+ // named `__pycache__` is the only case this matters for, and it is a correct, if exotic, match).
469
+ const rootBasename = Buffer.from(node_path_1.default.basename(node_path_1.default.resolve(capDir)));
336
470
  const entries = [];
337
- collectBundleEntries(rootBytes, Buffer.alloc(0), entries, { bytes: 0 }, { n: 0 });
471
+ collectBundleEntries(rootBytes, Buffer.alloc(0), rootBasename, entries, { bytes: 0 }, { n: 0 });
338
472
  // Sort by the raw-byte (separator-normalized) relpath so the digest is identical on Windows and POSIX,
339
473
  // and is independent of the on-disk creation/readdir order. Tie-break on kind so a (degenerate, never
340
474
  // produced on a real fs) file-and-dir same-relpath pair still has a stable order.
@@ -42,6 +42,15 @@ const semverMod = require('./semver-compare.cjs');
42
42
  // ---------------------------------------------------------------------------
43
43
  /** Stamp written onto every capability-owned shared-config entry, for surgical removal. */
44
44
  const CAP_MARKER = '_gsdCapability';
45
+ const GIT_SHA_PIN_RE = /^sha:[0-9a-f]{7,40}$/i;
46
+ function resolveIntegrityPin(integrity, parsed) {
47
+ if (typeof integrity === 'string' && integrity.length > 0)
48
+ return 'sha512';
49
+ if (parsed.kind === 'git' && typeof parsed.ref === 'string' && GIT_SHA_PIN_RE.test(parsed.ref)) {
50
+ return 'git-commit';
51
+ }
52
+ return 'none';
53
+ }
45
54
  /** Keys that must never be used as object indices (prototype-pollution guard). */
46
55
  function isUnsafeKey(k) {
47
56
  return k === '__proto__' || k === 'constructor' || k === 'prototype';
@@ -253,6 +262,28 @@ function confinedSharedFile(runtimeDir, relFile) {
253
262
  // isSafeHookScriptPath; see confinedBundleScript for why). Only [A-Za-z0-9._/-], no leading
254
263
  // `-` segment, no `..`, not absolute.
255
264
  const SAFE_HOOK_SCRIPT_RE = /^[A-Za-z0-9._/-]+$/;
265
+ // #3631 (defense-in-depth, mirrors capability-validator.cjs — KEEP BOTH IN SYNC): a declared script
266
+ // path must not point into the space bundleContentHash (capability-consent.cts) excludes from the
267
+ // consent-binding digest. A file whose basename ends `.pyc`/`.pyo` can contain perfectly valid
268
+ // JavaScript and would be executed by `node` regardless of extension, and a `__pycache__`/
269
+ // `.pytest_cache` segment marks a directory whose digest marker is suppressed — so a MANIFEST-DECLARED
270
+ // executable surface must never be able to reach either, or the exclusion becomes reachable from a
271
+ // path an attacker fully controls at declare-time rather than only via post-consent tamper.
272
+ // Regex asymmetry is DELIBERATE, KEEP BOTH RULES IN SYNC WITH capability-validator.cjs (byte-identical
273
+ // text, verified by the isSafeHookScriptPath parity test in tests/capability-registry.test.cjs):
274
+ // (i) PYCACHE_SUFFIX_RE is case-INSENSITIVE (`/i`) on purpose — a validator should be STRICTER than
275
+ // the digest it defends, so it rejects `x.PYC` too even though bundleContentHash's own suffix
276
+ // match (hasPycacheFileSuffix, capability-consent.cts) is byte-exact and would still hash it.
277
+ // (ii) The __pycache__/.pytest_cache SEGMENT match is case-SENSITIVE to match the digest's own
278
+ // byte-exact, case-sensitive directory-basename comparison (CPython always writes a lowercase
279
+ // `__pycache__`) — a validator segment match looser than the digest here would reject paths the
280
+ // digest would still hash, which is over-strict in the wrong direction for a defense-in-depth
281
+ // check layered on top of an already-correct digest.
282
+ // (iii) The `[/\\]` backslash alternations in both regexes are defensive/UNREACHABLE in practice:
283
+ // SAFE_HOOK_SCRIPT_RE (above) already rejects any backslash character outright, so a script
284
+ // string containing `\` never reaches either PYCACHE_*_RE check.
285
+ const PYCACHE_SEGMENT_RE = /(?:^|[/\\])(__pycache__|\.pytest_cache)(?:[/\\]|$)/;
286
+ const PYCACHE_SUFFIX_RE = /\.(pyc|pyo)$/i;
256
287
  function isSafeHookScriptPath(script) {
257
288
  if (typeof script !== 'string' || script.length === 0)
258
289
  return false;
@@ -267,6 +298,10 @@ function isSafeHookScriptPath(script) {
267
298
  if (seg.startsWith('-'))
268
299
  return false;
269
300
  }
301
+ if (PYCACHE_SEGMENT_RE.test(script))
302
+ return false;
303
+ if (PYCACHE_SUFFIX_RE.test(node_path_1.default.basename(script)))
304
+ return false;
270
305
  return true;
271
306
  }
272
307
  /**
@@ -504,6 +539,14 @@ function applyCapabilitySharedEdits(args) {
504
539
  settings['hooks'] = hooksObj;
505
540
  }
506
541
  if (mcpEntries.length > 0) {
542
+ // #3515 (epic #1900 F20): the MCP config below is written VERBATIM — command/args/env/cwd
543
+ // are NOT confined to the bundle the way hook scripts are (confinedBundleScript, D5 rule 5).
544
+ // This asymmetry is INTENTIONAL: most real MCP servers legitimately resolve command/args/cwd
545
+ // to global or npx installs outside the capability bundle, so confinement would break them.
546
+ // The compensating controls are disclosure + re-consent: the consent prompt renders an
547
+ // explicit "not confined to the bundle" notice for every spawned server (summarizeDisclosure,
548
+ // capability-trust.cts), and disclosureSignature folds command/args/env/cwd + the FULL
549
+ // rawConfig as stable-sorted JSON (#1459 finding 5), so ANY config change forces re-consent.
507
550
  const mcpObj = (typeof settings['mcpServers'] === 'object' && settings['mcpServers'] !== null && !Array.isArray(settings['mcpServers']))
508
551
  ? settings['mcpServers']
509
552
  : {};
@@ -846,6 +889,7 @@ async function installCapability(spec, opts) {
846
889
  stagedDir,
847
890
  strictKnownRegistries,
848
891
  hostVersion,
892
+ integrityPin: resolveIntegrityPin(opts.integrity, parsedPre),
849
893
  });
850
894
  if (!verdict.allowed) {
851
895
  return { status: 'blocked', disclosure: verdict.disclosure, blockReasons: verdict.blockReasons };
@@ -1038,6 +1082,7 @@ async function upgradeCapability(spec, opts) {
1038
1082
  stagedDir,
1039
1083
  strictKnownRegistries,
1040
1084
  hostVersion,
1085
+ integrityPin: resolveIntegrityPin(opts.integrity, parsedPre),
1041
1086
  });
1042
1087
  if (!verdict.allowed) {
1043
1088
  return { status: 'blocked', disclosure: verdict.disclosure, blockReasons: verdict.blockReasons };