@lannguyensi/harness 0.43.0 → 0.45.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 (237) hide show
  1. package/CHANGELOG.md +1248 -0
  2. package/dist/cli/adopt/derive.d.ts +16 -7
  3. package/dist/cli/adopt/derive.js +19 -2
  4. package/dist/cli/adopt/derive.js.map +1 -1
  5. package/dist/cli/adopt/index.js +18 -3
  6. package/dist/cli/adopt/index.js.map +1 -1
  7. package/dist/cli/apply/apply.d.ts +1 -0
  8. package/dist/cli/apply/apply.js +62 -6
  9. package/dist/cli/apply/apply.js.map +1 -1
  10. package/dist/cli/apply/generate-codex-config.js +5 -6
  11. package/dist/cli/apply/generate-codex-config.js.map +1 -1
  12. package/dist/cli/apply/generate-opencode-config.d.ts +58 -0
  13. package/dist/cli/apply/generate-opencode-config.js +210 -0
  14. package/dist/cli/apply/generate-opencode-config.js.map +1 -0
  15. package/dist/cli/apply/generate-settings.d.ts +90 -32
  16. package/dist/cli/apply/generate-settings.js +133 -10
  17. package/dist/cli/apply/generate-settings.js.map +1 -1
  18. package/dist/cli/apply/index.d.ts +2 -1
  19. package/dist/cli/apply/index.js +2 -1
  20. package/dist/cli/apply/index.js.map +1 -1
  21. package/dist/cli/approve/stdin-report.d.ts +15 -0
  22. package/dist/cli/approve/stdin-report.js +1 -1
  23. package/dist/cli/approve/stdin-report.js.map +1 -1
  24. package/dist/cli/approve/understanding.d.ts +75 -0
  25. package/dist/cli/approve/understanding.js +150 -2
  26. package/dist/cli/approve/understanding.js.map +1 -1
  27. package/dist/cli/audit.d.ts +1 -1
  28. package/dist/cli/audit.js +3 -2
  29. package/dist/cli/audit.js.map +1 -1
  30. package/dist/cli/doctor/claude-mcp.d.ts +19 -1
  31. package/dist/cli/doctor/claude-mcp.js +36 -10
  32. package/dist/cli/doctor/claude-mcp.js.map +1 -1
  33. package/dist/cli/doctor/codex.d.ts +3 -1
  34. package/dist/cli/doctor/codex.js +6 -9
  35. package/dist/cli/doctor/codex.js.map +1 -1
  36. package/dist/cli/doctor/format.js +94 -8
  37. package/dist/cli/doctor/format.js.map +1 -1
  38. package/dist/cli/doctor/index.d.ts +23 -0
  39. package/dist/cli/doctor/index.js +106 -2
  40. package/dist/cli/doctor/index.js.map +1 -1
  41. package/dist/cli/doctor/opencode.d.ts +24 -0
  42. package/dist/cli/doctor/opencode.js +107 -0
  43. package/dist/cli/doctor/opencode.js.map +1 -0
  44. package/dist/cli/doctor/target-checks.d.ts +11 -0
  45. package/dist/cli/doctor/target-checks.js +25 -0
  46. package/dist/cli/doctor/target-checks.js.map +1 -0
  47. package/dist/cli/doctor/toolchain-parity.d.ts +63 -0
  48. package/dist/cli/doctor/toolchain-parity.js +154 -0
  49. package/dist/cli/doctor/toolchain-parity.js.map +1 -0
  50. package/dist/cli/doctor/types.d.ts +85 -1
  51. package/dist/cli/doctor/types.js +1 -1
  52. package/dist/cli/doctor/types.js.map +1 -1
  53. package/dist/cli/doctor/understanding-mode-env.d.ts +39 -0
  54. package/dist/cli/doctor/understanding-mode-env.js +83 -0
  55. package/dist/cli/doctor/understanding-mode-env.js.map +1 -0
  56. package/dist/cli/dry-run.js +34 -13
  57. package/dist/cli/dry-run.js.map +1 -1
  58. package/dist/cli/explain.d.ts +1 -1
  59. package/dist/cli/explain.js +1 -1
  60. package/dist/cli/explain.js.map +1 -1
  61. package/dist/cli/index.js +62 -7
  62. package/dist/cli/index.js.map +1 -1
  63. package/dist/cli/init/composer.js +36 -13
  64. package/dist/cli/init/composer.js.map +1 -1
  65. package/dist/cli/init/interactive.d.ts +7 -4
  66. package/dist/cli/init/interactive.js +79 -27
  67. package/dist/cli/init/interactive.js.map +1 -1
  68. package/dist/cli/init/profiles.d.ts +2 -2
  69. package/dist/cli/init/profiles.js +32 -1
  70. package/dist/cli/init/profiles.js.map +1 -1
  71. package/dist/cli/init/templates.d.ts +2 -1
  72. package/dist/cli/init/templates.js +136 -24
  73. package/dist/cli/init/templates.js.map +1 -1
  74. package/dist/cli/pack/approve-escape.js +69 -8
  75. package/dist/cli/pack/approve-escape.js.map +1 -1
  76. package/dist/cli/pack/hook-branch-protection.js +1 -1
  77. package/dist/cli/pack/hook-branch-protection.js.map +1 -1
  78. package/dist/cli/pack/hook-codex-pre-tool-use.js +27 -1
  79. package/dist/cli/pack/hook-codex-pre-tool-use.js.map +1 -1
  80. package/dist/cli/pack/hook-post-merge-gate.js +44 -26
  81. package/dist/cli/pack/hook-post-merge-gate.js.map +1 -1
  82. package/dist/cli/pack/hook-pre-tool-use.js +91 -4
  83. package/dist/cli/pack/hook-pre-tool-use.js.map +1 -1
  84. package/dist/cli/pack/hook-solution-acceptance.js +19 -2
  85. package/dist/cli/pack/hook-solution-acceptance.js.map +1 -1
  86. package/dist/cli/pack/understanding-report-schema-hint.d.ts +13 -0
  87. package/dist/cli/pack/understanding-report-schema-hint.js +52 -3
  88. package/dist/cli/pack/understanding-report-schema-hint.js.map +1 -1
  89. package/dist/cli/pause/index.js +1 -1
  90. package/dist/cli/pause/index.js.map +1 -1
  91. package/dist/cli/policy/intercept.d.ts +148 -0
  92. package/dist/cli/policy/intercept.js +538 -79
  93. package/dist/cli/policy/intercept.js.map +1 -1
  94. package/dist/cli/session-start/index.d.ts +12 -3
  95. package/dist/cli/session-start/index.js +67 -11
  96. package/dist/cli/session-start/index.js.map +1 -1
  97. package/dist/cli/session-start/stale-base-check.d.ts +71 -0
  98. package/dist/cli/session-start/stale-base-check.js +379 -0
  99. package/dist/cli/session-start/stale-base-check.js.map +1 -0
  100. package/dist/cli/session-start/toolchain-parity.d.ts +142 -8
  101. package/dist/cli/session-start/toolchain-parity.js +244 -36
  102. package/dist/cli/session-start/toolchain-parity.js.map +1 -1
  103. package/dist/cli/smoke/assertions.d.ts +9 -2
  104. package/dist/cli/smoke/assertions.js +9 -2
  105. package/dist/cli/smoke/assertions.js.map +1 -1
  106. package/dist/cli/uninstall/index.js +18 -2
  107. package/dist/cli/uninstall/index.js.map +1 -1
  108. package/dist/cli/validate/checks.d.ts +3 -1
  109. package/dist/cli/validate/checks.js +298 -3
  110. package/dist/cli/validate/checks.js.map +1 -1
  111. package/dist/io/claude-mcp.d.ts +122 -1
  112. package/dist/io/claude-mcp.js +214 -12
  113. package/dist/io/claude-mcp.js.map +1 -1
  114. package/dist/io/duration.js.map +1 -0
  115. package/dist/io/expand-home.js.map +1 -0
  116. package/dist/io/extract.js.map +1 -0
  117. package/dist/io/harness-lock.js +1 -1
  118. package/dist/io/harness-lock.js.map +1 -1
  119. package/dist/{runtime → io}/ledger-record.d.ts +1 -1
  120. package/dist/io/ledger-record.js.map +1 -0
  121. package/dist/policies/index.d.ts +2 -2
  122. package/dist/policies/index.js +2 -2
  123. package/dist/policies/index.js.map +1 -1
  124. package/dist/policies/ledger-client.js +10 -6
  125. package/dist/policies/ledger-client.js.map +1 -1
  126. package/dist/policies/producers.js +1 -1
  127. package/dist/policies/producers.js.map +1 -1
  128. package/dist/policies/requires.js +2 -2
  129. package/dist/policies/requires.js.map +1 -1
  130. package/dist/policy-packs/builtin/branch-protection.js +28 -3
  131. package/dist/policy-packs/builtin/branch-protection.js.map +1 -1
  132. package/dist/policy-packs/builtin/post-merge-gate-runtime.d.ts +90 -10
  133. package/dist/policy-packs/builtin/post-merge-gate-runtime.js +112 -16
  134. package/dist/policy-packs/builtin/post-merge-gate-runtime.js.map +1 -1
  135. package/dist/policy-packs/builtin/post-merge-gate.js +84 -31
  136. package/dist/policy-packs/builtin/post-merge-gate.js.map +1 -1
  137. package/dist/policy-packs/builtin/solution-acceptance-runtime.d.ts +152 -8
  138. package/dist/policy-packs/builtin/solution-acceptance-runtime.js +260 -13
  139. package/dist/policy-packs/builtin/solution-acceptance-runtime.js.map +1 -1
  140. package/dist/policy-packs/builtin/solution-acceptance.js +17 -3
  141. package/dist/policy-packs/builtin/solution-acceptance.js.map +1 -1
  142. package/dist/policy-packs/builtin/understanding-before-execution/active-claim.d.ts +25 -0
  143. package/dist/policy-packs/builtin/understanding-before-execution/active-claim.js +89 -0
  144. package/dist/policy-packs/builtin/understanding-before-execution/active-claim.js.map +1 -0
  145. package/dist/policy-packs/builtin/understanding-before-execution/index.d.ts +13 -0
  146. package/dist/policy-packs/builtin/understanding-before-execution/index.js +39 -0
  147. package/dist/policy-packs/builtin/understanding-before-execution/index.js.map +1 -0
  148. package/dist/policy-packs/builtin/understanding-before-execution/ledger.d.ts +25 -0
  149. package/dist/policy-packs/builtin/understanding-before-execution/ledger.js +56 -0
  150. package/dist/policy-packs/builtin/understanding-before-execution/ledger.js.map +1 -0
  151. package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.d.ts +20 -0
  152. package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.js +79 -0
  153. package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.js.map +1 -0
  154. package/dist/policy-packs/builtin/understanding-before-execution/markers.d.ts +103 -0
  155. package/dist/policy-packs/builtin/understanding-before-execution/markers.js +203 -0
  156. package/dist/policy-packs/builtin/understanding-before-execution/markers.js.map +1 -0
  157. package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.d.ts +177 -0
  158. package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.js +309 -0
  159. package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.js.map +1 -0
  160. package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.d.ts +106 -0
  161. package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.js +209 -0
  162. package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.js.map +1 -0
  163. package/dist/policy-packs/builtin/understanding-before-execution/task-markers.d.ts +78 -0
  164. package/dist/policy-packs/builtin/understanding-before-execution/task-markers.js +185 -0
  165. package/dist/policy-packs/builtin/understanding-before-execution/task-markers.js.map +1 -0
  166. package/dist/policy-packs/builtin/understanding-before-execution-runtime.d.ts +1 -536
  167. package/dist/policy-packs/builtin/understanding-before-execution-runtime.js +9 -1095
  168. package/dist/policy-packs/builtin/understanding-before-execution-runtime.js.map +1 -1
  169. package/dist/policy-packs/builtin/understanding-before-execution.d.ts +109 -1
  170. package/dist/policy-packs/builtin/understanding-before-execution.js +183 -15
  171. package/dist/policy-packs/builtin/understanding-before-execution.js.map +1 -1
  172. package/dist/policy-packs/registry.d.ts +11 -0
  173. package/dist/policy-packs/registry.js +1 -1
  174. package/dist/policy-packs/registry.js.map +1 -1
  175. package/dist/policy-packs/runtime.d.ts +1 -1
  176. package/dist/policy-packs/runtime.js +10 -1
  177. package/dist/policy-packs/runtime.js.map +1 -1
  178. package/dist/runtime/agent-facing.js +1 -1
  179. package/dist/runtime/agent-facing.js.map +1 -1
  180. package/dist/runtime/bash-match-facts.d.ts +9 -7
  181. package/dist/runtime/bash-match-facts.js +9 -7
  182. package/dist/runtime/bash-match-facts.js.map +1 -1
  183. package/dist/runtime/bash-match-registry.d.ts +75 -38
  184. package/dist/runtime/bash-match-registry.js +279 -232
  185. package/dist/runtime/bash-match-registry.js.map +1 -1
  186. package/dist/runtime/bash-prefix-parse.d.ts +7 -0
  187. package/dist/runtime/bash-prefix-parse.js +205 -17
  188. package/dist/runtime/bash-prefix-parse.js.map +1 -1
  189. package/dist/runtime/command-normalize.d.ts +429 -4
  190. package/dist/runtime/command-normalize.js +1146 -67
  191. package/dist/runtime/command-normalize.js.map +1 -1
  192. package/dist/runtime/hermetic-spawn-guard.js +6 -0
  193. package/dist/runtime/hermetic-spawn-guard.js.map +1 -1
  194. package/dist/runtime/index.d.ts +2 -2
  195. package/dist/runtime/index.js +2 -2
  196. package/dist/runtime/index.js.map +1 -1
  197. package/dist/runtime/intercept.d.ts +247 -4
  198. package/dist/runtime/intercept.js +553 -61
  199. package/dist/runtime/intercept.js.map +1 -1
  200. package/dist/runtime/ledger-add.js +1 -1
  201. package/dist/runtime/ledger-add.js.map +1 -1
  202. package/dist/runtime/read-only-bash.js +356 -26
  203. package/dist/runtime/read-only-bash.js.map +1 -1
  204. package/dist/runtime/shell-word.d.ts +21 -0
  205. package/dist/runtime/shell-word.js +242 -0
  206. package/dist/runtime/shell-word.js.map +1 -0
  207. package/dist/schema/doctor.d.ts +25 -0
  208. package/dist/schema/doctor.js +23 -0
  209. package/dist/schema/doctor.js.map +1 -0
  210. package/dist/schema/extract.js +1 -1
  211. package/dist/schema/extract.js.map +1 -1
  212. package/dist/schema/index.d.ts +76 -0
  213. package/dist/schema/index.js +9 -0
  214. package/dist/schema/index.js.map +1 -1
  215. package/dist/schema/requires.js +2 -2
  216. package/dist/schema/requires.js.map +1 -1
  217. package/dist/schema/risk.d.ts +7 -0
  218. package/dist/schema/risk.js +23 -0
  219. package/dist/schema/risk.js.map +1 -1
  220. package/dist/schema/stale-base-check.d.ts +18 -0
  221. package/dist/schema/stale-base-check.js +37 -0
  222. package/dist/schema/stale-base-check.js.map +1 -0
  223. package/dist/schema/toolchain-parity.d.ts +3 -0
  224. package/dist/schema/toolchain-parity.js +13 -0
  225. package/dist/schema/toolchain-parity.js.map +1 -1
  226. package/package.json +5 -2
  227. package/dist/policies/duration.js.map +0 -1
  228. package/dist/policies/extract.js.map +0 -1
  229. package/dist/runtime/expand-home.js.map +0 -1
  230. package/dist/runtime/ledger-record.js.map +0 -1
  231. /package/dist/{policies → io}/duration.d.ts +0 -0
  232. /package/dist/{policies → io}/duration.js +0 -0
  233. /package/dist/{runtime → io}/expand-home.d.ts +0 -0
  234. /package/dist/{runtime → io}/expand-home.js +0 -0
  235. /package/dist/{policies → io}/extract.d.ts +0 -0
  236. /package/dist/{policies → io}/extract.js +0 -0
  237. /package/dist/{runtime → io}/ledger-record.js +0 -0
