@pareto4/pp-core 1.12.1 → 1.17.1

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 (196) hide show
  1. package/.claude-plugin/plugin.json +3 -3
  2. package/README.md +26 -9
  3. package/agents/pp-code-reviewer.md +3 -2
  4. package/agents/pp-debug-session-manager.md +9 -6
  5. package/agents/pp-executor.md +7 -21
  6. package/agents/pp-plan-checker.md +4 -4
  7. package/agents/pp-planner.md +23 -9
  8. package/agents/pp-vault-reviewer.md +274 -0
  9. package/agents/pp-verifier.md +7 -3
  10. package/bin/install.js +10 -3
  11. package/commands/pp-execute-phase.md +1 -3
  12. package/commands/pp-ns-review.md +2 -1
  13. package/commands/pp-plan-phase.md +1 -2
  14. package/commands/pp-vault-review.md +35 -0
  15. package/commands/pp-verify-work.md +1 -2
  16. package/gemini-extension.json +1 -1
  17. package/hooks/dist/pp-check-update.js +0 -0
  18. package/hooks/dist/pp-cursor-session-start.js +1 -1
  19. package/hooks/dist/pp-statusline.js +1 -1
  20. package/hooks/dist/pp-update-banner.js +0 -0
  21. package/hooks/pp-check-update.js +0 -0
  22. package/hooks/pp-cursor-session-start.js +1 -1
  23. package/hooks/pp-statusline.js +1 -1
  24. package/hooks/pp-update-banner.js +0 -0
  25. package/package.json +14 -19
  26. package/pareto-skills/_shared/create-project-board.sh +0 -0
  27. package/pareto-skills/pp-project-setup/SKILL.md +1 -1
  28. package/pp-core/bin/check-latest-version.cjs +0 -0
  29. package/pp-core/bin/detect-build-commands.sh +2 -2
  30. package/pp-core/bin/lib/capability-activation.cjs +83 -9
  31. package/pp-core/bin/lib/capability-registry.cjs +526 -22
  32. package/pp-core/bin/lib/capability-state.cjs +160 -4
  33. package/pp-core/bin/lib/capability-writer.cjs +19 -5
  34. package/pp-core/bin/lib/check-command-router.cjs +3867 -274
  35. package/pp-core/bin/lib/clusters.cjs +4 -0
  36. package/pp-core/bin/lib/command-aliases.cjs +0 -28
  37. package/pp-core/bin/lib/commands.cjs +9 -4
  38. package/pp-core/bin/lib/compose-bindings.cjs +253 -76
  39. package/pp-core/bin/lib/config-loader.cjs +7 -103
  40. package/pp-core/bin/lib/config-schema.cjs +1 -1
  41. package/pp-core/bin/lib/config.cjs +154 -62
  42. package/pp-core/bin/lib/decisions.cjs +104 -27
  43. package/pp-core/bin/lib/discovery-vault-check.cjs +246 -0
  44. package/pp-core/bin/lib/frontmatter.cjs +1 -1
  45. package/pp-core/bin/lib/git-base-branch.cjs +138 -10
  46. package/pp-core/bin/lib/git-provider.cjs +4 -4
  47. package/pp-core/bin/lib/init-command-router.cjs +5 -8
  48. package/pp-core/bin/lib/init-contract.cjs +653 -629
  49. package/pp-core/bin/lib/init-interactive.cjs +77 -21
  50. package/pp-core/bin/lib/init.cjs +24 -51
  51. package/pp-core/bin/lib/install-profiles.cjs +1 -1
  52. package/pp-core/bin/lib/io.cjs +8 -0
  53. package/pp-core/bin/lib/loop-resolver.cjs +169 -47
  54. package/pp-core/bin/lib/org-profile.cjs +544 -53
  55. package/pp-core/bin/lib/package-identity.cjs +3 -3
  56. package/pp-core/bin/lib/phase-command-router.cjs +1 -23
  57. package/pp-core/bin/lib/phase-scope.cjs +1 -1
  58. package/pp-core/bin/lib/phase.cjs +11 -203
  59. package/pp-core/bin/lib/platform-bindings.cjs +40 -19
  60. package/pp-core/bin/lib/practices-review-check.cjs +15 -9
  61. package/pp-core/bin/lib/profile-output.cjs +1 -1
  62. package/pp-core/bin/lib/shell-command-projection.cjs +104 -1
  63. package/pp-core/bin/lib/state.cjs +11 -85
  64. package/pp-core/bin/lib/template.cjs +1 -1
  65. package/pp-core/bin/lib/vault-audit.cjs +2594 -0
  66. package/pp-core/bin/lib/vault-command-router.cjs +2756 -60
  67. package/pp-core/bin/lib/vault-coordinates.cjs +13 -4
  68. package/pp-core/bin/lib/vault-index.cjs +699 -0
  69. package/pp-core/bin/lib/vault-mode.cjs +235 -0
  70. package/pp-core/bin/lib/vault-practices.cjs +1194 -89
  71. package/pp-core/bin/lib/vault-review.cjs +2102 -0
  72. package/pp-core/bin/lib/vault-seal.cjs +1936 -0
  73. package/pp-core/bin/lib/vault-standards.cjs +1505 -0
  74. package/pp-core/bin/lib/vault-trace.cjs +3184 -0
  75. package/pp-core/bin/lib/vault-update.cjs +242 -0
  76. package/pp-core/bin/lib/vault-write-back.cjs +78 -31
  77. package/pp-core/bin/lib/vault.cjs +2713 -634
  78. package/pp-core/bin/lib/verify.cjs +0 -11
  79. package/pp-core/bin/portable-timeout.sh +1 -1
  80. package/pp-core/bin/pp-tools.cjs +184 -37
  81. package/pp-core/bin/shared/config-defaults.manifest.json +0 -7
  82. package/pp-core/bin/shared/config-schema.manifest.json +0 -3
  83. package/pp-core/bin/shared/model-catalog.json +1 -0
  84. package/pp-core/bin/verify-reapply-patches.cjs +0 -0
  85. package/pp-core/org-profile.json +26 -0
  86. package/pp-core/references/autonomous-best-effort.md +8 -7
  87. package/pp-core/references/loop-hook-dispatch.md +0 -14
  88. package/pp-core/references/planner-gap-closure.md +0 -1
  89. package/pp-core/references/planner-tdd.md +0 -1
  90. package/pp-core/references/planner-vault-practices.md +31 -3
  91. package/pp-core/references/planner-vault-standards.md +320 -0
  92. package/pp-core/references/planning-config.md +0 -8
  93. package/pp-core/references/practice-arithmetic-contract.md +121 -0
  94. package/pp-core/references/tdd.md +0 -1
  95. package/pp-core/references/verifier-practice-verdicts.md +263 -0
  96. package/pp-core/templates/README.md +1 -0
  97. package/pp-core/templates/VAULT-REVIEW.md +202 -0
  98. package/pp-core/templates/phase-prompt.md +2 -6
  99. package/pp-core/templates/practices/README.md +137 -11
  100. package/pp-core/templates/project-charter.md +226 -0
  101. package/pp-core/workflows/_runtime-launcher.snippet.sh +0 -0
  102. package/pp-core/workflows/add-tests.md +55 -0
  103. package/pp-core/workflows/audit-fix.md +63 -0
  104. package/pp-core/workflows/audit-milestone.md +1 -1
  105. package/pp-core/workflows/autonomous.md +85 -11
  106. package/pp-core/workflows/check-todos.md +13 -2
  107. package/pp-core/workflows/code-review-fix.md +59 -1
  108. package/pp-core/workflows/code-review.md +10 -1
  109. package/pp-core/workflows/complete-milestone.md +1 -1
  110. package/pp-core/workflows/debug.md +56 -2
  111. package/pp-core/workflows/discovery-sprint/prefill.md +21 -0
  112. package/pp-core/workflows/discovery-sprint/prototype.md +46 -48
  113. package/pp-core/workflows/discovery-sprint/session-13-gates.md +2 -0
  114. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +1 -3
  115. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +1 -3
  116. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +1 -3
  117. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +20 -24
  118. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +1 -3
  119. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +1 -3
  120. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +1 -3
  121. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +1 -3
  122. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +1 -3
  123. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +1 -6
  124. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +1 -3
  125. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +1 -3
  126. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -3
  127. package/pp-core/workflows/discovery-sprint/templates/context-template.md +1 -1
  128. package/pp-core/workflows/discovery-sprint/vault-context.md +213 -0
  129. package/pp-core/workflows/discovery-sprint.md +7 -3
  130. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +4 -4
  131. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +43 -1
  132. package/pp-core/workflows/discovery-to-build.md +9 -1
  133. package/pp-core/workflows/discuss-phase.md +3 -10
  134. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +329 -0
  135. package/pp-core/workflows/execute-phase/steps/client-context-gate.md +3 -2
  136. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -1
  137. package/pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md +98 -0
  138. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
  139. package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +6 -8
  140. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +115 -14
  141. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +322 -18
  142. package/pp-core/workflows/execute-phase/steps/verifier-practice-payload.md +165 -0
  143. package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +163 -0
  144. package/pp-core/workflows/execute-phase.md +154 -110
  145. package/pp-core/workflows/execute-plan.md +58 -4
  146. package/pp-core/workflows/fast.md +53 -0
  147. package/pp-core/workflows/help/modes/full.md +1 -0
  148. package/pp-core/workflows/init/steps/client-context-capture.md +101 -0
  149. package/pp-core/workflows/init/steps/gitlab-provisioning.md +130 -0
  150. package/pp-core/workflows/init/steps/project-md-deferral-narration.md +66 -0
  151. package/pp-core/workflows/init/steps/reachability-outcome.md +70 -0
  152. package/pp-core/workflows/init.md +232 -272
  153. package/pp-core/workflows/new-project.md +31 -23
  154. package/pp-core/workflows/plan-phase.md +99 -43
  155. package/pp-core/workflows/quick.md +120 -2
  156. package/pp-core/workflows/secure-phase.md +1 -1
  157. package/pp-core/workflows/ship.md +57 -24
  158. package/pp-core/workflows/update.md +3 -3
  159. package/pp-core/workflows/validate-phase.md +60 -1
  160. package/pp-core/workflows/vault-review.md +325 -0
  161. package/pp-core/workflows/verify-work.md +13 -12
  162. package/scripts/changeset/cli.cjs +6 -5
  163. package/scripts/changeset/lint.cjs +0 -0
  164. package/scripts/changeset/new.cjs +33 -50
  165. package/scripts/dev-env.cjs +25 -85
  166. package/scripts/gen-capability-registry.cjs +199 -0
  167. package/scripts/gen-practice-arithmetic-contract.cjs +412 -0
  168. package/scripts/lib/allowlist-ratchet.cjs +1 -103
  169. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -2
  170. package/scripts/lint-docs-required.cjs +0 -0
  171. package/scripts/lint-pr-check-project-dir.cjs +1 -3
  172. package/scripts/lint-test-file-count.allowlist.json +13 -2
  173. package/scripts/practice-arithmetic-contract.allow.json +35 -0
  174. package/scripts/prompt-injection-scan.sh +22 -15
  175. package/scripts/record-vault-mode-golden.cjs +89 -0
  176. package/scripts/release-channel.cjs +0 -0
  177. package/scripts/sync-manifest-versions.cjs +5 -4
  178. package/scripts/sync-rulesets.sh +1 -1
  179. package/scripts/update-size-baseline.cjs +30 -274
  180. package/scripts/vault-calibrate.cjs +996 -0
  181. package/scripts/verify-opengsd-sweep.cjs +3 -5
  182. package/scripts/workflow-size.cjs +9 -61
  183. package/pp-core/bin/lib/dispatch-plan.cjs +0 -472
  184. package/pp-core/bin/lib/install-run.cjs +0 -536
  185. package/pp-core/bin/lib/phase-tier.cjs +0 -467
  186. package/pp-core/bin/lib/plan-graph.cjs +0 -368
  187. package/pp-core/references/planner-plan-graph.md +0 -66
  188. package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +0 -45
  189. package/pp-core/workflows/execute-phase/steps/install-run.md +0 -29
  190. package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +0 -23
  191. package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +0 -72
  192. package/pp-core/workflows/plan-phase/steps/phase-tier.md +0 -34
  193. package/scripts/lib/size-budget.cjs +0 -311
  194. package/scripts/lint-neutrality-names.cjs +0 -694
  195. package/scripts/lint-org-coordinates.allowlist.json +0 -35
  196. package/scripts/lint-org-coordinates.cjs +0 -426
