@pareto4/pp-core 1.13.0 → 1.18.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 (204) hide show
  1. package/.claude-plugin/plugin.json +3 -3
  2. package/README.md +31 -12
  3. package/agents/pp-code-fixer.md +0 -11
  4. package/agents/pp-code-reviewer.md +1 -77
  5. package/agents/pp-debug-session-manager.md +9 -6
  6. package/agents/pp-executor.md +12 -29
  7. package/agents/pp-plan-checker.md +4 -4
  8. package/agents/pp-planner.md +34 -20
  9. package/agents/pp-vault-reviewer.md +100 -23
  10. package/agents/pp-verifier.md +1 -3
  11. package/bin/install.js +10 -3
  12. package/commands/pp-execute-phase.md +1 -3
  13. package/commands/pp-ns-manage.md +1 -1
  14. package/commands/pp-ns-review.md +1 -1
  15. package/commands/pp-plan-phase.md +1 -2
  16. package/commands/pp-vault-promote.md +7 -8
  17. package/commands/pp-vault-review.md +6 -5
  18. package/commands/pp-verify-work.md +1 -2
  19. package/gemini-extension.json +1 -1
  20. package/hooks/dist/pp-check-update.js +0 -0
  21. package/hooks/dist/pp-cursor-session-start.js +1 -1
  22. package/hooks/dist/pp-statusline.js +1 -1
  23. package/hooks/dist/pp-update-banner.js +0 -0
  24. package/hooks/pp-check-update.js +0 -0
  25. package/hooks/pp-cursor-session-start.js +1 -1
  26. package/hooks/pp-statusline.js +1 -1
  27. package/hooks/pp-update-banner.js +0 -0
  28. package/package.json +10 -18
  29. package/pareto-skills/_shared/create-project-board.sh +0 -0
  30. package/pareto-skills/pp-project-setup/SKILL.md +1 -1
  31. package/pp-core/bin/check-latest-version.cjs +0 -0
  32. package/pp-core/bin/detect-build-commands.sh +2 -2
  33. package/pp-core/bin/lib/capability-activation.cjs +83 -9
  34. package/pp-core/bin/lib/capability-registry.cjs +201 -229
  35. package/pp-core/bin/lib/capability-state.cjs +93 -1
  36. package/pp-core/bin/lib/capability-writer.cjs +3 -3
  37. package/pp-core/bin/lib/check-command-router.cjs +1906 -1514
  38. package/pp-core/bin/lib/clusters.cjs +3 -0
  39. package/pp-core/bin/lib/command-aliases.cjs +0 -35
  40. package/pp-core/bin/lib/commands.cjs +9 -4
  41. package/pp-core/bin/lib/compose-bindings.cjs +251 -88
  42. package/pp-core/bin/lib/config-loader.cjs +7 -103
  43. package/pp-core/bin/lib/config-schema.cjs +1 -1
  44. package/pp-core/bin/lib/config.cjs +151 -62
  45. package/pp-core/bin/lib/decisions.cjs +4 -4
  46. package/pp-core/bin/lib/discovery-vault-check.cjs +246 -0
  47. package/pp-core/bin/lib/frontmatter.cjs +1 -38
  48. package/pp-core/bin/lib/git-base-branch.cjs +11 -12
  49. package/pp-core/bin/lib/git-provider.cjs +9 -10
  50. package/pp-core/bin/lib/init-command-router.cjs +5 -8
  51. package/pp-core/bin/lib/init-contract.cjs +620 -738
  52. package/pp-core/bin/lib/init-interactive.cjs +62 -4
  53. package/pp-core/bin/lib/init.cjs +24 -51
  54. package/pp-core/bin/lib/install-profiles.cjs +1 -1
  55. package/pp-core/bin/lib/io.cjs +4 -2
  56. package/pp-core/bin/lib/loop-resolver.cjs +165 -129
  57. package/pp-core/bin/lib/org-profile.cjs +402 -161
  58. package/pp-core/bin/lib/package-identity.cjs +3 -3
  59. package/pp-core/bin/lib/phase-command-router.cjs +1 -29
  60. package/pp-core/bin/lib/phase-scope.cjs +1 -1
  61. package/pp-core/bin/lib/phase.cjs +11 -253
  62. package/pp-core/bin/lib/platform-bindings.cjs +40 -19
  63. package/pp-core/bin/lib/profile-output.cjs +1 -1
  64. package/pp-core/bin/lib/shell-command-projection.cjs +104 -1
  65. package/pp-core/bin/lib/state.cjs +11 -85
  66. package/pp-core/bin/lib/template.cjs +1 -1
  67. package/pp-core/bin/lib/vault-command-router.cjs +1682 -477
  68. package/pp-core/bin/lib/vault-coordinates.cjs +16 -7
  69. package/pp-core/bin/lib/vault-index.cjs +656 -0
  70. package/pp-core/bin/lib/vault-seal.cjs +1851 -0
  71. package/pp-core/bin/lib/vault-standards-promote.cjs +577 -0
  72. package/pp-core/bin/lib/vault-standards-review.cjs +293 -0
  73. package/pp-core/bin/lib/vault-standards.cjs +1504 -0
  74. package/pp-core/bin/lib/vault-trace.cjs +155 -2778
  75. package/pp-core/bin/lib/vault-update.cjs +10 -59
  76. package/pp-core/bin/lib/vault.cjs +2570 -950
  77. package/pp-core/bin/lib/verify.cjs +0 -11
  78. package/pp-core/bin/portable-timeout.sh +1 -1
  79. package/pp-core/bin/pp-tools.cjs +157 -54
  80. package/pp-core/bin/shared/config-defaults.manifest.json +0 -7
  81. package/pp-core/bin/shared/config-schema.manifest.json +0 -3
  82. package/pp-core/bin/shared/model-catalog.json +2 -2
  83. package/pp-core/bin/verify-reapply-patches.cjs +0 -0
  84. package/pp-core/org-profile.json +26 -0
  85. package/pp-core/references/autonomous-best-effort.md +8 -7
  86. package/pp-core/references/loop-hook-dispatch.md +0 -25
  87. package/pp-core/references/planner-gap-closure.md +0 -1
  88. package/pp-core/references/planner-tdd.md +0 -1
  89. package/pp-core/references/planner-vault-standards.md +311 -0
  90. package/pp-core/references/planning-config.md +0 -8
  91. package/pp-core/references/tdd.md +0 -1
  92. package/pp-core/templates/README.md +1 -0
  93. package/pp-core/templates/VAULT-REVIEW.md +56 -0
  94. package/pp-core/templates/phase-prompt.md +2 -6
  95. package/pp-core/workflows/_runtime-launcher.snippet.sh +0 -0
  96. package/pp-core/workflows/add-tests.md +55 -4
  97. package/pp-core/workflows/audit-fix.md +63 -2
  98. package/pp-core/workflows/audit-milestone.md +1 -1
  99. package/pp-core/workflows/autonomous.md +85 -11
  100. package/pp-core/workflows/check-todos.md +13 -2
  101. package/pp-core/workflows/code-review-fix.md +59 -3
  102. package/pp-core/workflows/code-review.md +10 -57
  103. package/pp-core/workflows/complete-milestone.md +1 -1
  104. package/pp-core/workflows/debug.md +54 -4
  105. package/pp-core/workflows/discovery-sprint/prefill.md +19 -7
  106. package/pp-core/workflows/discovery-sprint/prototype.md +46 -49
  107. package/pp-core/workflows/discovery-sprint/session-13-gates.md +2 -0
  108. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +1 -3
  109. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +1 -3
  110. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +1 -3
  111. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +20 -24
  112. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +1 -3
  113. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +1 -3
  114. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +1 -3
  115. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +1 -3
  116. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +1 -3
  117. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +1 -6
  118. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +1 -3
  119. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +1 -3
  120. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -3
  121. package/pp-core/workflows/discovery-sprint/templates/context-template.md +1 -1
  122. package/pp-core/workflows/discovery-sprint/vault-context.md +207 -0
  123. package/pp-core/workflows/discovery-sprint.md +7 -3
  124. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +4 -4
  125. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +43 -1
  126. package/pp-core/workflows/discovery-to-build.md +10 -2
  127. package/pp-core/workflows/discuss-phase.md +3 -10
  128. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +327 -0
  129. package/pp-core/workflows/execute-phase/steps/client-context-gate.md +3 -2
  130. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -1
  131. package/pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md +78 -0
  132. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
  133. package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +6 -8
  134. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +115 -14
  135. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +219 -20
  136. package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +147 -0
  137. package/pp-core/workflows/execute-phase.md +152 -128
  138. package/pp-core/workflows/execute-plan.md +54 -6
  139. package/pp-core/workflows/fast.md +53 -4
  140. package/pp-core/workflows/help/modes/full.md +3 -3
  141. package/pp-core/workflows/init/steps/client-context-capture.md +101 -0
  142. package/pp-core/workflows/init/steps/gitlab-provisioning.md +130 -0
  143. package/pp-core/workflows/init/steps/project-md-deferral-narration.md +66 -0
  144. package/pp-core/workflows/init/steps/reachability-outcome.md +70 -0
  145. package/pp-core/workflows/init.md +232 -272
  146. package/pp-core/workflows/new-project.md +31 -23
  147. package/pp-core/workflows/plan-phase.md +98 -45
  148. package/pp-core/workflows/quick.md +117 -3
  149. package/pp-core/workflows/secure-phase.md +1 -1
  150. package/pp-core/workflows/ship.md +57 -28
  151. package/pp-core/workflows/undo.md +1 -1
  152. package/pp-core/workflows/update.md +3 -3
  153. package/pp-core/workflows/validate-phase.md +58 -3
  154. package/pp-core/workflows/vault-review.md +161 -23
  155. package/pp-core/workflows/vault-write-back.md +73 -110
  156. package/pp-core/workflows/verify-work.md +12 -14
  157. package/scripts/changeset/cli.cjs +6 -5
  158. package/scripts/changeset/lint.cjs +0 -0
  159. package/scripts/changeset/new.cjs +33 -50
  160. package/scripts/dev-env.cjs +25 -85
  161. package/scripts/gen-capability-registry.cjs +120 -13
  162. package/scripts/lib/allowlist-ratchet.cjs +1 -103
  163. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -2
  164. package/scripts/lint-docs-required.cjs +0 -0
  165. package/scripts/lint-pr-check-project-dir.cjs +1 -3
  166. package/scripts/lint-test-file-count.allowlist.json +3 -19
  167. package/scripts/lint-test-file-count.cjs +5 -6
  168. package/scripts/prompt-injection-scan.sh +19 -17
  169. package/scripts/release-channel.cjs +0 -0
  170. package/scripts/sync-manifest-versions.cjs +5 -4
  171. package/scripts/sync-rulesets.sh +1 -1
  172. package/scripts/update-size-baseline.cjs +30 -274
  173. package/scripts/verify-opengsd-sweep.cjs +3 -5
  174. package/scripts/workflow-size.cjs +9 -61
  175. package/pp-core/bin/lib/dispatch-plan.cjs +0 -483
  176. package/pp-core/bin/lib/install-run.cjs +0 -536
  177. package/pp-core/bin/lib/loop-gates.cjs +0 -213
  178. package/pp-core/bin/lib/phase-manifest.cjs +0 -527
  179. package/pp-core/bin/lib/phase-tier.cjs +0 -480
  180. package/pp-core/bin/lib/plan-graph.cjs +0 -368
  181. package/pp-core/bin/lib/practices-review-check.cjs +0 -938
  182. package/pp-core/bin/lib/vault-audit.cjs +0 -2070
  183. package/pp-core/bin/lib/vault-practices.cjs +0 -1990
  184. package/pp-core/bin/lib/vault-review.cjs +0 -2359
  185. package/pp-core/bin/lib/vault-write-back.cjs +0 -877
  186. package/pp-core/references/executor-vault-trace.md +0 -47
  187. package/pp-core/references/planner-plan-graph.md +0 -66
  188. package/pp-core/references/planner-vault-practices.md +0 -112
  189. package/pp-core/templates/practices/README.md +0 -251
  190. package/pp-core/templates/practices/error-handling.md +0 -18
  191. package/pp-core/templates/practices/minimal-comments.md +0 -14
  192. package/pp-core/templates/practices/naming-convention.md +0 -18
  193. package/pp-core/templates/practices/practice-template.md +0 -23
  194. package/pp-core/workflows/discovery-sprint/vault-fetch.md +0 -37
  195. package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +0 -45
  196. package/pp-core/workflows/execute-phase/steps/install-run.md +0 -29
  197. package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +0 -23
  198. package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +0 -73
  199. package/pp-core/workflows/execute-phase/steps/vault-door-gate.md +0 -19
  200. package/pp-core/workflows/plan-phase/steps/phase-tier.md +0 -43
  201. package/scripts/lib/size-budget.cjs +0 -311
  202. package/scripts/lint-neutrality-names.cjs +0 -694
  203. package/scripts/lint-org-coordinates.allowlist.json +0 -35
  204. package/scripts/lint-org-coordinates.cjs +0 -426