@@ -29,10 +29,24 @@
29
29
  // scope" section). Read-only git (status/log/diff/branch) and
30
30
  // unrelated shell are never touched.
31
31
  //
32
- // - The escape allowlist is checked FIRST in the blocker, unconditionally,
33
- // before manifest load or any ledger query — see hook-post-merge-gate.ts.
34
- // This module only supplies the matcher; the ordering guarantee lives
35
- // in the hook, not here.
32
+ // - DENY WINS over the escape allowlist (task 19356be7, REVERSES the
33
+ // original escape-first decision): the blocker classifies a command as
34
+ // gate-eligible iff the curated mutation matcher fires
35
+ // (`isGateEligibleCommand` below). The escape allowlist no longer
36
+ // short-circuits the whole command — under escape-first, any chained
37
+ // command containing an escape verb anywhere (`harness preflight &&
38
+ // git push origin master`, the documented normal workflow) skipped the
39
+ // gate entirely. Measured before changing, over a 66-row corpus with
40
+ // bash ground truth: per-segment evaluation and deny-wins are
41
+ // 0-divergent — a consequence of the pinned verb disjointness between
42
+ // the escape and deny lists — so the simplest (deny-wins) is
43
+ // implemented. The third candidate, escape-at-start-only, was measured
44
+ // and REJECTED: it diverges on 26 rows and still exempts three of the
45
+ // four lines this task exists to close (`git switch master && git push
46
+ // origin master` and friends). The recovery-never-starved property is preserved
47
+ // structurally: a non-eligible command is allowed before any manifest
48
+ // load or ledger query, exactly where the escape short-circuit used to
49
+ // sit (see hook-post-merge-gate.ts for the ordering).
36
50
  //
37
51
  // - Fails OPEN (allows) when the ledger is unreachable, unlike
38
52
  // branch-protection's fail-closed posture: without the ledger,
@@ -70,33 +84,80 @@ export function buildMergedTagContent(args) {
70
84
  * indirection defeats this match before the producer ever runs — not
71
85
  * attempted to close here (the MCP merge path, `pull_requests_merge`, is
72
86
  * the other documented gap; see the pack's instructions.md).
87
+ *
88
+ * Task 76671e5a: bare `&` added to the boundary alternation (`&&` kept to
89
+ * its left only to mirror `src/runtime/command-normalize.ts`'s alternation
90
+ * order for readability — NOT because order is load-bearing here, unlike
91
+ * that module). Same fix as `d834a065` applied to every policy trigger —
92
+ * bash starts a new command after a single `&`, so `A=x&gh pr merge` used
93
+ * to miss this trigger entirely and the producer silently never recorded
94
+ * the merged-tip fact for that spelling. Broadening a TRIGGER only widens
95
+ * when the producer fires (a strictly safer direction, mirrors the
96
+ * deny-side reasoning below), unlike the escape allowlist further down.
97
+ * Measured: swapping to `&|&&` produces a byte-identical match set for this
98
+ * matcher (a `RegExp.test` existential check over every start offset, not a
99
+ * segmenter — the reasoning that makes order matter in
100
+ * `command-normalize.ts`'s `BOUNDARY_RE`/`AMP_BOUNDARY_RE` does not transfer
101
+ * to a plain `.test()` matcher like this one).
73
102
  */
74
- export const GH_PR_MERGE_BASH_RE = /(?:^|\n|;|\||&&|\()\s*(?:\w+=\S+\s+)*gh\s+pr\s+merge\b/;
103
+ export const GH_PR_MERGE_BASH_RE = /(?:^|\n|;|\||&&|&|\()\s*(?:\w+=\S+\s+)*gh\s+pr\s+merge\b/;
75
104
  /**
76
105
  * Blocker deny-scope v1 (03-decisions.md): the curated mutation command
77
106
  * list. Deliberately NOT "every Bash command" — read-only git
78
107
  * (status/log/diff/branch) and unrelated shell stay unaffected. Same
79
108
  * anchoring convention as `GH_PR_MERGE_BASH_RE`.
109
+ *
110
+ * Task 76671e5a: bare `&` added to the boundary alternation, same reasoning
111
+ * as `GH_PR_MERGE_BASH_RE` above — this is a DENY-scope matcher, so
112
+ * broadening it is the STRICTER direction (more commands recognized as
113
+ * curated mutations, none dropped). Verified disjoint from the escape
114
+ * allowlist below on a single-command corpus (no verb overlap), so this
115
+ * broadening cannot lock out the documented recovery path.
80
116
  */
81
- export const CURATED_MUTATION_BASH_RE = /(?:^|\n|;|\||&&|\()\s*(?:\w+=\S+\s+)*(?:git(?:\s+-C\s+\S+)?\s+(?:commit|add|push|merge|rebase|cherry-pick|revert|reset|stash\s+(?:pop|apply))\b|gh\s+pr\s+(?:create|merge)\b)/;
117
+ export const CURATED_MUTATION_BASH_RE = /(?:^|\n|;|\||&&|&|\()\s*(?:\w+=\S+\s+)*(?:git(?:\s+-C\s+\S+)?\s+(?:commit|add|push|merge|rebase|cherry-pick|revert|reset|stash\s+(?:pop|apply))\b|gh\s+pr\s+(?:create|merge)\b)/;
82
118
  /**
83
119
  * Escape allowlist, git verbs (03-decisions.md): the exact recovery path
84
- * the deny message recommends, plus read-only stash inspection. Checked
85
- * BEFORE the curated-mutation match and before any manifest/ledger access
86
- * — see hook-post-merge-gate.ts for the ordering guarantee.
120
+ * the deny message recommends, plus read-only stash inspection. Since task
121
+ * 19356be7 this list no longer short-circuits the blocker — the deny
122
+ * match decides gate-eligibility, and every one of these verbs stays free
123
+ * simply because it is not a curated mutation (verb disjointness, pinned).
124
+ * The list remains the deny message's recovery vocabulary and a
125
+ * diagnostics aid.
126
+ *
127
+ * Task 76671e5a, DELIBERATELY LEFT on the old (no bare-`&`) boundary
128
+ * alphabet: this is an ALLOW-side matcher, so broadening it is the LOOSER
129
+ * direction. Its remaining consumers (diagnostics, docs interpolation)
130
+ * still must not silently widen what reads as "recovery". Pinned by a
131
+ * regression test asserting this regex's source never gains the `&|`
132
+ * boundary token.
87
133
  */
88
134
  export const ESCAPE_GIT_BASH_RE = /(?:^|\n|;|\||&&|\()\s*(?:\w+=\S+\s+)*git(?:\s+-C\s+\S+)?\s+(?:switch\b|checkout\b|pull\b|fetch\b|branch\s+-(?:d|D)\b|stash\s+(?:list|show)\b)/;
89
135
  /**
90
- * Escape allowlist, harness verbs (03-decisions.md): any invocation of
91
- * harness's own CLI (recovery commands like `harness session-start
92
- * branch-check`, diagnostics, a future self-check) always passes,
93
- * regardless of spelling — mirrors the `npx` / absolute-path /
94
- * `./node_modules/.bin` robustness the `deny-kill-switch-bash` regex in
136
+ * Escape allowlist, harness verbs (03-decisions.md): names any invocation
137
+ * of harness's own CLI (recovery commands like `harness session-start
138
+ * branch-check`, diagnostics, a future self-check), regardless of
139
+ * spelling — mirrors the `npx` / absolute-path / `./node_modules/.bin`
140
+ * robustness the `deny-kill-switch-bash` regex in
95
141
  * `src/cli/init/templates.ts` already established for the same class of
96
- * bypass concern.
142
+ * bypass concern. Since task 19356be7 its only job is the deny message's
143
+ * / diagnostics' vocabulary: a bare harness command stays free through
144
+ * verb disjointness with the curated deny list, not through a
145
+ * short-circuit.
146
+ *
147
+ * Task 76671e5a: DELIBERATELY LEFT narrow, same reasoning as
148
+ * `ESCAPE_GIT_BASH_RE` immediately above — an allow-side matcher, so
149
+ * broadening its boundary alphabet is the loosening direction.
97
150
  */
98
151
  export const ESCAPE_HARNESS_BASH_RE = /(?:^|\n|;|\||&&|\()\s*(?:\w+=\S+\s+)*(?:npx\s+|\S*\/)?harness\b/;
99
- /** True when `command` matches any escape pattern. Checked first, unconditionally. */
152
+ /**
153
+ * True when `command` matches any escape pattern. Since task 19356be7
154
+ * this is DIAGNOSTICS ONLY — the blocker's decision is
155
+ * `isGateEligibleCommand` below; when escape and deny both match, deny
156
+ * wins. (Under the original escape-first ordering this function was the
157
+ * blocker's first, unconditional check, which let `harness preflight &&
158
+ * git push origin master` — the documented normal workflow — skip the
159
+ * gate entirely.)
160
+ */
100
161
  export function isEscapeCommand(command) {
101
162
  return ESCAPE_GIT_BASH_RE.test(command) || ESCAPE_HARNESS_BASH_RE.test(command);
102
163
  }
@@ -104,6 +165,41 @@ export function isEscapeCommand(command) {
104
165
  export function isCuratedMutationCommand(command) {
105
166
  return CURATED_MUTATION_BASH_RE.test(command);
106
167
  }
168
+ /**
169
+ * The blocker's single classification entry point (task 19356be7): a
170
+ * command is gate-eligible iff the curated mutation matcher fires.
171
+ * Everything else — the whole recovery vocabulary, read-only git,
172
+ * unrelated shell — is allowed by the hook BEFORE any manifest load or
173
+ * ledger query, preserving the recovery-never-starved property the
174
+ * escape-first ordering used to provide.
175
+ *
176
+ * A thin wrapper over `isCuratedMutationCommand` by DELIBERATE choice,
177
+ * not by accident. An earlier iteration of this task pre-processed the
178
+ * command here, stripping the bodies of quoted-delimiter heredocs fed to
179
+ * a harness invocation, so that an `harness approve understanding <<'…'`
180
+ * report body mentioning a mutation verb would not become gate-eligible.
181
+ * Two review rounds found two successive families of UNDER-blocks in that
182
+ * pre-processing (bodies consumed that bash really executes), both rooted
183
+ * in the same cause: deciding where a heredoc body BEGINS requires
184
+ * re-implementing bash's quote and word grammar on the permissive side of
185
+ * a security boundary. Tasks `dbc6d303` and `5b1b24fb` had already been
186
+ * halted for that same reason. It was therefore removed rather than
187
+ * patched a third time; see
188
+ * `.ai/runs/2026-08-02-post-merge-gate-escape-precedence/03-decisions.md`
189
+ * D5/D6 for the measurement history, and task `5b1b24fb` for the real
190
+ * fix, which belongs at the `bash_match` layer for every surface at once.
191
+ *
192
+ * Accepted consequence, OVER-block direction: a harness report heredoc
193
+ * whose body carries a boundary-anchored mutation verb IS gate-eligible.
194
+ * It only denies while the branch tip equals a recorded merged tip, and
195
+ * `git switch <default>` — the gate's own recommended recovery — stays
196
+ * free. Same residual class as the quoted-text false positives this
197
+ * change already accepts. Do NOT reintroduce a strip here without a
198
+ * design that does not re-derive bash's grammar.
199
+ */
200
+ export function isGateEligibleCommand(command) {
201
+ return isCuratedMutationCommand(command);
202
+ }
107
203
  /**
108
204
  * Defensive `tool_output.exit_code` reader for the PostToolUse Bash
109
205
  * payload — Contract A. Returns `null` on ANY shape other than a plain
@@ -1 +1 @@
1
- {"version":3,"file":"post-merge-gate-runtime.js","sourceRoot":"","sources":["../../../src/policy-packs/builtin/post-merge-gate-runtime.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"post-merge-gate-runtime.js","sourceRoot":"","sources":["../../../src/policy-packs/builtin/post-merge-gate-runtime.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,2EAA2E;AAC3E,yEAAyE;AACzE,yEAAyE;AACzE,0EAA0E;AAC1E,sEAAsE;AACtE,yEAAyE;AACzE,iDAAiD;AACjD,EAAE;AACF,6CAA6C;AAC7C,iEAAiE;AACjE,EAAE;AACF,qEAAqE;AACrE,wEAAwE;AACxE,wEAAwE;AACxE,yEAAyE;AACzE,wEAAwE;AACxE,0EAA0E;AAC1E,oEAAoE;AACpE,yEAAyE;AACzE,kEAAkE;AAClE,qEAAqE;AACrE,EAAE;AACF,yEAAyE;AACzE,0EAA0E;AAC1E,0DAA0D;AAC1D,sEAAsE;AACtE,kEAAkE;AAClE,yCAAyC;AACzC,EAAE;AACF,uEAAuE;AACvE,2EAA2E;AAC3E,2DAA2D;AAC3D,sEAAsE;AACtE,yEAAyE;AACzE,wEAAwE;AACxE,2EAA2E;AAC3E,yEAAyE;AACzE,kEAAkE;AAClE,0EAA0E;AAC1E,iEAAiE;AACjE,2EAA2E;AAC3E,0EAA0E;AAC1E,2EAA2E;AAC3E,oFAAoF;AACpF,0EAA0E;AAC1E,2EAA2E;AAC3E,0DAA0D;AAC1D,EAAE;AACF,iEAAiE;AACjE,mEAAmE;AACnE,uEAAuE;AACvE,wEAAwE;AACxE,yEAAyE;AACzE,kDAAkD;AAClD,oDAAoD;AAEpD,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAErG,MAAM,CAAC,MAAM,SAAS,GAAG,iBAAiB,CAAC;AAE3C;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,wBAAwB,CAAC;AAE1D,gEAAgE;AAChE,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,MAAc,EAAE,GAAW;IACzE,OAAO,GAAG,iBAAiB,IAAI,IAAI,IAAI,MAAM,IAAI,GAAG,EAAE,CAAC;AACzD,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,qBAAqB,CAAC,IAMrC;IACC,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC/C,OAAO,GAAG,iBAAiB,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC;AAC9F,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,0DAA0D,CAAC;AAE9F;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,wBAAwB,GACnC,iLAAiL,CAAC;AAEpL;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAC7B,+IAA+I,CAAC;AAElJ;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,iEAAiE,CAAC;AAExG;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,OAAO,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,sBAAsB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AAClF,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,wBAAwB,CAAC,OAAe;IACtD,OAAO,wBAAwB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AAChD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAe;IACnD,OAAO,wBAAwB,CAAC,OAAO,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,eAAe,CAAC,UAAmB;IACjD,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACvE,MAAM,EAAE,GAAI,UAAsC,CAAC,WAAW,CAAC,CAAC;IAChE,OAAO,OAAO,EAAE,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACnE,CAAC;AAED,iEAAiE;AACjE,mEAAmE;AACnE,MAAM,YAAY,GAAG,oCAAoC,CAAC;AAE1D,4EAA4E;AAC5E,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,MAAM,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACrC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1B,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAC9B,sFAAsF,CAAC;AAIzF;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,uBAAuB,CAAC,YAAqB;IAC3D,IAAI,OAAO,YAAY,KAAK,QAAQ,IAAI,YAAY,KAAK,IAAI;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IACzF,MAAM,EAAE,GAAG,YAAuC,CAAC;IACnD,IAAI,EAAE,CAAC,aAAa,CAAC,KAAK,KAAK;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC3D,MAAM,MAAM,GAAG,OAAO,EAAE,CAAC,QAAQ,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACpE,MAAM,MAAM,GAAG,OAAO,EAAE,CAAC,QAAQ,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACpE,MAAM,QAAQ,GAAG,GAAG,MAAM,KAAK,MAAM,EAAE,CAAC;IACxC,MAAM,KAAK,GAAG,mBAAmB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACjD,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IACtC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;AACjD,CAAC;AAYD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,wBAAwB,CACtC,UAAmB,EACnB,YAAqB,EACrB,OAAe;IAEf,MAAM,QAAQ,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;IAC7C,IAAI,QAAQ,KAAK,CAAC,EAAE,CAAC;QACnB,OAAO;YACL,SAAS,EAAE,IAAI;YACf,QAAQ,EAAE,WAAW;YACrB,EAAE,EAAE,eAAe,CAAC,OAAO,CAAC;YAC5B,MAAM,EAAE,0CAA0C;SACnD,CAAC;IACJ,CAAC;IACD,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,OAAO;YACL,SAAS,EAAE,KAAK;YAChB,QAAQ,EAAE,MAAM;YAChB,EAAE,EAAE,IAAI;YACR,MAAM,EAAE,4BAA4B,QAAQ,gEAAgE;SAC7G,CAAC;IACJ,CAAC;IACD,MAAM,SAAS,GAAG,uBAAuB,CAAC,YAAY,CAAC,CAAC;IACxD,IAAI,SAAS,CAAC,OAAO,EAAE,CAAC;QACtB,OAAO;YACL,SAAS,EAAE,IAAI;YACf,QAAQ,EAAE,iBAAiB;YAC3B,EAAE,EAAE,eAAe,CAAC,OAAO,CAAC,IAAI,SAAS,CAAC,EAAE;YAC5C,MAAM,EAAE,0EAA0E;SACnF,CAAC;IACJ,CAAC;IACD,OAAO;QACL,SAAS,EAAE,KAAK;QAChB,QAAQ,EAAE,MAAM;QAChB,EAAE,EAAE,IAAI;QACR,MAAM,EACJ,sHAAsH;KACzH,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,wBAAwB,CAAC,GAAW;IAClD,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACzC,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,OAAO,qBAAqB,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC;AACzD,CAAC"}
@@ -38,11 +38,14 @@
38
38
  // when the CURRENT branch tip still equals a recorded merged tip
39
39
  // for this repo+branch — an exact sha match, not ancestry, so a new
40
40
  // commit (moving the tip) or a recycled branch name (a different
41
- // tip) both fall silently outside the gate. An ESCAPE ALLOWLIST
42
- // (git switch/checkout/pull/fetch, git branch -d/-D, git stash
43
- // list/show, any `harness ...` command) is checked FIRST, before
44
- // any manifest load or ledger query, so the recovery path can never
45
- // be starved by an unrelated failure.
41
+ // tip) both fall silently outside the gate. GATE-ELIGIBILITY is
42
+ // classified FIRST, before any manifest load or ledger query, so
43
+ // the recovery vocabulary (git switch/checkout/pull/fetch, git
44
+ // branch -d/-D, git stash list/show, any `harness ...` command) can
45
+ // never be starved by an unrelated failure — none of those verbs is
46
+ // a curated mutation. Task 19356be7: DENY WINS when one command
47
+ // chains recovery vocabulary with a curated mutation; the escape
48
+ // list no longer short-circuits the whole command string.
46
49
  //
47
50
  // Fails OPEN when the ledger is unreachable (unlike branch-protection's
48
51
  // fail-closed posture): without the ledger, "merged" and "not merged"
@@ -60,11 +63,14 @@
60
63
  // higher-risk gate; see templates.ts for the rationale), unlike
61
64
  // branch-protection's default-enabled precedent.
62
65
  //
63
- // Claude Code only in v1 (no Codex adapter): both hooks match on the
64
- // `Bash` tool name and classify by command text, a mechanism that has
65
- // no established Codex-adapter parity yet (mirrors `solution-acceptance`,
66
- // which ships with no Codex variant either). `resolve()` warns if wired
67
- // under `runtime: "codex"`.
66
+ // Claude Code only in v1 (no Codex or opencode adapter): both hooks
67
+ // match on the `Bash` tool name and classify by command text, a
68
+ // mechanism that has no established Codex-adapter parity yet (mirrors
69
+ // `solution-acceptance`, which ships with no Codex variant either) and
70
+ // no meaning at all under opencode (no declarative hook/event field to
71
+ // match against). `resolve()` warns whenever `runtime !== "claude-code"`
72
+ // (batch18 fix-round, task f34eb233 — widened from a codex-only check
73
+ // so opencode gets the same warning).
68
74
  import { z } from "zod";
69
75
  import { PolicyUxSchema } from "../../schema/policies.js";
70
76
  import { DEFAULT_RUNTIME } from "../runtime.js";
@@ -126,8 +132,19 @@ function buildHooks() {
126
132
  match: "Bash",
127
133
  command: BLOCKER_COMMAND,
128
134
  blocking: "hard",
129
- budget_ms: 5000,
130
- description: "Blocker: deny curated history-mutating Bash commands (git commit/add/push/merge/rebase/cherry-pick/revert/reset/stash pop|apply, gh pr create/merge) when the current branch tip matches a recorded merged tip. An escape allowlist (git switch/checkout/pull/fetch, git branch -d/-D, git stash list/show, any `harness ...` command) is checked first, unconditionally. Fails open when the ledger is unreachable.",
135
+ // 15000 (task 7bf47554): the blocker's own ledger query
136
+ // (queryLedgerByTag, src/cli/pack/hook-post-merge-gate.ts) is bounded
137
+ // by the grounding-mcp server's own `health.timeout_ms` (default
138
+ // 5000ms) — the same number the old budget_ms=5000 used for the
139
+ // outer Claude Code kill-timeout, leaving no margin. This gate
140
+ // already fails OPEN by design on a genuinely unreachable ledger
141
+ // (unlike branch-protection); raising the outer timeout does not
142
+ // change that posture, it only gives a merely-SLOW (not dead) ledger
143
+ // room to answer before the outer timeout could fire first and kill
144
+ // the subprocess mid-query, consistent with the other ledger-backed
145
+ // pack blockers.
146
+ budget_ms: 15000,
147
+ description: "Blocker: deny curated history-mutating Bash commands (git commit/add/push/merge/rebase/cherry-pick/revert/reset/stash pop|apply, gh pr create/merge) when the current branch tip matches a recorded merged tip. The recovery vocabulary (git switch/checkout/pull/fetch, git branch -d/-D, git stash list/show, any `harness ...` command) always passes as its own command, classified before any manifest or ledger access; chaining one of those with a curated mutation does NOT exempt the mutation. Fails open when the ledger is unreachable.",
131
148
  },
132
149
  ];
133
150
  }
@@ -142,7 +159,11 @@ function buildInstructions(pack, runtime) {
142
159
 
143
160
  ## Runtime
144
161
 
145
- ${runtime}${runtime === "codex" ? " (UNSUPPORTED — see \"Known gaps\" below; both hooks assume the Claude Code Bash tool surface)" : ""}
162
+ ${runtime}${runtime === "codex"
163
+ ? " (UNSUPPORTED — see \"Known gaps\" below; both hooks assume the Claude Code Bash tool surface)"
164
+ : runtime === "opencode"
165
+ ? " (UNSUPPORTED — see \"Known gaps\" below; opencode has no declarative hook/event wiring, so neither hook is projected into any opencode artefact)"
166
+ : ""}
146
167
 
147
168
  ## Trigger + signals
148
169
 
@@ -185,12 +206,17 @@ ${runtime}${runtime === "codex" ? " (UNSUPPORTED — see \"Known gaps\" below; b
185
206
 
186
207
  2. \`PreToolUse\` blocker (\`${BLOCKER_COMMAND}\`, blocking: hard) on
187
208
  \`Bash\`: checked in this order —
188
- a. **Escape allowlist first, unconditionally** (before any manifest
189
- load or ledger query): \`${ESCAPE_GIT_BASH_RE.source}\` OR
190
- \`${ESCAPE_HARNESS_BASH_RE.source}\` — always allowed.
191
- b. **Curated mutation match**: \`${CURATED_MUTATION_BASH_RE.source}\`
192
- — commands that don't match this pass through untouched.
193
- c. **Ledger check**: denies only when the current branch tip exactly
209
+ a. **Gate-eligibility classification first, unconditionally** (before
210
+ any manifest load or ledger query): the curated mutation matcher
211
+ \`${CURATED_MUTATION_BASH_RE.source}\`. Commands it
212
+ does not match pass through untouched — that unconditionally covers the whole recovery
213
+ vocabulary (\`${ESCAPE_GIT_BASH_RE.source}\` and
214
+ \`${ESCAPE_HARNESS_BASH_RE.source}\`), since none of those verbs are
215
+ curated mutations. **Deny wins over the recovery vocabulary** when
216
+ one command chains both (task 19356be7): \`harness preflight &&
217
+ git push\` is judged by its push half, not exempted by its
218
+ preflight half.
219
+ b. **Ledger check**: denies only when the current branch tip exactly
194
220
  equals a recorded \`${MERGED_TAG_PREFIX}:<repo>:<branch>:<sha>\` fact
195
221
  for this repo+branch. A new commit moves the tip (gate falls
196
222
  silent — that's legitimate continued work); a recycled branch name
@@ -204,9 +230,12 @@ ${runtime}${runtime === "codex" ? " (UNSUPPORTED — see \"Known gaps\" below; b
204
230
  \`git branch -d <branch>\` to clean up the merged local branch.
205
231
  - **Read-only / inspection commands** (\`git switch\`/\`checkout\`,
206
232
  \`pull\`/\`fetch\`, \`branch -d\`/\`-D\`, \`stash list\`/\`show\`, and any
207
- \`harness ...\` invocation) always pass, independent of ledger
208
- reachability — the gate can never itself become the reason the
209
- recovery path is unavailable.
233
+ \`harness ...\` invocation) always pass **as their own commands**,
234
+ independent of ledger reachability — the gate can never itself become
235
+ the reason the recovery path is unavailable. Chaining one of them with
236
+ a curated mutation does NOT exempt the mutation (the pre-19356be7
237
+ escape-first ordering did, which let the documented
238
+ preflight-then-push workflow skip the gate entirely).
210
239
 
211
240
  ## Known gaps (documented, not attempted in v1)
212
241
 
@@ -225,13 +254,19 @@ ${runtime}${runtime === "codex" ? " (UNSUPPORTED — see \"Known gaps\" below; b
225
254
  commands on a just-merged branch (e.g. hand-editing files outside
226
255
  git's own history primitives) are out of v1 scope, mirroring
227
256
  branch-protection's documented scope-cut.
228
- - **Chained-escape bypass**: escape-first is checked against the WHOLE
229
- command string, not per-clause, so a curated mutation chained with an
230
- escape verb (e.g. \`git commit -am x && git switch main\`) is fully
231
- ALLOWED — the escape match wins for the entire compound command. Same
232
- accepted class as the regex-vs-shell-eval residual above (\`eval\`,
233
- heredoc, \`sh -c\`); not attempted to close here, and deliberate: the
234
- alternative (per-clause splitting) would need a real shell parser.
257
+ - **Chained-escape bypass — CLOSED (task 19356be7)**: the original
258
+ escape-first ordering matched the escape list against the WHOLE
259
+ command string, so a curated mutation chained with an escape verb
260
+ (\`harness preflight && git push\`, \`git commit -am x && git switch
261
+ main\`) skipped the gate entirely. The blocker now classifies by the
262
+ curated mutation match alone (deny wins; measured 0-divergent from
263
+ per-clause evaluation thanks to the escape/deny verb disjointness —
264
+ escape-at-start-only was measured and rejected, it still exempts three
265
+ of the four lines this closed).
266
+ Residual, measured and accepted: a deny verb as quoted TEXT in a
267
+ chain (\`git pull && echo 'a && git push'\`) is gate-eligible even
268
+ though bash never runs it — the existing accepted false-positive
269
+ class, no longer rescued by a chained escape verb.
235
270
  - **Inverted trust-boundary residual**: a merged fact written by any
236
271
  means OTHER than the producer (e.g. directly via
237
272
  \`mcp__grounding-mcp__ledger_add\`) that happens to exactly match the
@@ -255,6 +290,11 @@ ${runtime}${runtime === "codex" ? " (UNSUPPORTED — see \"Known gaps\" below; b
255
290
  throwaway PR and check for a fresh \`${MERGED_TAG_PREFIX}\` fact in the
256
291
  evidence ledger.
257
292
  - **No Codex adapter**: see "Runtime" above.
293
+ - **No opencode adapter**: see "Runtime" above; opencode has no
294
+ declarative hook/event field (only a JS/TS plugin API), so neither
295
+ the producer nor the blocker hook above is ever projected into the
296
+ generated opencode artefact (\`harness.generated/opencode/opencode.json\`)
297
+ — this pack has no effect at all under \`--runtime opencode\`.
258
298
 
259
299
  ## Fail posture
260
300
 
@@ -283,8 +323,21 @@ export function resolve(pack, runtime = DEFAULT_RUNTIME) {
283
323
  },
284
324
  ];
285
325
  const warnings = [];
286
- if (runtime === "codex") {
287
- warnings.push(`policy_packs[${pack.name}]: post-merge-gate has no Codex adapter in v1; hooks are wired assuming the Claude Code Bash tool surface and will not fire correctly under a Codex runtime.`);
326
+ // HIGH-F1 (batch18 fix-round, task f34eb233): widened from
327
+ // `runtime === "codex"` to `runtime !== "claude-code"` so opencode
328
+ // gets the same pack-level warning codex already got — before this,
329
+ // opencode silently fell through this check (it is neither
330
+ // "claude-code" nor, before the widening, matched by
331
+ // `runtime === "codex"`), losing both the UNSUPPORTED marker above
332
+ // AND this warning even though opencode has no hook wiring at all
333
+ // (strictly worse than codex, whose hooks are at least PROJECTED,
334
+ // just against the wrong tool-name surface).
335
+ if (runtime !== "claude-code") {
336
+ const detail = runtime === "codex"
337
+ ? "hooks are wired assuming the Claude Code Bash tool surface and will not fire correctly under a Codex runtime"
338
+ : "opencode has no declarative hook/event wiring, so neither hook is projected into any opencode artefact and this pack has no effect";
339
+ const adapterName = runtime === "codex" ? "Codex" : "opencode";
340
+ warnings.push(`policy_packs[${pack.name}]: post-merge-gate has no ${adapterName} adapter in v1; ${detail}.`);
288
341
  }
289
342
  return { contribution: { hooks, files }, warnings };
290
343
  }
@@ -1 +1 @@
1
- {"version":3,"file":"post-merge-gate.js","sourceRoot":"","sources":["../../../src/policy-packs/builtin/post-merge-gate.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"post-merge-gate.js","sourceRoot":"","sources":["../../../src/policy-packs/builtin/post-merge-gate.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { type SignatureVerification } from "../../runtime/approval-signing.js";
1
2
  import type { PolicyPack } from "../../schema/index.js";
2
3
  export declare const PACK_NAME = "solution-acceptance";
3
4
  /**
@@ -14,6 +15,19 @@ export declare const DEFAULT_PROTECTED_COMPLETION_TOOLS: readonly ["task_finish"
14
15
  * load-bearing choke points; hardening this is follow-up `7207d8f9`.
15
16
  * Tolerates a leading `cd … &&`, inline `VAR=val` assignments, and `git -C
16
17
  * <path> push`.
18
+ *
19
+ * Task 76671e5a: bare `&` added to the boundary alternation (`&&` kept to
20
+ * its left only to mirror `src/runtime/command-normalize.ts`'s alternation
21
+ * order for readability — NOT because order is load-bearing here, unlike
22
+ * that module). Same fix as `d834a065` applied to every policy trigger —
23
+ * bash starts a new command after a single `&`, so `A=x&git push` used to
24
+ * slip past this DENY matcher entirely. Broadening is the stricter direction
25
+ * here (this gates completion actions, it is not an allow-list). Measured:
26
+ * swapping to `&|&&` produces a byte-identical match set for this matcher
27
+ * (a `RegExp.test` existential check over every start offset, not a
28
+ * segmenter — the reasoning that makes order matter in
29
+ * `command-normalize.ts`'s `BOUNDARY_RE`/`AMP_BOUNDARY_RE` does not transfer
30
+ * to a plain `.test()` matcher like this one).
17
31
  */
18
32
  export declare const DEFAULT_PUSH_BASH_RE: RegExp;
19
33
  /**
@@ -24,7 +38,18 @@ export declare const DEFAULT_PUSH_BASH_RE: RegExp;
24
38
  * surface (mirrors `resolveProtectedBranches` in branch-protection-runtime).
25
39
  */
26
40
  export declare function resolveProtectedCompletionTools(pack: PolicyPack): string[];
27
- /** The verdict marker the producer writes. Keep field-for-field with grounding-mcp. */
41
+ /**
42
+ * The verdict marker the producer writes. Keep field-for-field with
43
+ * grounding-mcp for the first 7 keys (golden-fixture-pinned below).
44
+ *
45
+ * `alg` / `signature` (harness/c7c3f606) are ADDITIVE and OPTIONAL: a
46
+ * legacy / not-yet-updated producer omits them entirely (parsed as
47
+ * `undefined`, never `null` — mirrors how `ApprovalMarker.reportContentHash`
48
+ * distinguishes "absent" from an explicit empty value), and such a verdict
49
+ * is REJECTED by `evaluateGate` (missing signature = forged/unsigned, fail
50
+ * closed) rather than silently accepted. See the module doc for the
51
+ * consumer-only / cross-repo-follow-up honesty.
52
+ */
28
53
  export interface Verdict {
29
54
  id: string;
30
55
  head: string;
@@ -33,7 +58,73 @@ export interface Verdict {
33
58
  blockers: string[];
34
59
  timestamp: string;
35
60
  source: string;
61
+ /** Signature algorithm tag; only `SIGNING_ALG` (`src/runtime/approval-signing.ts`) verifies. */
62
+ alg?: string;
63
+ /** HMAC-SHA256 hex signature over the canonical identifying tuple (see `verifyVerdictSignature`). */
64
+ signature?: string;
36
65
  }
66
+ /**
67
+ * Marker-id namespace the verdict's HMAC signature is bound to (mirrors
68
+ * `BRANCH_PROTECTION_MARKER_PREFIX` in branch-protection-runtime.ts): a
69
+ * validly-signed verdict for one id can never be replayed as a validly-
70
+ * signed verdict — or a validly-signed understanding-gate / branch-
71
+ * protection marker — for a different id, because `markerId` is bound into
72
+ * the signed payload (`approval-signing.ts` `canonicalPayload`) and this
73
+ * prefix keeps the three id spaces disjoint even where the raw ids
74
+ * (task ids, session ids) could otherwise collide.
75
+ */
76
+ export declare const VERDICT_MARKER_ID_PREFIX = "solution-verdict-";
77
+ /** The HMAC markerId for verdict `id` (NOT a filesystem path — see `verdictPathFor` for that). */
78
+ export declare function verdictMarkerId(id: string): string;
79
+ /**
80
+ * Sign `verdict`, reusing `signMarker` (`src/runtime/approval-signing.ts`)
81
+ * unmodified — same crypto as the understanding-gate / branch-protection
82
+ * markers (harness/f9485cc7). Payload mapping mirrors the approval
83
+ * marker's shape (`approvedAt`/`approvedBy`/`reportContentHash`) onto the
84
+ * verdict's own fields:
85
+ * - `approvedAt` <- `verdict.timestamp` (when the attestation was made)
86
+ * - `approvedBy` <- `verdict.source` (what attests it, e.g. "preflight")
87
+ * - `reportContentHash` <- `verdictContentHash(verdict)` (binds
88
+ * head/ready/confidence/blockers, the fields signMarker's fixed shape
89
+ * has no dedicated slot for)
90
+ *
91
+ * NOT wired to any production write path in harness (harness is a pure
92
+ * CONSUMER of this marker, see module doc) — exported as the pattern +
93
+ * exemplar a grounding-mcp-side producer change reuses, and used directly
94
+ * by this module's own tests to construct validly-signed fixtures.
95
+ *
96
+ * Load-bearing on the producer side: `signMarker` is called with
97
+ * `verdictMarkerId(verdict.id)`, while the consumer (`verifyVerdictSignature`
98
+ * below) verifies with `verdictMarkerId(id)` where `id` is the CALLER's id,
99
+ * not `verdict.id`. Those two only agree when a mirroring producer sets
100
+ * `verdict.id` to the EXACT id string the consumer looks the marker up by —
101
+ * byte-identical, no trimming or case normalization on either side — and the
102
+ * consumer additionally rejects outright when `verdict.id !== id` even if
103
+ * the signature itself still verifies (belt-and-braces, see `evaluateGate`).
104
+ */
105
+ export declare function signVerdict(generatedDir: string, verdict: Verdict): Verdict;
106
+ /**
107
+ * Verify `verdict`'s signature, reusing `verifyMarkerSignature` unmodified.
108
+ * Recomputes `verdictContentHash` from the (possibly tampered) verdict
109
+ * being checked — the SAME name-mapping `signVerdict` uses — so any
110
+ * post-signing edit to `head` / `ready` / `confidence` / `blockers` /
111
+ * `timestamp` / `source` fails verification, not just an edit to
112
+ * `signature` itself.
113
+ *
114
+ * `id` is the CALLER's id — the one `evaluateGate` is actually checking
115
+ * (`readVerdict`'s lookup key) — NOT `verdict.id` (a field read back out of
116
+ * the marker BODY, which is exactly the bytes an attacker controls when
117
+ * copying a validly-signed marker verbatim onto a different id's path).
118
+ * This mirrors the load-bearing invariant `approval-signing.ts`'s
119
+ * `signMarker` doc states explicitly: "verification always recomputes the
120
+ * HMAC using the id the caller is checking, NOT one read back out of the
121
+ * marker body." Trusting `verdict.id` here would let a byte-for-byte copy
122
+ * of one id's signed verdict pass verification under a DIFFERENT id's
123
+ * request, because the copy's `signature` and its own (also-copied)
124
+ * `verdict.id` field would always agree with each other regardless of which
125
+ * id actually asked.
126
+ */
127
+ export declare function verifyVerdictSignature(generatedDir: string, id: string, verdict: Verdict): SignatureVerification;
37
128
  /** Env knob that overrides the verdict directory (mirrors the producer). */
38
129
  export declare const VERDICT_DIR_ENV = "SOLUTION_VERDICT_DIR";
39
130
  /**
@@ -91,16 +182,69 @@ export interface GateResult {
91
182
  allowed: boolean;
92
183
  reason: string;
93
184
  verdict: Verdict | null;
185
+ /**
186
+ * True when `allowed` is false SPECIFICALLY because a verdict file
187
+ * existed and parsed (the 7 producer keys), but carried a missing or
188
+ * invalid HMAC signature (harness/c7c3f606), or a body that identifies a
189
+ * different id than the one being checked (cross-id replay, see the
190
+ * `verdict.id !== id` check below) — a legacy/not-yet-signing producer, a
191
+ * marker planted through some write primitive the write-guard doesn't
192
+ * enumerate, or content tampered/copied post-signing. Distinct from
193
+ * `verdict === null` (no marker at all, `forged: false`), from a
194
+ * signing-key I/O failure (fail-closed but NOT classified as forged,
195
+ * mirroring `SignatureVerification`'s `kind: "key-unavailable"` — a
196
+ * broken key file must not read as an active forgery attempt in audit
197
+ * output), and from a verdict with NO `alg`/`signature` fields at all
198
+ * whose `timestamp` / `source` also reads blank (fail-closed but NOT
199
+ * classified as forged either — a legitimately malformed, never-signed
200
+ * marker, not evidence of an attack; see the `MISSING_*_REASON` carve-out
201
+ * below). That last carve-out is scoped tightly: a verdict that DOES carry
202
+ * `alg`/`signature` but still hits the same blank-field reason IS
203
+ * classified `forged: true` — a signed field reading blank only happens
204
+ * via post-signing tampering, so it does not get the "legitimately
205
+ * malformed" benefit of the doubt (review R2, closes an audit-fidelity
206
+ * gap where a forger could blank a real signed verdict's `timestamp` to
207
+ * suppress this tag while the block itself still held). `allowed` is
208
+ * always false when `forged` is true.
209
+ */
210
+ forged: boolean;
94
211
  }
95
212
  /**
96
- * Evaluate the gate for `id` at `currentHead`. Mirrors grounding-mcp
97
- * `evaluateGate` EXACTLY: allow iff `verdict.ready === true` AND
98
- * `verdict.head === currentHead`. `confidence` is INFORMATIONAL ONLY and
99
- * never gates — a `ready:true confidence:0` verdict at HEAD passes — so the
100
- * harness consumer stays byte-parity with the producer's `solution_gate`
101
- * (an operator running `solution_gate` and the harness gate must agree).
213
+ * Evaluate the gate for `id` at `currentHead`. First verifies the verdict's
214
+ * HMAC signature (harness/c7c3f606, fail-closed — see `verifyVerdictSignature`);
215
+ * a verdict that fails this is REJECTED before its `ready` / `head` fields
216
+ * are ever trusted, with a distinct forged/unsigned reason so an
217
+ * operator/auditor can tell that apart from the routine "not ready yet" or
218
+ * "no verdict" cases. Signature verification needs `generatedDir` (the
219
+ * harness `.generated/` dir holding the shared signing key, NOT the verdict
220
+ * dir) — `undefined` when it could not be resolved, which fails closed with
221
+ * its own distinct (non-forged) reason.
222
+ *
223
+ * `verifyVerdictSignature` is called with `id` — the CALLER's id, i.e. the
224
+ * one this function is asked to check — not `verdict.id`. A SECOND,
225
+ * belt-and-braces check below independently rejects `verdict.id !== id`:
226
+ * even though the signature check alone already closes the cross-id-replay
227
+ * hole (a byte-for-byte copy of a validly-signed verdict onto a different
228
+ * id's path fails signature verification, because the markerId the
229
+ * signature is bound to no longer matches `id`), the verdict body's `id`
230
+ * field itself is NOT part of the signed payload (only `timestamp` /
231
+ * `source` / the head-ready-confidence-blockers content hash are), so it
232
+ * can drift from the id that was actually verified without invalidating an
233
+ * otherwise-valid signature. This check makes that drift fail loudly too,
234
+ * rather than silently ignoring an inconsistency in the body of a verdict
235
+ * that otherwise passed.
236
+ *
237
+ * Once signed and identity-checked, mirrors grounding-mcp `evaluateGate`:
238
+ * allow iff `verdict.ready === true` AND `verdict.head === currentHead`.
239
+ * `confidence` is INFORMATIONAL ONLY and never gates — a `ready:true
240
+ * confidence:0` verdict at HEAD passes — so the harness consumer stays
241
+ * byte-parity with the producer's `solution_gate` for THAT decision.
242
+ * Signature verification itself is NOT (yet) mirrored on the producer side
243
+ * — see module doc, "HONEST RESIDUAL": grounding-mcp's own `solution_gate`
244
+ * does not enforce this signature, only this harness consumer does, until a
245
+ * matching producer release ships.
102
246
  */
103
- export declare function evaluateGate(verdict: Verdict | null, currentHead: string | null, id: string): GateResult;
247
+ export declare function evaluateGate(verdict: Verdict | null, currentHead: string | null, id: string, generatedDir: string | undefined): GateResult;
104
248
  /**
105
249
  * Is `target` inside `dir` after resolution? Used for the path-tool arm
106
250
  * (Write/Edit/MultiEdit/NotebookEdit `file_path`) and for a Bash shell whose