@@ -47,7 +47,10 @@ application — what it does, who it's for, why it matters}
47
47
  ---
48
48
 
49
49
  ## Functional Requirements
50
- {From Session 3: EPICs and user stories, structured for requirements generation}
50
+ {From Session 3: EPICs and user stories, structured for requirements generation. When
51
+ `_requirements.md` exists it is the requirement source — it holds what prototype feedback and the
52
+ element sweep added: carry every `MVP` and `Later` row, and list each `Dropped` row under Out of
53
+ Scope with its decision}
51
54
 
52
55
  ### EPIC Registry
53
56
  | ID | Title | Priority | Phase | Stories |
@@ -76,6 +79,42 @@ application — what it does, who it's for, why it matters}
76
79
 
77
80
  ---
78
81
 
82
+ ## Screens & Prototype
83
+ {From `_screens.md`, `prototype/README.md`, `_feedback.md` and the latest sweep in `_sweep.md`,
84
+ row for row — never summarise them. Without a screen inventory or prototype, write
85
+ `No screens were inventoried.` or `No prototype was built.`}
86
+
87
+ ### Screen Inventory
88
+ | ID | Role | Purpose | Surface | Requirements |
89
+ |----|------|---------|---------|--------------|
90
+ | {SCR-nn} | {role} | {purpose} | {REQUIRED / DEFERRED (reason)} | {requirement IDs} |
91
+
92
+ **No screen (still MVP)**
93
+
94
+ | Requirement | Reason |
95
+ |-------------|--------|
96
+ | {requirement ID} | {why it needs no screen} |
97
+
98
+ ### Prototype
99
+ {Version, flows built, UI kit and version, and the gaps the README lists}
100
+
101
+ ### Element Sweep — {Sweep k — date — Prototype vk}
102
+ {Every row of the latest sweep, grouped by screen. `requirement` and `ui` rows are build work;
103
+ `drop` rows are sample data or mockup aids the build must not copy}
104
+
105
+ | Screen | Element | Outcome | Reference |
106
+ |--------|---------|---------|-----------|
107
+ | {SCR-nn} | {page#id} | {covered / requirement / ui / drop} | {requirement IDs or D-ss-n} |
108
+
109
+ ### Prototype Feedback
110
+ {Every row of every round, verbatim}
111
+
112
+ | Round | Anchor | Verdict | Feedback | Processing |
113
+ |-------|--------|---------|----------|------------|
114
+ | {Prototype vk} | {SCR-nn · #id} | {change / keep / drop / question} | {feedback} | {decision or requirement ID} |
115
+
116
+ ---
117
+
79
118
  ## Architecture & Technology
80
119
  {From Session 5: architecture style, tech stack, ADRs, key components}
81
120
 
@@ -172,6 +211,9 @@ list each parked question verbatim, honestly, as a known gap — never silently
172
211
  | Current state & pain points | `.planning/discovery/session-02.md` | `.planning/discovery/_context.md` §2 |
173
212
  | Requirements & user stories | `.planning/discovery/session-03.md` | `.planning/discovery/_context.md` §3 |
174
213
  | UX design principles | `.planning/discovery/session-04.md` | `.planning/discovery/_context.md` §4 |
214
+ | Requirement register | `.planning/discovery/_requirements.md` | `.planning/discovery/session-03.md` |
215
+ | Screens | `.planning/discovery/_screens.md` | `.planning/discovery/session-04.md` |
216
+ | Prototype | `.planning/discovery/prototype/` | `.planning/discovery/_feedback.md`, `.planning/discovery/_sweep.md` |
175
217
  | Architecture & tech stack | `.planning/discovery/session-05.md` | `.planning/discovery/_context.md` §5 |
176
218
  | Security & compliance | `.planning/discovery/session-06.md` | `.planning/discovery/_context.md` §6 |
177
219
  | Roles & identity | `.planning/discovery/session-07.md` | `.planning/discovery/_context.md` §7 |
@@ -218,10 +218,13 @@ Read these files (skip any that don't exist):
218
218
  3. All 13 session files in `.planning/discovery/session-NN.md` — for detail not captured in `_context.md`
219
219
  4. The Project Definition document (glob for `*-Project-Definition.md` in root)
220
220
  5. `.planning/discovery/prototype/README.md` — if a prototype exists (built by `/pp-discovery prototype`), note its state
221
+ 6. `.planning/discovery/_requirements.md`, `.planning/discovery/_screens.md`,
222
+ `.planning/discovery/_feedback.md` and `.planning/discovery/_sweep.md`
221
223
 
222
224
  **Spawn an Explore agent to read all session files in parallel** and extract every
223
225
  populated Notes Template table, all Key Decisions tables, all Action Items, and any
224
- authored content that replaces placeholder text.
226
+ authored content that replaces placeholder text. Read files 5–6 yourself, in full: the agent
227
+ extracts session files only.
225
228
 
226
229
  ### 3b. Synthesize into the handoff document
227
230
 
@@ -336,6 +339,11 @@ project template structure: use "What We're Building" as the description, includ
336
339
  Key Decisions, list requirements in Validated/Active/Out-of-Scope form, and reference
337
340
  Discovery artifacts (the section is added in Step 6a).
338
341
 
342
+ **Init deferral (D-07-03, OD-2).** Before writing the file, Read
343
+ `pp-core/workflows/init/steps/project-md-deferral-narration.md` and follow it — the single
344
+ rendering of the reachability-proof deferral `/pp-init` recorded, silent when none is on
345
+ file. Do not restate its logic here.
346
+
339
347
  **Inline source refs (D-05):** Every Key Decision and every requirement summary carried
340
348
  into PROJECT.md gets a compact trailing ref at its point of use — `(source: session-NN
341
349
  §Section)`, e.g. `Auth uses magic links, not passwords (source: session-05 §Auth Strategy)`
@@ -299,7 +299,7 @@ Read `@~/.claude/pp-core/references/scout-codebase.md` — it contains the phase
299
299
 
300
300
  <step name="dispatch_discuss_pre_hooks">
301
301
  ```bash
302
- DISCUSS_PRE_HOOKS_JSON=$(pp_run loop render-hooks discuss:pre --raw) || exit 1
302
+ DISCUSS_PRE_HOOKS_JSON=$(pp_run loop render-hooks discuss:pre --raw)
303
303
  ```
304
304
  Apply each entry in `activeHooks` per @~/.claude/pp-core/references/loop-hook-dispatch.md. Empty list → continue to `analyze_phase`.
305
305
  </step>
@@ -418,16 +418,9 @@ Write the file.
418
418
 
419
419
  <step name="dispatch_discuss_post_hooks">
420
420
  ```bash
421
- DISCUSS_POST_HOOKS_JSON=$(pp_run loop render-hooks discuss:post --raw) || exit 1
421
+ DISCUSS_POST_HOOKS_JSON=$(pp_run loop render-hooks discuss:post --raw)
422
422
  ```
423
- Apply each entry in `activeHooks` per @~/.claude/pp-core/references/loop-hook-dispatch.md. Empty list → continue to `propose_tier`.
424
- </step>
425
-
426
- <step name="propose_tier">
427
- ```bash
428
- TIER_PROPOSAL=$(pp_run query phase.tier-propose "${phase_number}" --record)
429
- ```
430
- Show `Tier: {effective.tier} — proposed {proposal.tier}: {proposal.reason}` in `confirm_creation`. Never ask; `--tier` on plan-phase overrides.
423
+ Apply each entry in `activeHooks` per @~/.claude/pp-core/references/loop-hook-dispatch.md. Empty list → continue to `confirm_creation`.
431
424
  </step>
432
425
 
433
426
  <step name="confirm_creation">
@@ -0,0 +1,329 @@
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:1106` |
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:1208,1218,1227,1244,1286,1295,1309` |
59
+ | `ship.practices` | `ship-practices` branch (`cmdShipPractices`) | yes — `false` on every report (deterministic, D-04); `true` only on an unrecognised `vault.mode`, answered before any report is computed (13-06, D-08); `false` with status `not_applicable` in standards mode (13-07, D-15) | `src/check-command-router.cts:4619,4629,4637,4647,4696,4736,4915` (CR-01 pass: re-derived again, and this row is now PROBED by the same two-directional predicate as the three below it — the note that used to sit here said "nothing will catch it rotting again", and that was the whole defect. It had rotted twice: at `e5cc07a` it cited `1307,1329,1358,1443`, none inside `cmdShipPractices` — `1307` was a comment.) 13-06: + the `bad-config` emission for an unrecognised `vault.mode`, D-08. 13-07: + the `not_applicable` emission in standards mode, D-15. 16-05: + the bindings-inert emission, the handler's first guard (`hasPlatformBindings`, D-37). 16-09: re-derived with the probe (moved, not extended). 16 review (CR-04, WR-03): re-derived with the probe after `appendSealProblems` began passing the phase's own records and the ledger footer stopped offering a regenerate for a malformed anchor (moved, not extended). 16 review iteration 2 (CR-02, D-98): re-derived with the probe after the SEAL-01 records gained the ancestry-checked directory start and the message its resume footer (moved, not extended). 16 review iteration 3 (CR-01, D-102): re-derived with the probe after the SEAL-01 records gained their attestations (moved, not extended). |
60
+ | `tdd.review-checkpoint` | `tdd-review-checkpoint` branch | yes — `block = violations > 0`, advisory in the capability | `src/check-command-router.cts:967,1061` |
61
+ | `ui.plan-gate` | `ui-plan-gate` branch (`computeUiPlanGate`) | yes — `block = frontend && !hasUiSpec` | `src/check-command-router.cts:742` (emitted at `:766`) |
62
+ | `ui.safety-gate` | `ui-safety-gate` branch (`computeUiSafetyGate`) | yes — `block = frontend && hasUiFiles && !hasUiSpec` | `src/check-command-router.cts:869` (emitted at `:895`) |
63
+ | `vault.audit` | `vault-audit` branch (`cmdVaultAudit`) — declared at **two** points as of 05-06: `ship:pre` and `verify:post`, both `blocking: true` / `onError: halt` / `when: vault.practices_enabled`; since 16-08 also as a `ship:pre` seal gate (`onRecordedArtifacts`) that renders with a switch off over recorded vault artifacts. Those are the two points at which work is finished and a real diff exists; `plan:post` is deliberately excluded, because nothing is built there and every check would return `NOT_MATCHED` (D-05-02). Reads ONE operator flag, `--range <from>..<to>` (08-06, D-08-19), which no enforcer call site appends — it exists so a phase can be audited by hand over a range the operator names; the `--unpaired` pair both points append is still ignored by design, and the positional scanner still needs no allowlist to skip either pair, because it skips the token after ANY flag by rule (04-16 WR-08) | yes — `false` on the inert path and on a clean change set, `true` once any practice check FAILs, and `true` on the three fail-closed argument paths (no positional, unresolvable phase directory, malformed `--range`); also `true` on an unrecognised `vault.mode` (13-06, D-08); `true` with status `disabled_with_artifacts` (kind `disabled_with_artifacts`) when `vault.enabled` or `vault.practices_enabled` is off, standards mode is not active, and the positional's phase holds engine-written vault records (16-09, D-21/D-25), answered before the mode refusal; `false` with status `not_applicable` in standards mode, so neither point halts on it (13-07, D-13) | `src/check-command-router.cts:5164,5190,5194,5202,5214,5234,5255,5275` (05-06: CHECKED by the same two-directional probe as the `vault.trace` row below — every cited line lies inside `cmdVaultAudit` and emits a block, and every emission site is cited) 13-06: + the `bad-config` emission for an unrecognised `vault.mode`, D-08. 13-07: + the `not_applicable` emission in standards mode, D-13. 16-05: + the bindings-inert emission, the handler's first guard (`hasPlatformBindings`, D-37). 16-09: + the `disabled_with_artifacts` emission on the switch-off arm (SEAL-02, D-21/D-25), re-derived with the probe. 16 review (CR-04, WR-03): re-derived with the probe after `appendSealProblems` began passing the phase's own records and the ledger footer stopped offering a regenerate for a malformed anchor (moved, not extended). 16 review iteration 2 (CR-02, D-98): re-derived with the probe after the SEAL-01 records gained the ancestry-checked directory start and the message its resume footer (moved, not extended). 16 review iteration 3 (CR-01, D-102): re-derived with the probe after the SEAL-01 records gained their attestations (moved, not extended). |
64
+ | `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. It is also `true` on an unrecognised `vault.mode` once the vault gate exists, whatever the policy says, answered after the inert branch (13-06, D-08, D-22) | `src/check-command-router.cts:1737,1750,1767,1806,1833,1895` (02-11: CHECKED by the same two-directional probe as the two rows above — 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) 13-06: + the `bad-config` emission for an unrecognised `vault.mode`, D-08. 14-06: + in standards mode 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. |
65
+ | `vault.trace` | `vault-trace` branch (`cmdVaultTrace`) — declared at **four** points, `plan:post`, `execute:post`, `ship:pre` and `verify:post`, all `blocking: true` / `onError: halt`, by **both** capabilities as of 15-06: `vault-practices` (`when: vault.practices_enabled`, `vaultMode: practices`) and `vault` (`when: vault.enabled`, `vaultMode: standards`), so exactly one declaration renders per point per mode (D-03); under an invalid or unresolved mode only the `vault` declaration renders, and it refuses (D-28). The handler branches on `isStandardsModeActive` right after its inert guard (15-08, D-26): in standards mode 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`), and appends `mode: standards` LAST to every envelope after the branch. Reads `--stage` (04-03) and `--unpaired` (04-07, D-10) in both modes; `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, `true` once a practices trace or a standards record fails to validate; also `true` on an unrecognised `vault.mode` (13-06, D-08), which since 15-08 is answered BEFORE the practices kill switch, so the gate the `vault` capability still renders under an invalid mode never passes having checked nothing (D-28, recorded as D-33); in standards mode 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` (kind `disabled_with_artifacts`) when a switch is off on the practices path and the positional's phase (or plan file) holds engine-written vault records (16-09, SEAL-02, D-21); `true` with status `practice_layer_absent` on the practices path when the client vault holds standards (`INDEX.md`) and no practice files (quick 261015-at3, QD-04), evaluated live from the cache after the argument checks | `src/check-command-router.cts:3398,3434,3448,3451,3482,3513,3538,3568,3596,3626,4118,4319,4330` (04-16: corrected and now 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) 13-06: + the `bad-config` emission for an unrecognised `vault.mode`, D-08. 15-08: + the standards verdict's emission, and the five Step-3 argument, phase and enumeration envelopes now leave through the mode-tagging `emit`; every standards emission stays inside `cmdVaultTrace`, so this probe sees all of them (D-31, C-12), and the list was re-derived with the probe's own `handlerRange`/`emitsBlock`. 16-07 (SEAL-01): at the strict points only, in both families, `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, so these numbers were re-derived with the same probe (moved by the step and its kinds), not extended. 16-09 (SEAL-02): + the `disabled_with_artifacts` emission inside the practices switch-off guard, re-derived with the same probe. 16 review (CR-04, WR-03): re-derived with the probe after `appendSealProblems` began passing the phase's own records and the ledger footer stopped offering a regenerate for a malformed anchor (moved, not extended). 16 review iteration 2 (CR-02, D-98): re-derived with the probe after the SEAL-01 records gained the ancestry-checked directory start and the message its resume footer (moved, not extended). 16 review iteration 3 (CR-01, D-102): re-derived with the probe after the SEAL-01 records gained their attestations (moved, not extended). 261015-at3 (QD-04): + the `practice_layer_absent` emission, the live standards-vault refusal, re-derived with the probe. |
66
+ | `verify.codebase-drift` | `verify-codebase-drift` branch (`verify.cmdVerifyCodebaseDrift`) | yes — `block = action_required` | `src/verify.cts:2134,2149,2164,2186,2231,2246` |
67
+ | `verify.schema-drift` | `verify-schema-drift` branch (`verify.cmdVerifySchemaDrift`) | yes — `block = !skipped && driftDetected` | `src/verify.cts:2051,2071,2108` |
68
+
69
+ **Footnote — the missing-field rule.** Every producer above emits a boolean `block` on every
70
+ *successful* path today, but that is an observation, not a guarantee: a producer added later, or an
71
+ `error()` early-return on a missing positional argument, can reach a caller with no `block` field at
72
+ all. Therefore the verdict read below (in step 1's fence, printed for step 2) treats a **missing or
73
+ non-boolean `block` as `false` (advisory)**. An
74
+ absent field must never be coerced into a halt — a gate that stops the build because a producer
75
+ forgot a key is a worse failure than the one it was trying to prevent. Producers that reach the
76
+ enforcer through `error()` are *command* errors and are handled by step 1, never by step 2.
77
+
78
+ ## Guard — inactive capability, zero work
79
+
80
+ Write this branch first, because it is what makes an inactive capability **structurally inert**:
81
+
82
+ - If `GATE_HOOKS_JSON` is empty, unset, or its `activeHooks` array is missing or empty **and**
83
+ `GATE_DIRECT_QUERY` is unset, **skip silently and return to the caller.** Print nothing.
84
+
85
+ A project whose capability is off never reaches any of the logic below. That is the whole inertness
86
+ promise — not a suppressed message, an unreached code path.
87
+
88
+ ## The loop
89
+
90
+ Process each entry of `activeHooks` where `kind == "gate"` **and** a `check.query` is present, in
91
+ array order. Read `activeHooks` in-context; do not pipe it through a shell parser.
92
+
93
+ A caller that handles a specific predicate itself — `ship.md` handles a `security` predicate branch
94
+ above its generic loop — is expected to have handled or skipped that entry **before** Reading this
95
+ file. This enforcer never sees it.
96
+
97
+ For each hook, bind `GATE_QUERY` to `hook.check.query`, `GATE_BLOCKING` to `hook.blocking`, and
98
+ `GATE_ONERROR` to `hook.onError` (absent means `skip`).
99
+
100
+ ## Step 1 — did the check COMMAND succeed?
101
+
102
+ ```bash
103
+ _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
104
+ 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)
105
+ CHECK_EXIT=$?
106
+ 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
107
+ echo "GATE_CHECK=failed"
108
+ else
109
+ 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');}")
110
+ 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('');}")
111
+ echo "GATE_CHECK=ok"
112
+ echo "GATE_BLOCK=$GATE_BLOCK"
113
+ if [ -n "$GATE_MESSAGE" ]; then printf 'GATE_MESSAGE=%s\n' "$GATE_MESSAGE"; fi
114
+ fi
115
+ ```
116
+
117
+ Capture `$?` on the line immediately after the assignment — any command in between overwrites it.
118
+
119
+ **This one fence runs the check AND reads its verdict, and it prints everything the prose below
120
+ routes on** (16 review W-1, D-97). It prints exactly one of two shapes:
121
+
122
+ - `GATE_CHECK=failed` — a **command** error (see below). Nothing else: no verdict is read.
123
+ - `GATE_CHECK=ok`, then `GATE_BLOCK=true` or `GATE_BLOCK=false`, then — only when the envelope
124
+ carries a non-empty `message` — `GATE_MESSAGE=` followed by that message. The message is printed
125
+ last, so everything after `GATE_MESSAGE=` to the end of the output is the message, however many
126
+ lines it has. Every line of it after the first is indented by two spaces (each line break, `\r`
127
+ and Unicode line separator included, becomes a newline plus two spaces, and other control
128
+ characters become spaces), so no message line can begin with `GATE_` or with the halt token: the
129
+ verdict is the one `GATE_BLOCK=` line at column 0, and a message cannot print a second one (16
130
+ review iteration 3 WR-01).
131
+
132
+ The verdict is computed here, in the shell that ran the check, for two measured reasons. **The
133
+ engine:** this is the only fence in this file that carries the launcher preamble (the launcher
134
+ generator keeps ONE preamble per file, in its first `pp_run` fence, and strips any other), so a
135
+ `pp_run` in any later fence of this file, run as its own call, resolves whatever `pp_run` is on
136
+ `PATH` — measured on a host with a global 1.14.0 engine there: the repo engine answered
137
+ `disabled_with_artifacts, block: true`, the PATH engine answered `inactive`, and a blocking gate
138
+ passed. **The parameters:** a second check call depends on the agent re-binding every `GATE_*`
139
+ parameter for it; one that drops `GATE_UNPAIRED=strict` at `ship:pre` or `verify:post` re-runs a
140
+ tolerant check, which skips SEAL-01, and the gate passes having checked less than step 1 did. Step 2
141
+ therefore runs no command at all; it reads what this fence printed.
142
+
143
+ Both positionals use the **conditional-expansion** idiom `${VAR:+"${VAR}"}` deliberately: an unset or
144
+ empty variable expands to *no word at all*, not to an empty string. A bare `"${GATE_POSITIONAL_ARG_2}"`
145
+ would hand every caller that leaves it unset an extra empty argument, which the router would read as a
146
+ present-but-blank slot. The two must stay identical in form for that reason.
147
+
148
+ `GATE_WS` uses the same idiom for the same reason, and it is a **flag pair** rather than a positional.
149
+ A flag pair must be written as **two separate expansions** — `${GATE_WS:+--ws} ${GATE_WS:+"${GATE_WS}"}`
150
+ — so that it emits both words or neither.
151
+
152
+ **Do NOT collapse it to `${GATE_WS:+--ws "${GATE_WS}"}`.** That single-expansion form is bash-specific
153
+ and fails SILENTLY elsewhere. POSIX and bash word-split the unquoted expansion into two arguments;
154
+ **zsh does not**, so the whole thing arrives as one argument `--ws demo-ws`, which the router does not
155
+ recognise as `--ws` and ignores. The gate then answers `status: unresolved / block: true /
156
+ phase_unresolved` on a partitioned project — the exact symptom of the omitted-`--ws` defect this
157
+ parameter exists to prevent, reintroduced at execution time rather than at authoring time. Reproduce
158
+ with `set -- ${V:+--ws "$V"}`: bash reports `argc=2`, zsh reports `argc=1`.
159
+
160
+ This matters because the shell that runs this line is the *dispatching agent's* shell, not this
161
+ document's fence. Several runtimes (macOS Claude Code among them) execute tool commands under zsh.
162
+ The two-expansion form is byte-identical in both shells, so it removes the dependency entirely.
163
+
164
+ Written correctly, flat mode still produces a byte-identical
165
+ command line to the one that shipped before this parameter existed. It is appended after the
166
+ positionals so it can never occupy a positional slot — the router reads `--ws` by name, and a phase
167
+ argument shifted into the wrong slot is the failure mode the two positionals above already warn about.
168
+
169
+ `GATE_STAGE` is the third use of that idiom, and it carries the same two properties for the same two
170
+ reasons: written as the two-expansion pair `${GATE_STAGE:+--stage} ${GATE_STAGE:+"${GATE_STAGE}"}` it
171
+ emits both words or neither, so a caller that sets
172
+ no stage produces a command line byte-identical to the one that shipped before this parameter
173
+ existed — and it carries the same zsh hazard described above, where collapsing it to one expansion
174
+ makes `--stage plan` arrive as a single ignored argument and silently reinstates the stage-blind
175
+ collection defect; and it too is appended after both positionals, so it reaches every gate declared at the
176
+ point without ever displacing a phase argument. A query with no `--stage` handling ignores an
177
+ unrecognised named flag — which is what makes it safe to append to an invocation line that is shared
178
+ by every gate at a loop point, not owned by the one that wanted it.
179
+
180
+ `GATE_UNPAIRED` is the fourth use of that idiom and carries all three of the same properties, for
181
+ the same three reasons: written as `${GATE_UNPAIRED:+--unpaired} ${GATE_UNPAIRED:+"${GATE_UNPAIRED}"}`
182
+ it emits both words or neither, so a caller that sets no strictness produces a command line
183
+ byte-identical to the one that shipped before this parameter existed; collapsing it to the single
184
+ expansion `${GATE_UNPAIRED:+--unpaired "${GATE_UNPAIRED}"}` makes `--unpaired strict` arrive as one
185
+ argument under zsh, which the router does not recognise, so the strict call sites would silently
186
+ degrade to the tolerant default — a gate that reports success because its own flag was dropped; and
187
+ it is appended after both positionals and after both other pairs, so it can never displace a phase
188
+ argument at the three points that pass one.
189
+
190
+ The command has failed when **any** of these holds: `CHECK_EXIT` is non-zero, `GATE_RESULT` is
191
+ empty, or `GATE_RESULT` does not parse as a JSON object. The fence tests exactly that and prints
192
+ `GATE_CHECK=failed`. That is a **command** error and *only* a command error — and so is a run that
193
+ printed **no** `GATE_CHECK=` line at all (the fence died before it, e.g. the launcher found no
194
+ engine): with nothing printed there is no verdict to read. Route it by the hook's `onError`:
195
+
196
+ - `onError == "halt"` → fatal. Print
197
+ `⚠ Gate check command failed (${GATE_POINT} / ${GATE_QUERY}): command error. Resolve before continuing.`,
198
+ run the halt block below, and stop the host workflow.
199
+ - `onError == "skip"` (or absent) → print
200
+ `⚠ Gate check skipped (${GATE_POINT} / ${GATE_QUERY}): check command error.`, move to the next
201
+ hook, and — this is an imperative, not a preference — **do NOT read a verdict.** The fence
202
+ prints none on this path, and none may be inferred.
203
+
204
+ Merging these two steps is the fail-open this gate exists to close: if a non-zero exit were read as
205
+ a verdict, `onError: skip` would swallow a genuine block as a command warning.
206
+
207
+ ## Step 2 — read the verdict
208
+
209
+ Reached **only** when step 1 passed — that is, when it printed `GATE_CHECK=ok`. **Step 2 runs no
210
+ command** — in particular it
211
+ never runs `pp_run check` again. It reads the `GATE_BLOCK=` line and, when present, the
212
+ `GATE_MESSAGE=` text that step 1's fence printed, and nothing else (step 1 explains why: a second
213
+ check call would answer from whatever engine is on `PATH`, with whatever parameters were re-bound).
214
+ The verdict therefore survives the agent's Bash tool starting a fresh shell for every call: what
215
+ crosses from step 1 to step 2 is printed text, never a shell variable.
216
+
217
+ Step 1 extracts the verdict with the same JSON round-trip idiom the sibling `vault-load-gate.md`
218
+ uses. It deliberately uses `node -e` rather than a JSON command-line filter, because no such filter
219
+ is a declared dependency of this project. The `.block === true` comparison **is** the missing-field
220
+ rule from the audit footnote: a missing, null, string or numeric `block` all evaluate to `false`, so
221
+ the fence prints `GATE_BLOCK=false` for them. (Its `catch` arm, which prints `true`, is unreachable
222
+ behind the command-error test; it exists so that a future edit which removes that test fails closed.)
223
+
224
+ - **`GATE_BLOCKING` is `true` AND `GATE_BLOCK` is `true` → HALT.** Print
225
+ `⚠ Gate blocked at ${GATE_POINT} (${GATE_QUERY}). Resolve before continuing.` and run the halt
226
+ block below.
227
+
228
+ **This halt is never bypassed by `onError`.** `onError` governs command errors from step 1 and
229
+ nothing else; it has no authority over the gate's own decision. A gate that reports a real block
230
+ and is then waved through by an error policy is not a gate.
231
+
232
+ - **`GATE_BLOCKING` is `false` → never halt.** If `GATE_BLOCK` is `true`, print one advisory line:
233
+ `ℹ Gate advisory at ${GATE_POINT} (${GATE_QUERY}). Continuing.` Then continue to the next hook.
234
+
235
+ - **`GATE_BLOCK` is `false` → continue.** No status line.
236
+
237
+ **Output-independence rule (a later plan's byte-identical regression pin depends on this).** On
238
+ every NON-halting path, the enforcer's printed output must be **identical** whether `GATE_BLOCKING`
239
+ is `true` or `false`. `blocking` decides one thing only: whether a `true` block halts. It must never
240
+ change what a passing or advisory verdict prints. Otherwise flipping a gate declaration from
241
+ advisory to blocking would change output for projects whose policy key is off — exactly what the
242
+ byte-identical-when-off promise forbids.
243
+
244
+ ## The halt mechanism (D-VH-7, option A — the workflow owns the exit)
245
+
246
+ There are two halves and both ship, because they serve two different observers.
247
+
248
+ **Half one — the named token, for the agent.** Print, on its own line:
249
+
250
+ ```
251
+ PARETO_GATE_BLOCKED:${GATE_QUERY}
252
+ ```
253
+
254
+ On seeing that token the agent stops the host workflow immediately, runs no further step, and
255
+ reports the block to the user.
256
+
257
+ **Half two — the process exit, for the runner.** In the *same* bash block:
258
+
259
+ ```bash
260
+ echo "PARETO_GATE_BLOCKED:${GATE_QUERY}"
261
+ exit 1
262
+ ```
263
+
264
+ An unattended runner or CI job cannot honour a sentence; it can only observe an exit code. Without
265
+ half two a blocked build reports success to the pipeline.
266
+
267
+ `pp_run check` itself still **exits 0 on every path**, by design (`output()` in `src/io.cts` writes
268
+ to fd 1 and returns; it never sets a non-zero code). That is deliberate and must not be "fixed":
269
+ making the check command exit non-zero would turn every block into a step-1 command error, which
270
+ `onError: skip` would then swallow. The exit code belongs to the workflow, not to the check.
271
+
272
+ ## Message passthrough
273
+
274
+ Independently of status, and on **every** run including passes: if step 1 printed a
275
+ `GATE_MESSAGE=` line, print it verbatim — the message is the text after `GATE_MESSAGE=`, to the end
276
+ of step 1's output, its later lines indented by two spaces — it is already operator-ready, possibly
277
+ multi-line text. A gate that has
278
+ something to tell the operator says it on every run, not only when its verdict is bad. This rule is
279
+ preserved word-for-word in effect from the ship.md loop this file replaced; a later plan makes both
280
+ the vault gate's actionable block text and its success-path observability ride on it.
281
+
282
+ ## Direct-query mode
283
+
284
+ Two entry points — the build entry point `fast` and the discovery sprint's
285
+ `discovery-sprint/vault-context.md` subflow — declare no `gsd:loop-host` frontmatter at all. They
286
+ have no rendered hooks, and adding points to them would bump the twelve-point loop-host contract.
287
+ They are supported here rather than in a third copy of the halt logic. The count in this sentence is
288
+ pinned against the `direct` rows of the Caller contract below, so a third caller must update both.
289
+ (`execute-plan` left this mode in 16-10 and `quick` in 16-11: each renders its own loop points and
290
+ filters the payload to the vault capabilities, so they are hook rows below.)
291
+
292
+ When `GATE_HOOKS_JSON` is empty **and** `GATE_DIRECT_QUERY` is set to a query name, synthesise a
293
+ single in-memory hook entry — that query, `blocking: true`, `onError: skip` — and run the identical
294
+ loop above over it.
295
+
296
+ **A direct caller must perform its own activation guard before Reading this file.** A synthesised
297
+ hook has not been through the registry's `when` evaluation and therefore carries none of its
298
+ inertness. The two guards such a caller must run first are:
299
+
300
+ 1. the **platform-bindings presence probe** (`pp_run query platform-bindings --pick present`; when
301
+ not `true`, do not Read this file at all — non-Pareto project, fully inert), and
302
+ 2. the **admission verb** (`pp_run vault admission`, D-28; `on` → Read this file, `off` or `inert`
303
+ → do not, anything else halts with `PARETO_GATE_BLOCKED:vault.admission`, D-05). Both direct
304
+ callers, `fast` (16-13) and the discovery subflow (16-04), run it.
305
+
306
+ ## Caller contract
307
+
308
+ | Caller | Mode | Sets |
309
+ |--------|------|------|
310
+ | `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 |
311
+ | `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>` |
312
+ | `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 |
313
+ | `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 |
314
+ | `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 |
315
+ | `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 |
316
+ | `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>` |
317
+ | `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 |
318
+ | `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) |
319
+ | `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 |
320
+ | `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) |
321
+ | `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) |
322
+ | `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` and `vault-practices` capabilities, 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 |
323
+ | `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` and `vault-practices` capabilities, 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` |
324
+ | `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 |
325
+ | `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` |
326
+ | `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) |
327
+ | `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` and `vault-practices` capabilities, D-30), after its own admission fence. Sets **no** positional, **no** `GATE_WS` and **no** `GATE_STAGE`: `vault.status` reads project state |
328
+ | `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 |
329
+ | `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.