@@ -0,0 +1,327 @@
1
+ # Generic `check.query` gate enforcer
2
+
3
+ **Naming note (read this first).** This file lives under `pp-core/workflows/execute-phase/steps/`
4
+ because that directory is the established home for lazily-Read workflow step fragments —
5
+ `plan-phase.md` already Reads two of its siblings across a workflow boundary, and creating a new
6
+ top-level directory has unverified installer implications. The directory name says *execute-phase*,
7
+ but this file serves **ship**, **plan** and **execute** call sites alike. Do not read the path as a
8
+ scope restriction.
9
+
10
+ This is the **single** generic `check.query` gate enforcer. Before it existed the same enforcement
11
+ loop was written twice — `ship.md` step 6 and `execute-phase.md` step 5.75 — against two contracts
12
+ that disagreed about which envelope field carries the verdict. GATE-02 collapses them into this one
13
+ implementation; **D-VH-7** fixes which contract survives: the two-step one from step 5.75, reading
14
+ the boolean `block`, not `status == "block"`.
15
+
16
+ This file is **Read lazily** by its callers. It is never eagerly imported, so it costs its callers
17
+ nothing until the moment a gate actually has to be enforced.
18
+
19
+ ## Parameters — set these before Reading this file
20
+
21
+ | Variable | Required | Meaning |
22
+ |----------|----------|---------|
23
+ | `GATE_POINT` | yes | Loop point label used in operator messages, e.g. `ship:pre`, `execute:pre`, `execute:wave:post`. Message text only; it never changes control flow. |
24
+ | `GATE_HOOKS_JSON` | yes (hook mode) | The already-rendered hooks JSON from the caller's own `pp_run loop render-hooks <point> --raw` call. This file never renders hooks itself. **`GATE_HOOKS_JSON=$X` — in a caller's prose and in the Caller contract below — means the JSON on the `X=` line that caller's render fence PRINTED** (16 review iteration 2 CR-01, D-99): the hooks are read in-context, and the agent's Bash tool starts a fresh shell for every call, so a payload a fence only assigned is invisible, and the guard below would skip a blocking gate silently. Every render fence that feeds this enforcer therefore prints `X={"activeHooks":[…]}` (without the large `rendered` text), and a test fails when one does not. |
25
+ | `GATE_POSITIONAL_ARG` | no | When non-empty, passed to `pp_run check` as a single positional argument after the query name. This is how the wave gate passes the phase number. |
26
+ | `GATE_POSITIONAL_ARG_2` | no | When non-empty, passed to `pp_run check` as a **second** positional argument, after `GATE_POSITIONAL_ARG` and in that order. The `plan:post` caller in `plan-phase.md` §13e uses it to pass the requirement-ID list after the phase directory, which is what `gap-analysis.plan-post` has always received in its fourth argument slot. Setting this while leaving `GATE_POSITIONAL_ARG` empty is a **caller error**: the enforcer passes them positionally in order, so an absent first argument silently shifts this one into the first slot. |
27
+ | `GATE_WS` | no | The active workstream name (the value of `--ws`), empty in flat mode. When non-empty the enforcer appends `--ws "$GATE_WS"` to the check command, because a phase **number** positional cannot be resolved without it on a partitioned project: the locator falls back to the flat `.planning/phases`, which does not exist there, and the handler reports the phase as unresolvable. Handlers are workstream-aware; historically only the call sites were not, and an omitted `--ws` here turned a `blocking: true` gate into a hard halt on every workstream project. Callers passing a phase **directory** in `GATE_POSITIONAL_ARG` do not need it — the path already carries the partition — but setting it is harmless and correct. |
28
+ | `GATE_STAGE` | no | The lifecycle stage the dispatching point represents — `plan` or `execute`. When non-empty the enforcer appends `--stage "$GATE_STAGE"` to the check command. A query that does not understand the flag ignores it, because the flag pair is appended **after** both positionals and therefore never occupies a positional slot. `plan:post` sets it to `plan`, so the trace gate validates that point's own artifacts — the PLAN.md files — and not the SUMMARY.md files of a previous execution. `execute:post` deliberately leaves it **unset**, so that point validates both stages, which is the documented invariant: `plan:post` sees PLAN.md files only, `execute:post` sees both and the PLAN.md side is already known good. |
29
+ | `GATE_UNPAIRED` | no | Whether a plan artifact carrying no executed counterpart is a defect — `strict` or `tolerant`. When non-empty the enforcer appends `--unpaired "$GATE_UNPAIRED"` to the check command; `vault.trace` reads it (04-07, D-10) and every other query ignores an unrecognised named flag, because the pair is appended **after** both positionals and therefore never occupies a positional slot. **The default when the variable is absent is `tolerant`, and that default is deliberate**: two of the four `vault.trace` call sites are mid-lifecycle — `plan:post` runs when no plan has been executed yet, and `execute:wave:post` runs while later waves are still pending — so at those points an unpaired plan is the normal state of a correct run, not a fault. A `strict` default would turn a forgotten token at either of them into a continuous halt on every project that has the capability on. The two points that DO set `strict` are `ship:pre` and `verify:post`, which is the same claim from the other side: those are the two points at which every plan must already have an executed counterpart, so an unpaired plan there is a real defect. `verify:post` qualifies that with the run mode — it sets `strict` only when the host's `WAVE_FILTER` is unset, because a `--wave N` run is the third mid-lifecycle case (04-12, CR-04). |
30
+ | `GATE_DIRECT_QUERY` | no | Direct-query mode (see below) for build entry points that declare no loop-host frontmatter. Only consulted when `GATE_HOOKS_JSON` is empty. |
31
+
32
+ `GATE_POSITIONAL_ARG` may also name one `*-PLAN.md` file: `check vault.trace` then judges that plan's
33
+ records alone (16-05, D-38), and every other query refuses a file. `execute-plan.md`'s `plan:post`
34
+ row below is the caller that passes one.
35
+
36
+ ## Producer audit — every declared `check.query` in the repo
37
+
38
+ Enumerated from `capabilities/*/capability.json` (`grep -rho '"query": *"[^"]*"' capabilities/*/capability.json | sort -u`)
39
+ and traced to its handler branch in `src/check-command-router.cts`. The router normalises dots to
40
+ hyphens, so `verify.schema-drift` routes to the `verify-schema-drift` branch.
41
+
42
+ **D-08-30 — the citations stay line numbers, and that is a decision rather than an inheritance.**
43
+ Every row below rots on ANY line change to the router, and this phase watched it happen twice inside
44
+ a single plan (08-02) and three more times across the wave. Two alternatives were weighed: citing the
45
+ emitting *expressions* instead (unstable in a different way — the same literal `block: true,` appears
46
+ five times in `cmdVaultTrace` alone, so a citation could not name one), and dropping the column
47
+ (which is what the rows were BEFORE 04-16, and is exactly how one of them came to claim a handler
48
+ emitted a constant `false` for three plans after it stopped doing so). Line numbers are kept because
49
+ the probe at `tests/vault-trace.integration.test.cjs:9235,9258` (`handlerRange`, `emitsBlock`) is TWO-DIRECTIONAL: a rotted citation is a
50
+ loud red on the next run, never a silent lie, and the completeness half also forbids a row that names
51
+ a convenient subset. The rot is real; it is now the cheapest kind — a mechanically re-derivable one.
52
+ Re-derive with the probe's own `handlerRange`/`emitsBlock` predicates; never by arithmetic on the
53
+ old numbers, which is how 05-13 shifted three rows ~200 lines into a different function.
54
+
55
+ | Query | Handler branch / function | Emits boolean `block` on every path? | Emission site(s) |
56
+ |-------|---------------------------|--------------------------------------|------------------|
57
+ | `gap-analysis.plan-post` | `gap-analysis-plan-post` branch (`cmdGapAnalysisPlanPost`) | yes — always `false` (advisory by design) | `src/check-command-router.cts:1067` |
58
+ | `ship.platform-drift` | `ship-platform-drift` branch (`cmdShipPlatformDrift`) | yes — the only producer that can emit `block: true` from a threshold comparison | `src/check-command-router.cts:1169,1179,1188,1205,1247,1256,1270` |
59
+ | `tdd.review-checkpoint` | `tdd-review-checkpoint` branch | yes — `block = violations > 0`, advisory in the capability | `src/check-command-router.cts:928,1022` |
60
+ | `ui.plan-gate` | `ui-plan-gate` branch (`computeUiPlanGate`) | yes — `block = frontend && !hasUiSpec` | `src/check-command-router.cts:703` (emitted at `:727`) |
61
+ | `ui.safety-gate` | `ui-safety-gate` branch (`computeUiSafetyGate`) | yes — `block = frontend && hasUiFiles && !hasUiSpec` | `src/check-command-router.cts:830` (emitted at `:856`) |
62
+ | `vault.status` | `vault-status` branch (`cmdVaultStatus`) | yes — COMPUTED, not constant: `block` is the `vault.required` policy applied to the shared tier-keyed verdict leaf (`computeVaultVerdict`, GATE-01 / D-VH-4). It is `false` with the policy off (STRICT-01's byte-identical promise), on the inert path (`.platform-bindings.json` absent — that branch runs FIRST by requirement, T-02-28, because `tier !== 'client'` is true of `undefined` and would otherwise block every non-Pareto project), and for a present, fresh, client-tier vault; it is `true` on a required miss, on a fallback-tier load, on a stale client vault whatever its source (D-08-01; the block skips staleness only when `vault.cache_ttl_hours` is 0, D-08-28), and on a client cache fetched from a ref the project no longer configures (D-08-23). The attributed override (`applyVaultOverride`, GATE-07) then gets the last word on every blocking path, and that overwrite is the last emission site cited; and on a passing required-on verdict the envelope now carries a `message` (the success line, D-08-20) — the enforcer's every-run passthrough prints it. | `src/check-command-router.cts:1626,1641,1680,1707,1768` (02-11: CHECKED by the same two-directional probe as the other rows — every cited line lies inside `cmdVaultStatus` and emits a block, and every emission site is cited. This was the one row the probe did NOT cover, so it is the row that rotted: wave 1 wrote the claim before 02-06 computed the block, and 05-13 shifted the citations arithmetically into the manifest-sanitiser, ~200 lines outside the handler) 14-06: + the envelope always carries `reason`, `status` may be `degraded` (index missing / unparseable / empty / unresolvable / unchecked, D-12), the flat index fields follow the override keys, and the healthy message is the one-line standards card (D-15); a degraded index blocks exactly when `vault.required` is on, through the same override (D-13), so it adds no emission site of its own. |
63
+ | `vault.trace` | `vault-trace` branch (`cmdVaultTrace`) — declared by the `vault` capability (`when: vault.enabled`) at **four** points, `plan:post`, `execute:post`, `ship:pre` and `verify:post`, all `blocking: true` / `onError: halt`, and once more at `ship:pre` as a seal gate (`onRecordedArtifacts: true`) that renders with `vault.enabled` off over a phase holding vault records (16-08). It judges each PLAN's `<vault_standards>` block and each SUMMARY's `## Standards Consulted` section through `src/vault-standards.cts` and fails closed on the `standards_*` kinds (`standards_block_absent`, `standards_block_duplicate`, `standards_block_unterminated`, `standards_block_malformed`, `standards_reason_invalid`, `standards_document_duplicate`, `standards_degraded_mixed`, `standards_none_without_reason`, `standards_path_not_resolved`, `standards_pin_mismatch`, `standards_cache_unreadable`, `standards_degraded_untrue`, `standards_consulted_absent`, `standards_consulted_mismatch`, `standards_plan_id_mismatch`, `standards_pair_missing`, `standards_orphan_summary`, `standards_anchor_malformed`). Reads `--stage` (04-03) and `--unpaired` (04-07, D-10); `ship:pre` sets `GATE_UNPAIRED=strict` unconditionally, `verify:post` sets it only on a full-phase run and leaves it unset when **either** partial-execution mode is active — the host's `WAVE_FILTER` set, or `--gaps-only` (04-12, CR-04; 04-18, WR-01) — and the other two leave the tolerant default | yes — `false` before any policy is read (inert), `false` with status `inactive` when `vault.enabled` is off and the phase holds no vault records, `true` once a standards record fails to validate; an agreeing degraded record with `vault.required` off is `false` with a message (D-29), and a recorded override is `false` at the tolerant points and `true` at the strict ones; `true` with status `disabled_with_artifacts` when `vault.enabled` is off and the positional's phase (or plan file) holds engine-written vault records (16-09, SEAL-02, D-21) — the answer the `ship:pre` seal gate dispatches for | `src/check-command-router.cts:2982,2994,2997,3029,3060,3085,3115,3141,3416` (04-16: CHECKED — a test asserts every line cited here lies inside `cmdVaultTrace` and carries a `block` emission, because four plans in this pass moved these numbers and nothing noticed). 16-07 (SEAL-01): at the strict points only, `appendSealProblems` compares the phase's recorded end anchors with `HEAD` through bounded git and adds `uncovered_source_commit` (a source commit after the ends that no record's start-end range covers), `anchor_missing`, `anchor_start_missing`, `anchor_unknown`, `anchor_not_ancestor` and `seal_could_not_check` (all blocking) and the `anchor_backfilled` note; it pushes problems into `checked` and owns no emission site and no status. 16-09 (SEAL-02): + the `disabled_with_artifacts` emission inside the switch-off guard. Standards-only vault: re-derived with the probe's own `handlerRange`/`emitsBlock` after the second record family, the invalid-config refusal and the envelope's mode tag left the handler. |
64
+ | `verify.codebase-drift` | `verify-codebase-drift` branch (`verify.cmdVerifyCodebaseDrift`) | yes — `block = action_required` | `src/verify.cts:2134,2149,2164,2186,2231,2246` |
65
+ | `verify.schema-drift` | `verify-schema-drift` branch (`verify.cmdVerifySchemaDrift`) | yes — `block = !skipped && driftDetected` | `src/verify.cts:2051,2071,2108` |
66
+
67
+ **Footnote — the missing-field rule.** Every producer above emits a boolean `block` on every
68
+ *successful* path today, but that is an observation, not a guarantee: a producer added later, or an
69
+ `error()` early-return on a missing positional argument, can reach a caller with no `block` field at
70
+ all. Therefore the verdict read below (in step 1's fence, printed for step 2) treats a **missing or
71
+ non-boolean `block` as `false` (advisory)**. An
72
+ absent field must never be coerced into a halt — a gate that stops the build because a producer
73
+ forgot a key is a worse failure than the one it was trying to prevent. Producers that reach the
74
+ enforcer through `error()` are *command* errors and are handled by step 1, never by step 2.
75
+
76
+ ## Guard — inactive capability, zero work
77
+
78
+ Write this branch first, because it is what makes an inactive capability **structurally inert**:
79
+
80
+ - If `GATE_HOOKS_JSON` is empty, unset, or its `activeHooks` array is missing or empty **and**
81
+ `GATE_DIRECT_QUERY` is unset, **skip silently and return to the caller.** Print nothing.
82
+
83
+ A project whose capability is off never reaches any of the logic below. That is the whole inertness
84
+ promise — not a suppressed message, an unreached code path.
85
+
86
+ ## The loop
87
+
88
+ Process each entry of `activeHooks` where `kind == "gate"` **and** a `check.query` is present, in
89
+ array order. Read `activeHooks` in-context; do not pipe it through a shell parser.
90
+
91
+ A caller that handles a specific predicate itself — `ship.md` handles a `security` predicate branch
92
+ above its generic loop — is expected to have handled or skipped that entry **before** Reading this
93
+ file. This enforcer never sees it.
94
+
95
+ For each hook, bind `GATE_QUERY` to `hook.check.query`, `GATE_BLOCKING` to `hook.blocking`, and
96
+ `GATE_ONERROR` to `hook.onError` (absent means `skip`).
97
+
98
+ ## Step 1 — did the check COMMAND succeed?
99
+
100
+ ```bash
101
+ _PP_SHIM_NAME="pp-tools.cjs"; _pp_root() { ( for _v in $(env | sed -n 's/^\(GIT_[A-Za-z0-9_]*\)=.*/\1/p'); do unset "$_v"; done; _c=$(git rev-parse --git-common-dir 2>/dev/null); if [ "${_c##*/}" = .git ] && cd "$_c/.." 2>/dev/null; then pwd -P; else git rev-parse --show-toplevel 2>/dev/null || pwd; fi ); }; pp_run() { case "$PP_TOOLS" in *.cjs) node "$PP_TOOLS" "$@";; *) "$PP_TOOLS" "$@";; esac; }; _PP_RUNTIME_ROOT="${RUNTIME_DIR:-$(_pp_root)}"; PP_TOOLS="${_PP_RUNTIME_ROOT}/pp-core/bin/${_PP_SHIM_NAME}"; if [ -f "$PP_TOOLS" ]; then :; elif [ -f "${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}"; elif command -v pp-tools >/dev/null 2>&1; then PP_TOOLS="$(command -v pp-tools)"; else echo "ERROR: pp-tools.cjs not found at $PP_TOOLS and pp-tools is not on PATH. Run: npx -y @pareto4/pp-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${PP_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${PP_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
102
+ GATE_RESULT=$(pp_run check "${GATE_QUERY}" ${GATE_POSITIONAL_ARG:+"${GATE_POSITIONAL_ARG}"} ${GATE_POSITIONAL_ARG_2:+"${GATE_POSITIONAL_ARG_2}"} ${GATE_WS:+--ws} ${GATE_WS:+"${GATE_WS}"} ${GATE_STAGE:+--stage} ${GATE_STAGE:+"${GATE_STAGE}"} ${GATE_UNPAIRED:+--unpaired} ${GATE_UNPAIRED:+"${GATE_UNPAIRED}"} --raw)
103
+ CHECK_EXIT=$?
104
+ if [ "$CHECK_EXIT" -ne 0 ] || [ -z "$GATE_RESULT" ] || ! printf '%s' "$GATE_RESULT" | node -e "const j=JSON.parse(require('fs').readFileSync(0,'utf-8'));if(j===null||typeof j!=='object')process.exit(1)" 2>/dev/null; then
105
+ echo "GATE_CHECK=failed"
106
+ else
107
+ GATE_BLOCK=$(printf '%s' "$GATE_RESULT" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{process.stdout.write(String(JSON.parse(d).block===true));}catch{process.stdout.write('true');}")
108
+ GATE_MESSAGE=$(printf '%s' "$GATE_RESULT" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const m=JSON.parse(d).message;process.stdout.write(typeof m==='string'?m.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u0084\u0086-\u009F]/g,' ').replace(/\r\n|[\r\n\u0085\u2028\u2029]/g,'\n '):'');}catch{process.stdout.write('');}")
109
+ echo "GATE_CHECK=ok"
110
+ echo "GATE_BLOCK=$GATE_BLOCK"
111
+ if [ -n "$GATE_MESSAGE" ]; then printf 'GATE_MESSAGE=%s\n' "$GATE_MESSAGE"; fi
112
+ fi
113
+ ```
114
+
115
+ Capture `$?` on the line immediately after the assignment — any command in between overwrites it.
116
+
117
+ **This one fence runs the check AND reads its verdict, and it prints everything the prose below
118
+ routes on** (16 review W-1, D-97). It prints exactly one of two shapes:
119
+
120
+ - `GATE_CHECK=failed` — a **command** error (see below). Nothing else: no verdict is read.
121
+ - `GATE_CHECK=ok`, then `GATE_BLOCK=true` or `GATE_BLOCK=false`, then — only when the envelope
122
+ carries a non-empty `message` — `GATE_MESSAGE=` followed by that message. The message is printed
123
+ last, so everything after `GATE_MESSAGE=` to the end of the output is the message, however many
124
+ lines it has. Every line of it after the first is indented by two spaces (each line break, `\r`
125
+ and Unicode line separator included, becomes a newline plus two spaces, and other control
126
+ characters become spaces), so no message line can begin with `GATE_` or with the halt token: the
127
+ verdict is the one `GATE_BLOCK=` line at column 0, and a message cannot print a second one (16
128
+ review iteration 3 WR-01).
129
+
130
+ The verdict is computed here, in the shell that ran the check, for two measured reasons. **The
131
+ engine:** this is the only fence in this file that carries the launcher preamble (the launcher
132
+ generator keeps ONE preamble per file, in its first `pp_run` fence, and strips any other), so a
133
+ `pp_run` in any later fence of this file, run as its own call, resolves whatever `pp_run` is on
134
+ `PATH` — measured on a host with a global 1.14.0 engine there: the repo engine answered
135
+ `disabled_with_artifacts, block: true`, the PATH engine answered `inactive`, and a blocking gate
136
+ passed. **The parameters:** a second check call depends on the agent re-binding every `GATE_*`
137
+ parameter for it; one that drops `GATE_UNPAIRED=strict` at `ship:pre` or `verify:post` re-runs a
138
+ tolerant check, which skips SEAL-01, and the gate passes having checked less than step 1 did. Step 2
139
+ therefore runs no command at all; it reads what this fence printed.
140
+
141
+ Both positionals use the **conditional-expansion** idiom `${VAR:+"${VAR}"}` deliberately: an unset or
142
+ empty variable expands to *no word at all*, not to an empty string. A bare `"${GATE_POSITIONAL_ARG_2}"`
143
+ would hand every caller that leaves it unset an extra empty argument, which the router would read as a
144
+ present-but-blank slot. The two must stay identical in form for that reason.
145
+
146
+ `GATE_WS` uses the same idiom for the same reason, and it is a **flag pair** rather than a positional.
147
+ A flag pair must be written as **two separate expansions** — `${GATE_WS:+--ws} ${GATE_WS:+"${GATE_WS}"}`
148
+ — so that it emits both words or neither.
149
+
150
+ **Do NOT collapse it to `${GATE_WS:+--ws "${GATE_WS}"}`.** That single-expansion form is bash-specific
151
+ and fails SILENTLY elsewhere. POSIX and bash word-split the unquoted expansion into two arguments;
152
+ **zsh does not**, so the whole thing arrives as one argument `--ws demo-ws`, which the router does not
153
+ recognise as `--ws` and ignores. The gate then answers `status: unresolved / block: true /
154
+ phase_unresolved` on a partitioned project — the exact symptom of the omitted-`--ws` defect this
155
+ parameter exists to prevent, reintroduced at execution time rather than at authoring time. Reproduce
156
+ with `set -- ${V:+--ws "$V"}`: bash reports `argc=2`, zsh reports `argc=1`.
157
+
158
+ This matters because the shell that runs this line is the *dispatching agent's* shell, not this
159
+ document's fence. Several runtimes (macOS Claude Code among them) execute tool commands under zsh.
160
+ The two-expansion form is byte-identical in both shells, so it removes the dependency entirely.
161
+
162
+ Written correctly, flat mode still produces a byte-identical
163
+ command line to the one that shipped before this parameter existed. It is appended after the
164
+ positionals so it can never occupy a positional slot — the router reads `--ws` by name, and a phase
165
+ argument shifted into the wrong slot is the failure mode the two positionals above already warn about.
166
+
167
+ `GATE_STAGE` is the third use of that idiom, and it carries the same two properties for the same two
168
+ reasons: written as the two-expansion pair `${GATE_STAGE:+--stage} ${GATE_STAGE:+"${GATE_STAGE}"}` it
169
+ emits both words or neither, so a caller that sets
170
+ no stage produces a command line byte-identical to the one that shipped before this parameter
171
+ existed — and it carries the same zsh hazard described above, where collapsing it to one expansion
172
+ makes `--stage plan` arrive as a single ignored argument and silently reinstates the stage-blind
173
+ collection defect; and it too is appended after both positionals, so it reaches every gate declared at the
174
+ point without ever displacing a phase argument. A query with no `--stage` handling ignores an
175
+ unrecognised named flag — which is what makes it safe to append to an invocation line that is shared
176
+ by every gate at a loop point, not owned by the one that wanted it.
177
+
178
+ `GATE_UNPAIRED` is the fourth use of that idiom and carries all three of the same properties, for
179
+ the same three reasons: written as `${GATE_UNPAIRED:+--unpaired} ${GATE_UNPAIRED:+"${GATE_UNPAIRED}"}`
180
+ it emits both words or neither, so a caller that sets no strictness produces a command line
181
+ byte-identical to the one that shipped before this parameter existed; collapsing it to the single
182
+ expansion `${GATE_UNPAIRED:+--unpaired "${GATE_UNPAIRED}"}` makes `--unpaired strict` arrive as one
183
+ argument under zsh, which the router does not recognise, so the strict call sites would silently
184
+ degrade to the tolerant default — a gate that reports success because its own flag was dropped; and
185
+ it is appended after both positionals and after both other pairs, so it can never displace a phase
186
+ argument at the three points that pass one.
187
+
188
+ The command has failed when **any** of these holds: `CHECK_EXIT` is non-zero, `GATE_RESULT` is
189
+ empty, or `GATE_RESULT` does not parse as a JSON object. The fence tests exactly that and prints
190
+ `GATE_CHECK=failed`. That is a **command** error and *only* a command error — and so is a run that
191
+ printed **no** `GATE_CHECK=` line at all (the fence died before it, e.g. the launcher found no
192
+ engine): with nothing printed there is no verdict to read. Route it by the hook's `onError`:
193
+
194
+ - `onError == "halt"` → fatal. Print
195
+ `⚠ Gate check command failed (${GATE_POINT} / ${GATE_QUERY}): command error. Resolve before continuing.`,
196
+ run the halt block below, and stop the host workflow.
197
+ - `onError == "skip"` (or absent) → print
198
+ `⚠ Gate check skipped (${GATE_POINT} / ${GATE_QUERY}): check command error.`, move to the next
199
+ hook, and — this is an imperative, not a preference — **do NOT read a verdict.** The fence
200
+ prints none on this path, and none may be inferred.
201
+
202
+ Merging these two steps is the fail-open this gate exists to close: if a non-zero exit were read as
203
+ a verdict, `onError: skip` would swallow a genuine block as a command warning.
204
+
205
+ ## Step 2 — read the verdict
206
+
207
+ Reached **only** when step 1 passed — that is, when it printed `GATE_CHECK=ok`. **Step 2 runs no
208
+ command** — in particular it
209
+ never runs `pp_run check` again. It reads the `GATE_BLOCK=` line and, when present, the
210
+ `GATE_MESSAGE=` text that step 1's fence printed, and nothing else (step 1 explains why: a second
211
+ check call would answer from whatever engine is on `PATH`, with whatever parameters were re-bound).
212
+ The verdict therefore survives the agent's Bash tool starting a fresh shell for every call: what
213
+ crosses from step 1 to step 2 is printed text, never a shell variable.
214
+
215
+ Step 1 extracts the verdict with the same JSON round-trip idiom the sibling `vault-load-gate.md`
216
+ uses. It deliberately uses `node -e` rather than a JSON command-line filter, because no such filter
217
+ is a declared dependency of this project. The `.block === true` comparison **is** the missing-field
218
+ rule from the audit footnote: a missing, null, string or numeric `block` all evaluate to `false`, so
219
+ the fence prints `GATE_BLOCK=false` for them. (Its `catch` arm, which prints `true`, is unreachable
220
+ behind the command-error test; it exists so that a future edit which removes that test fails closed.)
221
+
222
+ - **`GATE_BLOCKING` is `true` AND `GATE_BLOCK` is `true` → HALT.** Print
223
+ `⚠ Gate blocked at ${GATE_POINT} (${GATE_QUERY}). Resolve before continuing.` and run the halt
224
+ block below.
225
+
226
+ **This halt is never bypassed by `onError`.** `onError` governs command errors from step 1 and
227
+ nothing else; it has no authority over the gate's own decision. A gate that reports a real block
228
+ and is then waved through by an error policy is not a gate.
229
+
230
+ - **`GATE_BLOCKING` is `false` → never halt.** If `GATE_BLOCK` is `true`, print one advisory line:
231
+ `ℹ Gate advisory at ${GATE_POINT} (${GATE_QUERY}). Continuing.` Then continue to the next hook.
232
+
233
+ - **`GATE_BLOCK` is `false` → continue.** No status line.
234
+
235
+ **Output-independence rule (a later plan's byte-identical regression pin depends on this).** On
236
+ every NON-halting path, the enforcer's printed output must be **identical** whether `GATE_BLOCKING`
237
+ is `true` or `false`. `blocking` decides one thing only: whether a `true` block halts. It must never
238
+ change what a passing or advisory verdict prints. Otherwise flipping a gate declaration from
239
+ advisory to blocking would change output for projects whose policy key is off — exactly what the
240
+ byte-identical-when-off promise forbids.
241
+
242
+ ## The halt mechanism (D-VH-7, option A — the workflow owns the exit)
243
+
244
+ There are two halves and both ship, because they serve two different observers.
245
+
246
+ **Half one — the named token, for the agent.** Print, on its own line:
247
+
248
+ ```
249
+ PARETO_GATE_BLOCKED:${GATE_QUERY}
250
+ ```
251
+
252
+ On seeing that token the agent stops the host workflow immediately, runs no further step, and
253
+ reports the block to the user.
254
+
255
+ **Half two — the process exit, for the runner.** In the *same* bash block:
256
+
257
+ ```bash
258
+ echo "PARETO_GATE_BLOCKED:${GATE_QUERY}"
259
+ exit 1
260
+ ```
261
+
262
+ An unattended runner or CI job cannot honour a sentence; it can only observe an exit code. Without
263
+ half two a blocked build reports success to the pipeline.
264
+
265
+ `pp_run check` itself still **exits 0 on every path**, by design (`output()` in `src/io.cts` writes
266
+ to fd 1 and returns; it never sets a non-zero code). That is deliberate and must not be "fixed":
267
+ making the check command exit non-zero would turn every block into a step-1 command error, which
268
+ `onError: skip` would then swallow. The exit code belongs to the workflow, not to the check.
269
+
270
+ ## Message passthrough
271
+
272
+ Independently of status, and on **every** run including passes: if step 1 printed a
273
+ `GATE_MESSAGE=` line, print it verbatim — the message is the text after `GATE_MESSAGE=`, to the end
274
+ of step 1's output, its later lines indented by two spaces — it is already operator-ready, possibly
275
+ multi-line text. A gate that has
276
+ something to tell the operator says it on every run, not only when its verdict is bad. This rule is
277
+ preserved word-for-word in effect from the ship.md loop this file replaced; a later plan makes both
278
+ the vault gate's actionable block text and its success-path observability ride on it.
279
+
280
+ ## Direct-query mode
281
+
282
+ Two entry points — the build entry point `fast` and the discovery sprint's
283
+ `discovery-sprint/vault-context.md` subflow — declare no `gsd:loop-host` frontmatter at all. They
284
+ have no rendered hooks, and adding points to them would bump the twelve-point loop-host contract.
285
+ They are supported here rather than in a third copy of the halt logic. The count in this sentence is
286
+ pinned against the `direct` rows of the Caller contract below, so a third caller must update both.
287
+ (`execute-plan` left this mode in 16-10 and `quick` in 16-11: each renders its own loop points and
288
+ filters the payload to the vault capability, so they are hook rows below.)
289
+
290
+ When `GATE_HOOKS_JSON` is empty **and** `GATE_DIRECT_QUERY` is set to a query name, synthesise a
291
+ single in-memory hook entry — that query, `blocking: true`, `onError: skip` — and run the identical
292
+ loop above over it.
293
+
294
+ **A direct caller must perform its own activation guard before Reading this file.** A synthesised
295
+ hook has not been through the registry's `when` evaluation and therefore carries none of its
296
+ inertness. The two guards such a caller must run first are:
297
+
298
+ 1. the **platform-bindings presence probe** (`pp_run query platform-bindings --pick present`; when
299
+ not `true`, do not Read this file at all — non-Pareto project, fully inert), and
300
+ 2. the **admission verb** (`pp_run vault admission`, D-28; `on` → Read this file, `off` or `inert`
301
+ → do not, anything else halts with `PARETO_GATE_BLOCKED:vault.admission`, D-05). Both direct
302
+ callers, `fast` (16-13) and the discovery subflow (16-04), run it.
303
+
304
+ ## Caller contract
305
+
306
+ | Caller | Mode | Sets |
307
+ |--------|------|------|
308
+ | `pp-core/workflows/ship.md` step 6 | hook | `GATE_POINT=ship:pre`, `GATE_HOOKS_JSON=$SHIP_PRE_HOOKS_JSON`, `GATE_POSITIONAL_ARG="${PHASE_DIR}"`, `GATE_UNPAIRED=strict` — sets **no** `GATE_WS` (a directory carries the partition) and **no** `GATE_STAGE` (this point validates both stages). The positional was empty until 04-09; emptying it again makes a blocking `vault.trace` gate halt every ship on every vault-enabled project |
309
+ | `pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md` (dispatched by the host's execute:pre step) | hook | `GATE_POINT=execute:pre`, `GATE_HOOKS_JSON=$EXECUTE_PRE_HOOKS_JSON`, `GATE_POSITIONAL_ARG="${PHASE_NUMBER}"`, `GATE_WS=<workstream>` |
310
+ | `pp-core/workflows/code-review-fix.md` `vault_admission_gate` (16-15) | hook | `GATE_POINT=execute:pre`, `GATE_HOOKS_JSON=$CRF_EXECUTE_PRE_HOOKS_JSON` (the door's own `execute:pre` render, filtered in the fence to the vault capabilities, D-30), after its own admission fence and before its agent writes source. Sets **no** positional, **no** `GATE_WS` and **no** `GATE_STAGE`: `vault.status` reads project state |
311
+ | `pp-core/workflows/validate-phase.md` § 0.5 (16-15) | hook | `GATE_POINT=execute:pre`, `GATE_HOOKS_JSON=$VP_EXECUTE_PRE_HOOKS_JSON` (the door's own `execute:pre` render, filtered in the fence to the vault capabilities, D-30), after its own admission fence and before its agent writes source. Sets **no** positional, **no** `GATE_WS` and **no** `GATE_STAGE`: `vault.status` reads project state |
312
+ | `pp-core/workflows/add-tests.md` `vault_admission_gate` (16-15) | hook | `GATE_POINT=execute:pre`, `GATE_HOOKS_JSON=$AT_EXECUTE_PRE_HOOKS_JSON` (the door's own `execute:pre` render, filtered in the fence to the vault capabilities, D-30), after its own admission fence and before its agent writes source. Sets **no** positional, **no** `GATE_WS` and **no** `GATE_STAGE`: `vault.status` reads project state |
313
+ | `pp-core/workflows/audit-fix.md` `vault_admission_gate` (16-15) | hook | `GATE_POINT=execute:pre`, `GATE_HOOKS_JSON=$AF_EXECUTE_PRE_HOOKS_JSON` (the door's own `execute:pre` render, filtered in the fence to the vault capabilities, D-30), after its own admission fence and before its agent writes source. Sets **no** positional, **no** `GATE_WS` and **no** `GATE_STAGE`: `vault.status` reads project state |
314
+ | `pp-core/workflows/execute-phase.md` step 5.75 | hook | `GATE_POINT=execute:wave:post`, `GATE_HOOKS_JSON=$WAVE_POST_HOOKS_JSON`, `GATE_POSITIONAL_ARG="${PHASE_NUMBER}"`, `GATE_WS=<workstream>` |
315
+ | `pp-core/workflows/execute-phase.md` execute:post step | hook | `GATE_POINT=execute:post`, `GATE_HOOKS_JSON=$EXECUTE_POST_HOOKS_JSON`, `GATE_POSITIONAL_ARG="${PHASE_NUMBER}"`, `GATE_WS=<workstream>` — sets **no** `GATE_STAGE`, deliberately: this is the point that validates both stages |
316
+ | `pp-core/workflows/execute-phase/steps/verify-post-gate.md` (dispatched by TWO callers: the host's end-of-phase verification step, and `verify-work.md`'s issues-are-zero branch, which exposes no partial-execution mode and is therefore always strict) | hook | `GATE_POINT=verify:post`, `GATE_HOOKS_JSON=$VERIFY_POST_HOOKS_JSON`, `GATE_POSITIONAL_ARG="${PHASE_DIR}"`, and `GATE_UNPAIRED=strict` **only on a full-phase run: when the host's `WAVE_FILTER` is unset AND the invocation carried no `--gaps-only`** — on a `--wave N` or a `--gaps-only` run it leaves `GATE_UNPAIRED` unset so the tolerant default applies, because a partially-executed run legitimately leaves untouched plans unexecuted and this dispatch runs BEFORE the host's `handle_partial_wave_execution` step, so an unconditional strict verdict here halted every partial run on every vault-enabled project (04-12, CR-04; 04-18 WR-01 widened the step file's condition to `--gaps-only`, and this row named `WAVE_FILTER` alone until 260912-kz7 — a stale row is a lie the next author implements). This is the one conditional strictness in the table, and the condition is deliberate: unlike `ship:pre`, whose preflight requires a passed verification and therefore admits no partial run, this point is reachable in a mode where an unpaired plan is the correct state. Sets **no** `GATE_WS` (a directory carries the partition) and **no** `GATE_STAGE`, deliberately, exactly as `execute:post` omits it: this point validates both stages. The body lives in a step file because the host is under a phase-6 ratchet that is never re-baselined (D-05) |
317
+ | `pp-core/workflows/plan-phase.md` §13e | hook | `GATE_POINT=plan:post`, `GATE_HOOKS_JSON=$PLAN_POST_HOOKS_JSON`, `GATE_POSITIONAL_ARG="${PHASE_DIR}"`, `GATE_POSITIONAL_ARG_2="${PHASE_REQ_IDS}"`, `GATE_WS=<workstream>`, `GATE_STAGE=plan` — the only caller using the second positional, and the only one setting a stage |
318
+ | `pp-core/workflows/plan-phase.md` plan:pre vault gate | hook | `GATE_POINT=plan:pre`, `GATE_HOOKS_JSON=$PLAN_PRE_HOOKS_JSON` — skips `ui.plan-gate`, which §5.6 above already ran with its own phase positional and whose outcome it owns. `GATE_POSITIONAL_ARG` is left **empty** deliberately: `vault.status` reads project state, not a phase, and every remaining gate at this point that needs a positional is caller-handled (D-VH-7). Sets **no** `GATE_WS` (there is no phase-number positional to resolve) and **no** `GATE_STAGE`. Handing the whole payload here with no skip made the enforcer issue `check ui.plan-gate` with no argument; `onError: halt` turned that command error into a hard stop on every vault-enabled plan run, before the vault verdict was read (02-09, G-1) |
319
+ | `pp-core/workflows/autonomous.md` §3a.5-iii | hook | `GATE_POINT=plan:pre`, `GATE_HOOKS_JSON=$HOOKS_JSON` — skips `ui.plan-gate`, which §3a.5-ii above owns on every activation set that renders it, the gate-only set (`ui_phase=false` + `ui_safety_gate=true`) included. `GATE_POSITIONAL_ARG` is left **empty** deliberately, for the same reason as the `plan-phase.md` plan:pre row. Sets **no** `GATE_WS` and **no** `GATE_STAGE`. The skip is sound only because §3a.5-ii's run trigger covers the gate-only set — an unconditional skip without that trigger would leave a `blocking: true` gate enforced by nobody under `--auto` (02-09, T-02-41) |
320
+ | `pp-core/workflows/quick.md` Step 2.4 (16-11) | hook | `GATE_POINT=plan:pre`, `GATE_HOOKS_JSON=$QUICK_PLAN_PRE_HOOKS_JSON` (its own `plan:pre` render, filtered in the fence to the `vault` capability, D-30, so no `ui.plan-gate` reaches the enforcer), after its own admission fence. `GATE_POSITIONAL_ARG` is left **empty** deliberately (`vault.status` reads project state); sets **no** `GATE_WS` and **no** `GATE_STAGE`. Quick's `execute:pre` render feeds only the executor prompt's fragments, never a gate |
321
+ | `pp-core/workflows/debug.md` § 0b (16-14) | hook | `GATE_POINT=plan:pre`, `GATE_HOOKS_JSON=$DEBUG_PLAN_PRE_HOOKS_JSON` (its own `plan:pre` render, filtered in the fence to the `vault` capability, D-30), after its own admission fence and before either session-manager spawn. `GATE_POSITIONAL_ARG` is left **empty** deliberately (`vault.status` reads project state); sets **no** `GATE_WS` and **no** `GATE_STAGE` |
322
+ | `pp-core/workflows/quick.md` Step 5.55 (16-11) | hook | `GATE_POINT=plan:post`, `GATE_HOOKS_JSON=$QUICK_PLAN_POST_HOOKS_JSON` (filtered the same way), `GATE_POSITIONAL_ARG="${QUICK_DIR}"`, `GATE_STAGE=plan` — before Step 5.6's plan commit and the executor spawn (D-34). Sets **no** `GATE_WS`: a quick directory carries its partition |
323
+ | `pp-core/workflows/quick.md` Step 6.1 (16-11) | hook | `GATE_POINT=execute:post`, `GATE_HOOKS_JSON=$QUICK_EXECUTE_POST_HOOKS_JSON` (filtered the same way), `GATE_POSITIONAL_ARG="${QUICK_DIR}"`, `GATE_UNPAIRED=strict` (one plan, one SUMMARY, nothing pending, D-03) — before the STATE.md row and the final commit (D-34). Sets **no** `GATE_STAGE` and **no** `GATE_WS` |
324
+ | `pp-core/workflows/fast.md` (plan 02-05; admission on the verb since 16-13) | direct | `GATE_POINT=fast:pre`, `GATE_DIRECT_QUERY=vault.status`, own activation guards (the bindings probe, then `vault admission`); with the vault on, `fast` also refuses edits outside `.planning/` and backstops its commit (D-07) |
325
+ | `pp-core/workflows/execute-plan.md` `vault_admission_gate` (16-10) | hook | `GATE_POINT=execute:pre`, `GATE_HOOKS_JSON=$EXECUTE_PRE_HOOKS_JSON` (its own `execute:pre` render, filtered in the fence to the `vault` capability, D-30), after its own admission fence. Sets **no** positional, **no** `GATE_WS` and **no** `GATE_STAGE`: `vault.status` reads project state |
326
+ | `pp-core/workflows/execute-plan.md` `vault_trace_gate` (16-10) | hook | `GATE_POINT=plan:post`, `GATE_HOOKS_JSON=$PLAN_POST_HOOKS_JSON` (its own `plan:post` render, filtered the same way), `GATE_POSITIONAL_ARG="$PLAN_PATH"` (the ONE `*-PLAN.md` file this run executes, never its phase directory, D-06), `GATE_STAGE=plan`. Sets **no** `GATE_WS`: the file carries its partition |
327
+ | `pp-core/workflows/discovery-sprint/vault-context.md` (plan 11-03) | direct | `GATE_POINT=discovery:pre`, `GATE_DIRECT_QUERY=vault.status`, own activation guards; Read from the discovery sprint's Flow 2 Step 1 and prefill Step 0 only, never from a read-only arm (D-11-12); runs `vault-fetch.md` in direct mode FIRST, so both `plan:pre` halt surfaces are inherited in their order (D-11-11); `discovery:pre` is a message label, not a loop point — `GATE_POINT` never changes control flow, which is what makes a non-canonical label safe here |
@@ -10,12 +10,13 @@ out of scope here.
10
10
 
11
11
  ```bash
12
12
  _PP_SHIM_NAME="pp-tools.cjs"; _pp_root() { ( for _v in $(env | sed -n 's/^\(GIT_[A-Za-z0-9_]*\)=.*/\1/p'); do unset "$_v"; done; _c=$(git rev-parse --git-common-dir 2>/dev/null); if [ "${_c##*/}" = .git ] && cd "$_c/.." 2>/dev/null; then pwd -P; else git rev-parse --show-toplevel 2>/dev/null || pwd; fi ); }; pp_run() { case "$PP_TOOLS" in *.cjs) node "$PP_TOOLS" "$@";; *) "$PP_TOOLS" "$@";; esac; }; _PP_RUNTIME_ROOT="${RUNTIME_DIR:-$(_pp_root)}"; PP_TOOLS="${_PP_RUNTIME_ROOT}/pp-core/bin/${_PP_SHIM_NAME}"; if [ -f "$PP_TOOLS" ]; then :; elif [ -f "${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}"; elif command -v pp-tools >/dev/null 2>&1; then PP_TOOLS="$(command -v pp-tools)"; else echo "ERROR: pp-tools.cjs not found at $PP_TOOLS and pp-tools is not on PATH. Run: npx -y @pareto4/pp-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${PP_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${PP_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
13
- CTX_GATE_PRESENT=$(pp_run query platform-bindings --pick present 2>/dev/null || echo false)
13
+ CTX_GATE_PRESENT=$(pp_run query platform-bindings --pick present 2>/dev/null || echo true)
14
14
  CTX_BLOCK=$(pp_run query generate-client-context --read 2>/dev/null || echo '{"present":false}')
15
15
  CTX_BLOCK_PRESENT=$(printf '%s' "$CTX_BLOCK" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{process.stdout.write(String((JSON.parse(d).present)||false));}catch{process.stdout.write('false');}")
16
16
  ```
17
17
 
18
- **If `CTX_GATE_PRESENT` != `true`:** skip entirely (non-Pareto project — fully inert).
18
+ **If `CTX_GATE_PRESENT` != `true`:** skip entirely (non-Pareto project — fully inert). A probe that
19
+ cannot answer (a malformed bindings file) reads as present, as `hasPlatformBindings` does (D-47 b).
19
20
 
20
21
  **If `CTX_GATE_PRESENT` == `true` and `CTX_BLOCK_PRESENT` == `true`:** the client-context
21
22
  block exists and is on the executor's read path. Surface a one-line confirmation
@@ -5,7 +5,7 @@ unchanged; the host now lazily Reads it.
5
5
 
6
6
  **Why it moved.** Phase 9's W-1 fix gave this step's `portable-timeout.sh` source the same
7
7
  resolve-then-check shape the other eight sites use, which grew `execute-phase.md` by 417 bytes to
8
- 94088. That passed `tests/workflow-size-baseline.json` after a conscious re-baseline, but a SECOND
8
+ 93943. That passed `tests/workflow-size-baseline.json` after a conscious re-baseline, but a SECOND
9
9
  and independent ratchet — `tests/phase6-capstone-conformance.test.cjs:207`, the ADR-857 shrink
10
10
  ratchet — asserts `execute-phase.md` stays strictly **under 93600**, and it was never re-baselined
11
11
  because it is not supposed to be: its whole purpose is that the host loop body only ever shrinks.
@@ -0,0 +1,78 @@
1
+ # `execute:pre` capability dispatch — fetch, then decide (GATE-03)
2
+
3
+ Extracted from `execute-phase.md` so the host loop body keeps shrinking, per ADR-857 Phase 6.
4
+ `steps/` files are outside `tests/workflow-size-baseline.json` by design; the host is not.
5
+
6
+ This step is dispatched from `execute-phase.md`'s `execute_pre_dispatch` step, which sits *before*
7
+ `safe_resume_gate` deliberately: that is the first step that can spawn an executor, and the gate must
8
+ run before any code is written, not after.
9
+
10
+ Read the `activeHooks` array from the `EXECUTE_PRE_HOOKS_JSON=` line the host's render fence printed, in-context (do NOT pipe it through a
11
+ shell parser).
12
+
13
+ **Admission first (D-28, D-05).** `execute-phase.md` Reads this file unconditionally, so this is the
14
+ first point on the execute path that can refuse a config the gate cannot read. It precedes the inert
15
+ branch below because that branch is exactly what a broken config reaches: no hook renders at all.
16
+ Without `.platform-bindings.json` it is inert and prints nothing.
17
+
18
+ ```bash
19
+ _PP_SHIM_NAME="pp-tools.cjs"; _pp_root() { ( for _v in $(env | sed -n 's/^\(GIT_[A-Za-z0-9_]*\)=.*/\1/p'); do unset "$_v"; done; _c=$(git rev-parse --git-common-dir 2>/dev/null); if [ "${_c##*/}" = .git ] && cd "$_c/.." 2>/dev/null; then pwd -P; else git rev-parse --show-toplevel 2>/dev/null || pwd; fi ); }; pp_run() { case "$PP_TOOLS" in *.cjs) node "$PP_TOOLS" "$@";; *) "$PP_TOOLS" "$@";; esac; }; _PP_RUNTIME_ROOT="${RUNTIME_DIR:-$(_pp_root)}"; PP_TOOLS="${_PP_RUNTIME_ROOT}/pp-core/bin/${_PP_SHIM_NAME}"; if [ -f "$PP_TOOLS" ]; then :; elif [ -f "${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}"; elif command -v pp-tools >/dev/null 2>&1; then PP_TOOLS="$(command -v pp-tools)"; else echo "ERROR: pp-tools.cjs not found at $PP_TOOLS and pp-tools is not on PATH. Run: npx -y @pareto4/pp-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${PP_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${PP_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
20
+ VAULT_ADMIT_PRESENT=$(pp_run query platform-bindings --pick present 2>/dev/null || echo true)
21
+ VAULT_ADMIT=inert
22
+ if [ "$VAULT_ADMIT_PRESENT" = true ]; then VAULT_ADMIT=$(pp_run vault admission 2>/dev/null || echo unrecognised); fi
23
+ case "$VAULT_ADMIT" in on) echo "VAULT_ADMIT=on" ;; off|inert) : ;; *) pp_run vault admission --explain; echo "PARETO_GATE_BLOCKED:vault.admission"; exit 1 ;; esac
24
+ ```
25
+
26
+ **If `activeHooks` is empty or absent:** skip the rest of this file silently. This branch is what
27
+ keeps a project with no active `execute:pre` capability structurally inert.
28
+
29
+ **Otherwise, in this order — the order is the requirement:**
30
+
31
+ 1. **Fetch first.** For any active entry where `kind == "step"` and `capId == "vault"`, set
32
+ `VAULT_FETCH_HOOKS_JSON=$EXECUTE_PRE_HOOKS_JSON`, then Read and
33
+ execute `pp-core/workflows/execute-phase/steps/vault-fetch.md`. The fetch is attempted *before*
34
+ any block decision so a fresh clone or a CI runner with a reachable vault fetches and passes,
35
+ rather than being blocked for a cache it never had a chance to populate (GATE-03).
36
+
37
+ The binding is not decoration and it is not the same shape as step 2's: `vault-fetch.md` is
38
+ Read from three call sites at two loop points, so it guards on the **parameter**
39
+ `VAULT_FETCH_HOOKS_JSON` and names no caller's own payload variable. Until 02-10 it named
40
+ `plan-phase.md`'s, which this dispatcher never sets and never aliased — and the instruction
41
+ immediately after the guard is *"if no active vault step hook exists: skip"*. A literal
42
+ reading therefore skipped the `execute:pre` fetch outright and let step 2 decide on whatever
43
+ cache happened to exist, which is exactly the fresh-clone false block this ordering exists to
44
+ prevent (02-VERIFICATION, G-2). Binding it here is the whole fix at this call site.
45
+
46
+ 2. **Then gate.** Set `GATE_POINT=execute:pre`, `GATE_HOOKS_JSON=$EXECUTE_PRE_HOOKS_JSON`,
47
+ `GATE_POSITIONAL_ARG="${PHASE_NUMBER}"` and `GATE_WS` to the active workstream name (empty in flat
48
+ mode) — a phase-number positional cannot resolve without it on a partitioned project — then Read and follow
49
+ `pp-core/workflows/execute-phase/steps/check-query-gate.md` — the single generic `check.query`
50
+ gate enforcer (GATE-02, D-VH-7). It owns the halt decision; do not restate its loop here.
51
+
52
+ 3. **Then render.** If the workflow is still running, set `VAULT_GATE_POINT=execute` and Read and
53
+ execute `pp-core/workflows/execute-phase/steps/vault-load-gate.md`. It reports the load state for
54
+ the operator and decides nothing — the decision was step 2's.
55
+
56
+ This line is what makes the load-gate file's `execute:pre` message column live. Until this phase
57
+ that column was written and never dispatched: the execute half of a file that claimed to serve
58
+ both call sites was dead code.
59
+
60
+ 4. **Then record the start anchor (D-31).** If the workflow is still running and the
61
+ `activeHooks` array holds an entry with `capId == "vault"` (the same mechanical check step 1 uses
62
+ for the fetch), run `pp_run vault anchor start "${PHASE_DIR}" --raw` and print its one-line result
63
+ to the operator. Otherwise skip it.
64
+
65
+ It writes `${PHASE_DIR}/.vault-anchors.json` with `start` = HEAD. Every record the phase's
66
+ executors write later (the `vault standards consulted` anchor line) reads it as its START anchor,
67
+ beside the END anchor (HEAD at write time) the CLI records itself. A commit is then covered only
68
+ when it falls inside some record's start..end range, so an ungoverned commit followed by any
69
+ record can no longer hide behind that record's end. Re-running at a new HEAD (a resumed run, `--wave N`, `--gaps-only`) records that HEAD as the start of every
70
+ record written from now on (D-98), so nothing committed between two runs is claimed by a later
71
+ record; the earlier start stays in the file's history, where a worktree executor's end-only record
72
+ written before the resume still finds the start that preceded its own end. The phase's own commits
73
+ made between its last record and the resume are then uncovered, and the strict gate names them with
74
+ their repair: `vault anchor backfill <summary> --from <a recorded start> --attest <commit>`, the
75
+ operator's attestation of each commit by its own id; nothing is widened, and every check lists it.
76
+ The verb prints nothing and writes nothing unless `vault admission` answers
77
+ `on`, exits 0 on every path, and reports a refusal as `ok: false` with a reason that is printed
78
+ and never halts. A phase that has no start anchor fails at the strict points instead.
@@ -0,0 +1,94 @@
1
+ # Per-plan worktree decision (#2772)
2
+
3
+ Run this for **each plan in the current wave** before its `Agent()` dispatch. The output `USE_WORKTREES_FOR_PLAN` gates the dispatch branch (worktree mode vs sequential mode) for that plan only — other plans in the same wave can still take the worktree path.
4
+
5
+ `SUBMODULE_PATHS` is computed once in the `initialize` step (parsed from `.gitmodules`).
6
+
7
+ `PLAN_FILES` is the whitespace-separated list of paths the plan declared it will touch, extracted from the `phase-plan-index` JSON loaded in `discover_and_group_plans`:
8
+
9
+ ```bash
10
+ # plan_json is the JSON object for this plan from PLAN_INDEX.plans[]
11
+ # files_modified is an array of strings (repo-relative paths or globs)
12
+ PLAN_FILES=$(jq -r '.files_modified // [] | join(" ")' <<<"$plan_json")
13
+ plan_id=$(jq -r '.id' <<<"$plan_json")
14
+ ```
15
+
16
+ Then run the per-plan gate:
17
+
18
+ ```bash
19
+ USE_WORKTREES_FOR_PLAN="$USE_WORKTREES"
20
+
21
+ if [ -n "$SUBMODULE_PATHS" ] && [ "$USE_WORKTREES_FOR_PLAN" != "false" ]; then
22
+ if [ -z "$PLAN_FILES" ]; then
23
+ # Fallback: planned paths are unknown/unparseable — fall back to the safe
24
+ # behavior (disable worktree isolation for this plan) and log why.
25
+ echo "[worktree] Plan ${plan_id}: files_modified missing/unparseable — disabling worktree isolation as a safety fallback (submodule project)"
26
+ USE_WORKTREES_FOR_PLAN=false
27
+ else
28
+ # Compute intersection with glob-safe normalization. Both sides are
29
+ # normalized (strip leading "./", strip trailing "/") and matched
30
+ # bidirectionally so a globby planned path like "vendor/**/*.c" still
31
+ # matches submodule "vendor/foo", and "./vendor/foo/bar.c" matches
32
+ # submodule "vendor/foo".
33
+ INTERSECT=""
34
+ set -f # disable globbing while iterating literal patterns
35
+ for sm_raw in $SUBMODULE_PATHS; do
36
+ # Normalize submodule path: strip ./ prefix and trailing /
37
+ sm="${sm_raw#./}"
38
+ sm="${sm%/}"
39
+ [ -z "$sm" ] && continue
40
+ for pf_raw in $PLAN_FILES; do
41
+ # Normalize planned path the same way
42
+ pf="${pf_raw#./}"
43
+ pf="${pf%/}"
44
+ [ -z "$pf" ] && continue
45
+ matched=0
46
+ # Direction 1: planned path is the submodule or lies inside it
47
+ case "$pf" in
48
+ "$sm"|"$sm"/*) matched=1 ;;
49
+ esac
50
+ # Direction 2: submodule lies inside the planned path (e.g. plan
51
+ # declares "vendor" or a glob expanding to a directory containing
52
+ # the submodule).
53
+ if [ "$matched" -eq 0 ]; then
54
+ case "$sm" in
55
+ "$pf"|"$pf"/*) matched=1 ;;
56
+ esac
57
+ fi
58
+ # Direction 3: planned path uses a glob — strip glob wildcards
59
+ # and check whether the resulting prefix overlaps the submodule
60
+ # path in either direction.
61
+ if [ "$matched" -eq 0 ]; then
62
+ case "$pf" in
63
+ *'*'*|*'?'*|*'['*)
64
+ # Take the literal prefix before the first glob metachar.
65
+ prefix="${pf%%[*?[]*}"
66
+ prefix="${prefix%/}"
67
+ if [ -n "$prefix" ]; then
68
+ case "$sm" in
69
+ "$prefix"|"$prefix"/*) matched=1 ;;
70
+ esac
71
+ if [ "$matched" -eq 0 ]; then
72
+ case "$prefix" in
73
+ "$sm"|"$sm"/*) matched=1 ;;
74
+ esac
75
+ fi
76
+ fi
77
+ ;;
78
+ esac
79
+ fi
80
+ if [ "$matched" -eq 1 ]; then
81
+ INTERSECT="$INTERSECT $pf_raw"
82
+ fi
83
+ done
84
+ done
85
+ set +f
86
+ if [ -n "$INTERSECT" ]; then
87
+ echo "[worktree] Plan ${plan_id}: planned paths intersect submodule paths (${INTERSECT# }) — disabling worktree isolation for this plan"
88
+ USE_WORKTREES_FOR_PLAN=false
89
+ fi
90
+ fi
91
+ fi
92
+ ```
93
+
94
+ After running this for the plan, the dispatch branches in `execute_waves` step 3 MUST gate on `USE_WORKTREES_FOR_PLAN` for the current plan, not on the project-level `USE_WORKTREES`. Track which plans in this wave actually used worktrees (append `plan_id` to a `WAVE_WORKTREE_PLANS` accumulator when `USE_WORKTREES_FOR_PLAN != false`) — the post-wave cleanup step (5.5) uses this to decide whether worktree-merge cleanup is needed at all.
@@ -1,13 +1,11 @@
1
1
  # Step: post_merge_gate
2
2
 
3
- Post-merge build & test gate. Runs exactly once per wave, after the wave's merge
4
- step — the wave's one full-suite run (`suite_runs: 1` in the dispatch plan); no
5
- executor runs the full suite. Catches cross-plan integration failures that
6
- per-plan checks cannot detect.
7
-
8
- **Step A — Build gate:** run it only when the dispatch plan's wave `build` is true.
9
- When it is false (no plan in the wave touches source), print
10
- `⚠ Post-merge build gate skipped — no plan in this wave touches source` and go to Step B.
3
+ Post-merge build & test gate. Runs after all worktrees in a wave are merged
4
+ (parallel mode), or after the last plan completes (serial mode). Catches
5
+ cross-plan integration failures that individual worktree self-checks cannot
6
+ detect.
7
+
8
+ **Step A — Build gate:**
11
9
 
12
10
  ```bash
13
11
  _PP_SHIM_NAME="pp-tools.cjs"; _pp_root() { ( for _v in $(env | sed -n 's/^\(GIT_[A-Za-z0-9_]*\)=.*/\1/p'); do unset "$_v"; done; _c=$(git rev-parse --git-common-dir 2>/dev/null); if [ "${_c##*/}" = .git ] && cd "$_c/.." 2>/dev/null; then pwd -P; else git rev-parse --show-toplevel 2>/dev/null || pwd; fi ); }; pp_run() { case "$PP_TOOLS" in *.cjs) node "$PP_TOOLS" "$@";; *) "$PP_TOOLS" "$@";; esac; }; _PP_RUNTIME_ROOT="${RUNTIME_DIR:-$(_pp_root)}"; PP_TOOLS="${_PP_RUNTIME_ROOT}/pp-core/bin/${_PP_SHIM_NAME}"; if [ -f "$PP_TOOLS" ]; then :; elif [ -f "${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}"; elif command -v pp-tools >/dev/null 2>&1; then PP_TOOLS="$(command -v pp-tools)"; else echo "ERROR: pp-tools.cjs not found at $PP_TOOLS and pp-tools is not on PATH. Run: npx -y @pareto4/pp-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${PP_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${PP_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi