@pareto4/pp-core 1.13.0 → 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 (201) hide show
  1. package/.claude-plugin/plugin.json +3 -3
  2. package/README.md +26 -9
  3. package/agents/pp-code-reviewer.md +2 -1
  4. package/agents/pp-debug-session-manager.md +9 -6
  5. package/agents/pp-executor.md +10 -13
  6. package/agents/pp-plan-checker.md +4 -4
  7. package/agents/pp-planner.md +23 -9
  8. package/agents/pp-vault-reviewer.md +244 -21
  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 +1 -1
  13. package/commands/pp-plan-phase.md +1 -2
  14. package/commands/pp-vault-review.md +5 -3
  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 +396 -82
  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 +3334 -946
  35. package/pp-core/bin/lib/clusters.cjs +3 -0
  36. package/pp-core/bin/lib/command-aliases.cjs +0 -35
  37. package/pp-core/bin/lib/commands.cjs +9 -4
  38. package/pp-core/bin/lib/compose-bindings.cjs +251 -88
  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 +4 -4
  43. package/pp-core/bin/lib/discovery-vault-check.cjs +246 -0
  44. package/pp-core/bin/lib/frontmatter.cjs +1 -38
  45. package/pp-core/bin/lib/git-base-branch.cjs +13 -11
  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 +648 -738
  49. package/pp-core/bin/lib/init-interactive.cjs +76 -4
  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 +189 -129
  54. package/pp-core/bin/lib/org-profile.cjs +469 -152
  55. package/pp-core/bin/lib/package-identity.cjs +3 -3
  56. package/pp-core/bin/lib/phase-command-router.cjs +1 -29
  57. package/pp-core/bin/lib/phase-scope.cjs +1 -1
  58. package/pp-core/bin/lib/phase.cjs +11 -253
  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 +650 -126
  66. package/pp-core/bin/lib/vault-command-router.cjs +2604 -367
  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 +139 -72
  71. package/pp-core/bin/lib/vault-review.cjs +194 -451
  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 +607 -254
  75. package/pp-core/bin/lib/vault-update.cjs +9 -33
  76. package/pp-core/bin/lib/vault-write-back.cjs +70 -42
  77. package/pp-core/bin/lib/vault.cjs +2734 -920
  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 -54
  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 +2 -2
  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 -25
  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 +17 -34
  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 +20 -23
  100. package/pp-core/workflows/_runtime-launcher.snippet.sh +0 -0
  101. package/pp-core/workflows/add-tests.md +55 -4
  102. package/pp-core/workflows/audit-fix.md +63 -2
  103. package/pp-core/workflows/audit-milestone.md +1 -1
  104. package/pp-core/workflows/autonomous.md +85 -11
  105. package/pp-core/workflows/check-todos.md +13 -2
  106. package/pp-core/workflows/code-review-fix.md +59 -3
  107. package/pp-core/workflows/code-review.md +10 -1
  108. package/pp-core/workflows/complete-milestone.md +1 -1
  109. package/pp-core/workflows/debug.md +54 -4
  110. package/pp-core/workflows/discovery-sprint/prefill.md +19 -7
  111. package/pp-core/workflows/discovery-sprint/prototype.md +46 -49
  112. package/pp-core/workflows/discovery-sprint/session-13-gates.md +2 -0
  113. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +1 -3
  114. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +1 -3
  115. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +1 -3
  116. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +20 -24
  117. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +1 -3
  118. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +1 -3
  119. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +1 -3
  120. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +1 -3
  121. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +1 -3
  122. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +1 -6
  123. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +1 -3
  124. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +1 -3
  125. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -3
  126. package/pp-core/workflows/discovery-sprint/templates/context-template.md +1 -1
  127. package/pp-core/workflows/discovery-sprint/vault-context.md +213 -0
  128. package/pp-core/workflows/discovery-sprint.md +7 -3
  129. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +4 -4
  130. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +43 -1
  131. package/pp-core/workflows/discovery-to-build.md +10 -2
  132. package/pp-core/workflows/discuss-phase.md +3 -10
  133. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +329 -0
  134. package/pp-core/workflows/execute-phase/steps/client-context-gate.md +3 -2
  135. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -1
  136. package/pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md +98 -0
  137. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
  138. package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +6 -8
  139. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +115 -14
  140. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +322 -20
  141. package/pp-core/workflows/execute-phase/steps/verifier-practice-payload.md +165 -0
  142. package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +163 -0
  143. package/pp-core/workflows/execute-phase.md +155 -128
  144. package/pp-core/workflows/execute-plan.md +56 -6
  145. package/pp-core/workflows/fast.md +53 -4
  146. package/pp-core/workflows/help/modes/full.md +3 -3
  147. package/pp-core/workflows/init/steps/client-context-capture.md +101 -0
  148. package/pp-core/workflows/init/steps/gitlab-provisioning.md +130 -0
  149. package/pp-core/workflows/init/steps/project-md-deferral-narration.md +66 -0
  150. package/pp-core/workflows/init/steps/reachability-outcome.md +70 -0
  151. package/pp-core/workflows/init.md +232 -272
  152. package/pp-core/workflows/new-project.md +31 -23
  153. package/pp-core/workflows/plan-phase.md +98 -45
  154. package/pp-core/workflows/quick.md +117 -3
  155. package/pp-core/workflows/secure-phase.md +1 -1
  156. package/pp-core/workflows/ship.md +57 -28
  157. package/pp-core/workflows/undo.md +1 -1
  158. package/pp-core/workflows/update.md +3 -3
  159. package/pp-core/workflows/validate-phase.md +58 -3
  160. package/pp-core/workflows/vault-review.md +282 -22
  161. package/pp-core/workflows/verify-work.md +12 -14
  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 -11
  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 +12 -10
  173. package/scripts/practice-arithmetic-contract.allow.json +35 -0
  174. package/scripts/prompt-injection-scan.sh +22 -17
  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 -483
  184. package/pp-core/bin/lib/install-run.cjs +0 -536
  185. package/pp-core/bin/lib/loop-gates.cjs +0 -213
  186. package/pp-core/bin/lib/phase-manifest.cjs +0 -527
  187. package/pp-core/bin/lib/phase-tier.cjs +0 -480
  188. package/pp-core/bin/lib/plan-graph.cjs +0 -368
  189. package/pp-core/references/executor-vault-trace.md +0 -47
  190. package/pp-core/references/planner-plan-graph.md +0 -66
  191. package/pp-core/workflows/discovery-sprint/vault-fetch.md +0 -37
  192. package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +0 -45
  193. package/pp-core/workflows/execute-phase/steps/install-run.md +0 -29
  194. package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +0 -23
  195. package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +0 -73
  196. package/pp-core/workflows/execute-phase/steps/vault-door-gate.md +0 -19
  197. package/pp-core/workflows/plan-phase/steps/phase-tier.md +0 -43
  198. package/scripts/lib/size-budget.cjs +0 -311
  199. package/scripts/lint-neutrality-names.cjs +0 -694
  200. package/scripts/lint-org-coordinates.allowlist.json +0 -35
  201. package/scripts/lint-org-coordinates.cjs +0 -426
@@ -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
@@ -1,25 +1,126 @@
1
- # Vault fetch + summary (plan:pre and execute:pre step dispatch)
1
+ # vault fetch + summary — the bespoke step dispatch, served at `plan:pre` and `execute:pre`
2
2
 
3
- Capability-driven dispatch for an active step hook where `kind == "step"` and `capId == "vault"`, at plan:pre (hooks in `PLAN_PRE_HOOKS_JSON`) or execute:pre (hooks in `EXECUTE_PRE_HOOKS_JSON`). Runs `vault fetch` then `vault summary` so `.planning/vault/SUMMARY.md` exists on the researcher and planner read paths, and so the `vault.status` gate that follows judges a fresh fetch. The generic step contract dispatches only `ref.skill`/`ref.agent` hooks — not `ref.command` — so this bespoke block owns the `vault fetch` command run. The registry evaluates `when: vault.enabled`; an inert capability yields no hook. `onError: skip` — a failed fetch logs a warning and continues; the gate after it decides whether a missing client vault may proceed (it stops only when `vault.required` is true).
3
+ Capability-driven dispatch for an active vault step hook where `kind == "step"` and `capId == "vault"`. The `vault` capability declares that step at **two** loop points — `plan:pre` and `execute:pre` — and this one file serves both, Read from ten call sites — nine hook-mode callers at those two points and one direct-mode caller with no loop point (see the Direct mode paragraph below); the caller table below names them. Runs `vault fetch`, which now writes `.planning/vault/SUMMARY.md` itself (STRICT-05). What that buys differs by point and both reasons are real: at `plan:pre` the summary exists on the researcher and planner read paths before the researcher `Agent()` spawn; at `execute:pre` the fetch is attempted *before* the gate decides, so a fresh clone or a CI runner with a reachable vault fetches and passes rather than being blocked for a cache it never had a chance to populate (GATE-03). The generic step contract dispatches only `ref.skill`/`ref.agent` hooks — not `ref.command` — so this bespoke block owns the `vault fetch` command run. The registry evaluates `when: vault.enabled`; an inert capability yields no hook.
4
4
 
5
- Read the active step hook from the point's hooks JSON (in-context) where `kind == "step"` and `capId == "vault"`.
5
+ ## Callers — bind `VAULT_FETCH_HOOKS_JSON` (hook mode) or set `VAULT_FETCH_DIRECT` (direct mode) before Reading this file
6
6
 
7
- **If no active vault step hook exists:** skip (`vault.enabled` is not true). From a side door (`vault-door-gate.md`), `DOOR_VAULT=true` is the active step hook.
7
+ `VAULT_FETCH_HOOKS_JSON` is a **parameter**, in exactly the sense `check-query-gate.md`'s
8
+ `GATE_HOOKS_JSON` is one: this file never renders hooks itself, and the caller sets it to the
9
+ `activeHooks` payload the caller already rendered for **its own** loop point. This file therefore
10
+ names no caller's payload variable anywhere, deliberately — it is Read from ten places, nine at
11
+ two loop points and one with no loop point at all, so any one variable name is dead at the other
12
+ nine, and the guard's next instruction is *skip*. A step file that hard-names one caller's variable
13
+ does not merely mis-document the other callers; it silently turns their fetch off
14
+ (02-VERIFICATION, G-2).
8
15
 
9
- **If an active vault step hook exists** (recognizable by `capId == "vault"` + `ref.command == "vault fetch"`):
16
+ | Caller | Loop point | Binding |
17
+ |--------|------------|---------|
18
+ | `pp-core/workflows/plan-phase.md`, `### plan:pre vault fetch + summary` | `plan:pre` | `VAULT_FETCH_HOOKS_JSON` ← the `plan:pre` payload that section already has in scope |
19
+ | `pp-core/workflows/autonomous.md`, §3a.5-i, the `ref.command` + `capId == "vault"` branch | `plan:pre` | `VAULT_FETCH_HOOKS_JSON` ← the `plan:pre` payload §3a.5 rendered |
20
+ | `pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md`, step 1 "Fetch first" | `execute:pre` | `VAULT_FETCH_HOOKS_JSON` ← the `execute:pre` payload that dispatcher already reads its `activeHooks` from |
21
+ | `pp-core/workflows/quick.md`, Step 2.4 (16-11) | `plan:pre` | `VAULT_FETCH_HOOKS_JSON` ← quick's `plan:pre` payload, filtered in the fence to the `vault` and `vault-practices` capabilities, after its own admission fence |
22
+ | `pp-core/workflows/debug.md`, § 0b Vault admission and load gate (16-14) | `plan:pre` | `VAULT_FETCH_HOOKS_JSON` ← debug's `plan:pre` payload, filtered in the fence to the `vault` and `vault-practices` capabilities, after its own admission fence |
23
+ | `pp-core/workflows/code-review-fix.md`, `vault_admission_gate` (16-15) | `execute:pre` | `VAULT_FETCH_HOOKS_JSON` ← the door's `execute:pre` payload, filtered in the fence to the `vault` and `vault-practices` capabilities, after its own admission fence |
24
+ | `pp-core/workflows/validate-phase.md`, § 0.5 (16-15) | `execute:pre` | `VAULT_FETCH_HOOKS_JSON` ← the door's `execute:pre` payload, filtered in the fence to the `vault` and `vault-practices` capabilities, after its own admission fence |
25
+ | `pp-core/workflows/add-tests.md`, `vault_admission_gate` (16-15) | `execute:pre` | `VAULT_FETCH_HOOKS_JSON` ← the door's `execute:pre` payload, filtered in the fence to the `vault` and `vault-practices` capabilities, after its own admission fence |
26
+ | `pp-core/workflows/audit-fix.md`, `vault_admission_gate` (16-15) | `execute:pre` | `VAULT_FETCH_HOOKS_JSON` ← the door's `execute:pre` payload, filtered in the fence to the `vault` and `vault-practices` capabilities, after its own admission fence |
27
+ | `pp-core/workflows/discovery-sprint/vault-context.md`, § 2 "Fetch" (Read from Flow 2 Step 1 and prefill Step 0 of the discovery sprint) | none — direct mode (D-11-09) | `VAULT_FETCH_DIRECT=true`; the payload parameter is left empty; the two activation guards run first, in order |
28
+
29
+ A **next caller** (the eleventh) inherits one obligation and one only: bind this parameter from the payload it
30
+ rendered for its own point, before the Read — or, when it has no loop point, run the two activation
31
+ guards and set `VAULT_FETCH_DIRECT` (Direct mode below). Nothing in this file needs to change to
32
+ accommodate it — that is the whole reason both parameters exist instead of a sentence naming a
33
+ caller's variable.
34
+
35
+ The step's static `onError: skip` remains the **registry-level dispatch policy** and is unchanged. Layered on top of it is a **runtime** decision this block makes for itself: when `vault.required` is true, a failed fetch or an unwritten summary hard-stops with `PARETO_GATE_BLOCKED:vault.fetch` and a non-zero exit (STRICT-03, GATE-09). A static `onError` cannot depend on a config value, which is why the arm lives here rather than in the capability declaration. With `vault.required` false — the default — every path below behaves exactly as it did before: a warning, and the run continues.
36
+
37
+ **Admission first (D-28, D-05).** This check runs before the hook-presence check below because a loop-point host renders no vault hook at all on a config the gate cannot read, so without it a direct or unconditional caller would skip the fetch silently. It is inert without `.platform-bindings.json` (a probe that cannot answer, such as on a malformed bindings file, defers to the verb, which checks bindings first itself); `on`, `off` and `inert` continue to the hook check exactly as before, and anything else (`config-unreadable`, or no answer) halts naming the file and the repair (research C-4).
10
38
 
11
39
  ```bash
12
40
  _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
- # vault fetch: populates .planning/vault/ cache + manifest
14
- if ! pp_run vault fetch 2>&1; then
41
+ VAULT_ADMIT_PRESENT=$(pp_run query platform-bindings --pick present 2>/dev/null || echo true)
42
+ VAULT_ADMIT=inert
43
+ if [ "$VAULT_ADMIT_PRESENT" = true ]; then VAULT_ADMIT=$(pp_run vault admission 2>/dev/null || echo unrecognised); fi
44
+ 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
45
+ ```
46
+
47
+ Read the active vault step hook from `VAULT_FETCH_HOOKS_JSON` (in-context) where `kind == "step"` and `capId == "vault"`. A caller binding this payload to `$X` means the JSON on the `X=` line the caller's render fence PRINTED (D-99): a payload a fence only assigned is invisible in the next call.
48
+
49
+ **If no active vault step hook exists** and `VAULT_FETCH_DIRECT` is not the literal `true`: skip (non-Pareto project, `vault.enabled` false, or vault bindings absent).
50
+
51
+ **Direct mode — `VAULT_FETCH_DIRECT` (D-11-09):** consulted **only** when `VAULT_FETCH_HOOKS_JSON` is empty, mirroring the `GATE_DIRECT_QUERY` parameter `check-query-gate.md` already ships for build entry points that declare no loop-host frontmatter. A caller with no loop point cannot render a payload at all: the `vault` capability declares this step at exactly `plan:pre` and `execute:pre`, and the twelve canonical loop points are frozen by the loop-host contract, so there is no third point such a caller could render and no payload for it to bind. Setting this parameter to the literal `true` makes the block below run exactly as it does for an active hook — the same `vault fetch`, the same `.planning/vault/SUMMARY.md` write, the same three arms and the same required-miss halt token and non-zero exit — so a direct caller inherits the first of the two `plan:pre` halt surfaces from its owner rather than authoring a fourth copy of it (D-11-11). A direct caller MUST run the two activation guards itself first, in this order: `pp_run query platform-bindings --pick present` (when not `true`, do not Read this file at all — non-Pareto project, fully inert), then `pp_run vault admission` (16-04, D-28: on `on`, Read this file; on `off` or `inert`, do not Read it; on anything else, `config-unreadable` or no answer, print `pp_run vault admission --explain` and halt with `PARETO_GATE_BLOCKED:vault.admission` and a non-zero exit, because a config the gate cannot read is not a vault that is off). A direct dispatch has not been through the registry's `when` evaluation and therefore carries none of its inertness, which is the same obligation `check-query-gate.md` § Direct-query mode places on a direct caller of the gate. Hook-mode callers are unaffected: with a non-empty payload this parameter is never read, so all hook-mode callers behave exactly as they did before it existed.
52
+
53
+ The block below also records the verdict before and after the fetch and says so when the fetch changed it (D-08-08): the plan:pre / execute:pre ordering is fetch-then-gate (`plan-phase.md` §5.1 then §5.6; `execute-pre-dispatch.md` steps 1-2), so a by-hand `check vault.status` taken earlier can legitimately disagree with the gate — the disagreement is temporal, and this line is what makes it explicable (D-08-05). Both snapshots are pure reads (D-08-04): they report the manifest, they never fetch, and they add nothing at all on a run that does not reach the block — an inactive hook with no direct-mode caller — because they live inside the run condition above and nowhere else.
54
+
55
+ **If an active vault step hook exists** (recognizable by `capId == "vault"` + `ref.command == "vault fetch"`) — or `VAULT_FETCH_DIRECT` is `true` with an empty payload:
56
+
57
+ ```bash
58
+ # vault.required — read ONCE, with an explicit default (STRICT-03). The key is
59
+ # declared in `capabilities/vault/capability.json`, so this read returns the
60
+ # project's configured value; with it set true the hard-stop arm below is live.
61
+ VAULT_REQUIRED=$(pp_run query config-get vault.required --raw --default false 2>/dev/null || echo false)
62
+
63
+ # vault fetch: populates .planning/vault/ cache + manifest AND writes
64
+ # .planning/vault/SUMMARY.md itself. The separate `vault summary` subcommand
65
+ # call that used to live here is deliberately GONE (STRICT-05): the fetch and the
66
+ # summary were paired only by this markdown, so a direct CLI fetch, an --auto run
67
+ # that never dispatched this step, or an agent skipping the second instruction
68
+ # each produced a populated cache with no SUMMARY.md. The pairing is now code.
69
+ # D-08-08: a pure read of the manifest as it stands BEFORE this fetch rewrites
70
+ # it. This is the reading an operator's own by-hand `check vault.status` would
71
+ # have produced a moment ago, captured here because nothing downstream can
72
+ # reconstruct it once the fetch has landed.
73
+ VAULT_PRE_VERDICT=$(pp_run check vault.status --raw 2>/dev/null || echo '{}')
74
+ VAULT_FETCH_JSON=$(pp_run vault fetch --raw 2>/dev/null || echo '{}')
75
+ VAULT_SOURCE=$(printf '%s' "$VAULT_FETCH_JSON" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{process.stdout.write(JSON.parse(d).source||'');}catch{process.stdout.write('');}")
76
+ VAULT_SUMMARY_WRITTEN=$(printf '%s' "$VAULT_FETCH_JSON" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{process.stdout.write(String(JSON.parse(d).summaryWritten===true));}catch{process.stdout.write('false');}")
77
+ VAULT_SUMMARY_ERROR=$(printf '%s' "$VAULT_FETCH_JSON" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{process.stdout.write(String(JSON.parse(d).summaryError||''));}catch{process.stdout.write('');}")
78
+ VAULT_FETCH_REASON=$(printf '%s' "$VAULT_FETCH_JSON" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const p=JSON.parse(d);process.stdout.write(String(p.fallbackReason||p.warning||''));}catch{process.stdout.write('');}")
79
+
80
+ # D-08-08: the same pure read AFTER the fetch, and a comparison of the five
81
+ # fields that decide the gate. Values are envelope fields this file's siblings
82
+ # already print; `reason` is a frozen code. Pipes and newlines are stripped and
83
+ # each summary is cut at 300 characters, because the two summaries are carried
84
+ # through a single pipe-delimited string that `cut` then splits.
85
+ VAULT_POST_VERDICT=$(pp_run check vault.status --raw 2>/dev/null || echo '{}')
86
+ VAULT_VERDICT_DIFF=$(PRE="$VAULT_PRE_VERDICT" POST="$VAULT_POST_VERDICT" node -e "const F=['tier','ref','stale','block','reason'];const g=(s)=>{let p={};try{p=JSON.parse(s)||{};}catch{p={};}return F.map((k)=>k+'='+String(p[k]===undefined||p[k]===null?'':p[k])).join(' ').replace(/[\r\n|]/g,'').slice(0,300);};const a=g(process.env.PRE||'');const b=g(process.env.POST||'');process.stdout.write(a===b?'same||':'changed|'+a+'|'+b);")
87
+ VAULT_VERDICT_CHANGED=$(printf '%s' "$VAULT_VERDICT_DIFF" | cut -d'|' -f1)
88
+ VAULT_VERDICT_BEFORE=$(printf '%s' "$VAULT_VERDICT_DIFF" | cut -d'|' -f2)
89
+ VAULT_VERDICT_AFTER=$(printf '%s' "$VAULT_VERDICT_DIFF" | cut -d'|' -f3)
90
+ if [ "$VAULT_VERDICT_CHANGED" = "changed" ]; then
91
+ echo "ℹ vault fetch changed the gate verdict: before — ${VAULT_VERDICT_BEFORE}; after — ${VAULT_VERDICT_AFTER}. A check vault.status read taken before this fetch described the earlier state (D-08-08)."
92
+ fi
93
+
94
+ # Read the OUTCOME, never the exit code: `pp-tools vault ...` exits 0 on every
95
+ # path by design (D-VH-7 — the command reports, the workflow halts), so the old
96
+ # `if ! pp_run vault fetch` guard could not take its failure branch.
97
+ VAULT_FETCH_FAILED=false
98
+ if [ -z "$VAULT_SOURCE" ] || [ "$VAULT_SOURCE" = "miss" ]; then VAULT_FETCH_FAILED=true; fi
99
+ VAULT_LOAD_OK=true
100
+ if [ "$VAULT_FETCH_FAILED" = "true" ] || [ "$VAULT_SUMMARY_WRITTEN" != "true" ]; then VAULT_LOAD_OK=false; fi
101
+
102
+ if [ "$VAULT_REQUIRED" = "true" ] && [ "$VAULT_LOAD_OK" = "false" ]; then
103
+ # Arm 3 — vault.required is on: a miss, a failed fetch and an unwritable
104
+ # summary all hard-stop (D-VH-4). Print the failure verbatim, then the named
105
+ # halt token and a non-zero exit — the same two halves the extracted enforcer
106
+ # uses, so an agent stops and an unattended runner sees the exit code.
107
+ echo "✗ vault could not be loaded and vault.required is true (STRICT-03)" >&2
108
+ if [ -n "$VAULT_FETCH_REASON" ]; then echo " fetch: $VAULT_FETCH_REASON" >&2; fi
109
+ if [ -n "$VAULT_SUMMARY_ERROR" ]; then echo " summary: $VAULT_SUMMARY_ERROR" >&2; fi
110
+ echo "PARETO_GATE_BLOCKED:vault.fetch"
111
+ exit 1
112
+ elif [ "$VAULT_FETCH_FAILED" = "true" ]; then
113
+ # Arm 1 — fetch failed, vault.required off. Same effect as before this plan.
15
114
  echo "⚠ vault fetch failed — continuing without vault context (onError:skip)" >&2
115
+ if [ -n "$VAULT_FETCH_REASON" ]; then echo " reason: $VAULT_FETCH_REASON" >&2; fi
116
+ elif [ "$VAULT_SUMMARY_WRITTEN" != "true" ]; then
117
+ # Arm 2 — fetch succeeded, the summary did not reach disk. STRICT-05: this is
118
+ # the branch the confirmation line used to fall through to unconditionally,
119
+ # pointing agents at a SUMMARY.md that was never written.
120
+ echo "⚠ vault summary not written — .planning/vault/SUMMARY.md is absent (onError:skip)" >&2
121
+ if [ -n "$VAULT_SUMMARY_ERROR" ]; then echo " reason: $VAULT_SUMMARY_ERROR" >&2; fi
16
122
  else
17
- # vault summary: writes .planning/vault/SUMMARY.md — the bounded index
18
- # that planner, researcher, and executor pointers reference.
19
- if ! pp_run vault summary 2>&1; then
20
- echo "⚠ vault summary failed — SUMMARY.md may be absent (onError:skip)" >&2
21
- else
22
- echo "✓ vault fetch + summary complete: .planning/vault/SUMMARY.md"
23
- fi
123
+ # The claim is now reachable ONLY when summaryWritten is true.
124
+ echo "✓ vault fetch + summary complete: .planning/vault/SUMMARY.md"
24
125
  fi
25
126
  ```
@@ -1,28 +1,330 @@
1
- # Vault load gate (plan:pre + execute:pre)
1
+ # Vault load gate — state rendering (plan:pre + execute:pre)
2
2
 
3
- One file serves both call sites: the host sets `VAULT_GATE_POINT` to `plan` (plan:pre) or
4
- `execute` (execute:pre) before executing this step. The verdict, its wording and whether it
5
- blocks are decided by the CLI — `computeVaultVerdict` is the only owner. This step relays the
6
- card verbatim and reads one field; it never re-derives any part of the verdict.
3
+ Renders the vault load state for the operator before planning or build. The host sets
4
+ `VAULT_GATE_POINT` to `plan` (for plan:pre) or `execute` (for execute:pre) before
5
+ executing this step — **one file serves both call sites** (D-05). The §8 message table
6
+ (design §8) column is selected by this parameter.
7
+
8
+ **This file decides nothing.** The halt belongs to
9
+ `pp-core/workflows/execute-phase/steps/check-query-gate.md`, the single generic `check.query`
10
+ enforcer, which both call sites run *before* reading this file (GATE-02, D-VH-7). There is exactly
11
+ one halt implementation in the repo and this is not it. Deciding and rendering are separated on
12
+ purpose: if you are here because a gate did not stop a build, the enforcer is where to look. Do not
13
+ add a halt to this file.
14
+
15
+ Gated entirely on `.platform-bindings.json`: when no bindings file is present, this gate
16
+ is a **no-op** — fully inert for non-Pareto projects. The verdict is read from
17
+ `pp_run check vault.status --raw` — a pure manifest read that never fetches, never errors,
18
+ and degrades to `{"present":false}` on any problem.
7
19
 
8
20
  ```bash
9
21
  _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
10
- VAULT_CARD=$(pp_run check vault.status --card --point "${VAULT_GATE_POINT:-plan}") || exit 1
11
- VAULT_ACTION=$(pp_run check vault.status --pick action) || exit 1
12
- [ -n "$VAULT_ACTION" ] || exit 1
13
- [ -z "$VAULT_CARD" ] || printf '%s\n' "$VAULT_CARD"
14
- echo "VAULT_ACTION=$VAULT_ACTION"
15
- [ "$VAULT_ACTION" != stop ] || exit 1
22
+ VAULT_GATE_PRESENT=$(pp_run query platform-bindings --pick present 2>/dev/null || echo true)
23
+ ```
24
+
25
+ **If `VAULT_GATE_PRESENT` != `true`:** skip entirely (non-Pareto project — fully inert). A probe
26
+ that cannot answer (a malformed bindings file exits 1) reads as present, as `hasPlatformBindings`
27
+ does (16-04 D-47 b), so the card renders the verdict instead of vanishing.
28
+
29
+ ```bash
30
+ VAULT_VERDICT=$(pp_run check vault.status --raw 2>/dev/null || echo '{"present":false}')
31
+ VAULT_STATUS=$(printf '%s' "$VAULT_VERDICT" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{process.stdout.write(JSON.parse(d).status||'miss');}catch{process.stdout.write('miss');}")
32
+ VAULT_TIER=$(printf '%s' "$VAULT_VERDICT" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{process.stdout.write(JSON.parse(d).tier||'');}catch{process.stdout.write('');}")
33
+ VAULT_FALLBACK_REASON=$(printf '%s' "$VAULT_VERDICT" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{process.stdout.write(JSON.parse(d).fallbackReason||'');}catch{process.stdout.write('');}")
34
+ VAULT_REPO=$(printf '%s' "$VAULT_VERDICT" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const p=JSON.parse(d);process.stdout.write(p.repo||p.source||'');}catch{process.stdout.write('');}")
35
+ VAULT_REF=$(printf '%s' "$VAULT_VERDICT" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{process.stdout.write(JSON.parse(d).ref||'');}catch{process.stdout.write('');}")
36
+ VAULT_SHA=$(printf '%s' "$VAULT_VERDICT" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const s=JSON.parse(d).resolvedSha||'';process.stdout.write(s.slice(0,8));}catch{process.stdout.write('');}")
37
+ VAULT_FILE_COUNT=$(printf '%s' "$VAULT_VERDICT" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{process.stdout.write(String(JSON.parse(d).fileCount??0));}catch{process.stdout.write('0');}")
38
+ VAULT_STALE=$(printf '%s' "$VAULT_VERDICT" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{process.stdout.write(String(!!JSON.parse(d).stale));}catch{process.stdout.write('false');}")
39
+ VAULT_FETCHED_AT=$(printf '%s' "$VAULT_VERDICT" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{process.stdout.write(JSON.parse(d).fetchedAt||'');}catch{process.stdout.write('');}")
40
+ # 14-07 (D-21/D-25): the engine's operator message, relayed verbatim by the degraded row below — one text, one owner.
41
+ VAULT_MESSAGE=$(printf '%s' "$VAULT_VERDICT" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const m=JSON.parse(d).message;process.stdout.write(typeof m==='string'?m:'');}catch{process.stdout.write('');}")
42
+ # D-08-27: the CONFIGURED client coordinates, read straight from
43
+ # `.platform-bindings.json`. This is a PURE BINDINGS READ: the verdict envelope is
44
+ # deliberately not consulted for it, because with `vault.required` off the envelope
45
+ # carries no drift field by design (STRICT-01), and D-08-29 rules the drift fact is
46
+ # rendered as prose in this file rather than added as a new envelope key.
47
+ VAULT_BINDINGS_JSON=$(pp_run query platform-bindings --raw 2>/dev/null || echo '{}')
48
+ VAULT_CONFIGURED_REPO_SET=$(printf '%s' "$VAULT_BINDINGS_JSON" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const c=JSON.parse(d).client;process.stdout.write(c&&typeof c.vaultRepo==='string'&&c.vaultRepo.trim()?'true':'false');}catch{process.stdout.write('false');}")
49
+ # Mirrors `resolveClientCoordinates` (src/vault-coordinates.cts): `client.vaultRef`
50
+ # trimmed, or `main` when a client repo is configured with no ref, or empty when no
51
+ # client repo is configured at all — so the ref rendered below is the same one the
52
+ # verdict compares against. T-08-39: the value is project-authored, but it is
53
+ # interpolated into an agent's context, so bound it — charset `[A-Za-z0-9._/-]`,
54
+ # cut at 100 characters.
55
+ VAULT_CONFIGURED_REF=$(printf '%s' "$VAULT_BINDINGS_JSON" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const c=JSON.parse(d).client;const set=!!(c&&typeof c.vaultRepo==='string'&&c.vaultRepo.trim());const r=set?((typeof c.vaultRef==='string'?c.vaultRef.trim():'')||'main'):'';process.stdout.write(r.replace(/[^A-Za-z0-9._\/-]/g,'').slice(0,100));}catch{process.stdout.write('');}")
56
+ VAULT_CONFIGURED_REF_RAW=$(printf '%s' "$VAULT_BINDINGS_JSON" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const c=JSON.parse(d).client;const set=!!(c&&typeof c.vaultRepo==='string'&&c.vaultRepo.trim());const r=set?((typeof c.vaultRef==='string'?c.vaultRef.trim():'')||'main'):'';process.stdout.write(r);}catch{process.stdout.write('');}")
57
+ VAULT_CONFIGURED_REF_TEXT="${VAULT_CONFIGURED_REF:-(none configured)}"
58
+ # D-08-24 / D-08-29: ref drift — a client-tier cache whose ref is not the ref the
59
+ # bindings configure. With `VAULT_REQUIRED` on the enforcer already stopped the run
60
+ # for this state (`client_vault_ref_drifted_from_configured`, plan 08-01), so the
61
+ # warning this flag selects is reached only with the policy off — the schema default.
62
+ # D-08-39: these lines are a COPY of `resolveRefDrift`'s predicate (src/vault.cts) —
63
+ # the one consumer that cannot call the owner — and are PINNED against it by the
64
+ # agreement test in tests/vault-status-check.test.cjs, which extracts them BY PREFIX
65
+ # and runs them in bash and zsh. Compare the RAW ref: the charset bound above is a
66
+ # RENDERING bound, and applying it to a comparison input claimed drift on `v1.2.1+b`,
67
+ # a legal git ref, because the `+` was stripped before the test.
68
+ VAULT_REF_DRIFT=false
69
+ if [ "$VAULT_TIER" = "client" ] && [ "$VAULT_CONFIGURED_REPO_SET" = "true" ] && [ -n "$VAULT_REF" ] && [ "$VAULT_CONFIGURED_REF_RAW" != "$VAULT_REF" ]; then VAULT_REF_DRIFT=true; fi
70
+ # Compute human-readable cache age when stale
71
+ VAULT_AGE=""
72
+ if [ "$VAULT_STALE" = "true" ] && [ -n "$VAULT_FETCHED_AT" ]; then
73
+ VAULT_AGE=$(VAULT_FETCHED_AT="$VAULT_FETCHED_AT" node -e "const ms=Date.now()-new Date(process.env.VAULT_FETCHED_AT).getTime();const d=Math.floor(ms/86400000);const h=Math.floor((ms%86400000)/3600000);process.stdout.write(d>0?d+'d':h+'h');")
74
+ fi
75
+ # STRICT-03: the enforcement policy. Read once, here, through the same resolver
76
+ # every other read path uses. With it off this file behaves exactly as it always has.
77
+ VAULT_REQUIRED=$(pp_run query config-get vault.required --raw --default false 2>/dev/null || echo false)
78
+ # STRICT-04: practice matching is separately activated. "Not enabled" and "zero
79
+ # matched" are different facts and must never be rendered the same way.
80
+ # Phase 15 D-25: asked of the SAME resolver the vault-practices gates use, at verify:post, where an
81
+ # UNFILTERED vault-practices gate (vault.audit) is declared in both vault modes, so this prints true
82
+ # exactly when the capability's gates render. A config-get read disagreed with the gates on the flat,
83
+ # string and root-true + workstream-empty shapes (15-RESEARCH C-1).
84
+ VAULT_PRACTICES_ENABLED=$(pp_run loop render-hooks verify:post --active-cap vault-practices 2>/dev/null || echo false)
85
+ # Detect --auto / --chain in host $ARGUMENTS. SURFACING ONLY — see below.
86
+ AUTO_MODE=false
87
+ if echo "${ARGUMENTS:-}" | grep -qE -- '--auto|--chain'; then AUTO_MODE=true; fi
88
+ ```
89
+
90
+ Select message based on `VAULT_STATUS`, `VAULT_TIER`, and `VAULT_GATE_POINT`, then handle confirmation.
91
+
92
+ ---
93
+
94
+ ## What this file does under each policy setting (STRICT-03)
95
+
96
+ - **`VAULT_REQUIRED` is `false` or absent (the default).** Everything below behaves exactly as it
97
+ always has: surface the state message and require an explicit `confirmed`. The rendered text is
98
+ byte-identical to the pre-policy build for the same verdict, pinned by committed goldens in
99
+ `tests/vault-read-path.test.cjs`.
100
+
101
+ - **`VAULT_REQUIRED` is `true` and the verdict blocks.** You will not reach this file — the enforcer
102
+ has already stopped the run — unless an accepted attributed override suppressed the block; then
103
+ `status` may be `degraded` and the degraded row below applies (D-25). **There is deliberately
104
+ nothing to do here for those states.** Do not
105
+ add a halt, a warning or a prompt for them out of a sense of incompleteness; a second
106
+ implementation is exactly how two halts drift apart.
107
+
108
+ - **`VAULT_REQUIRED` is `true` and the verdict passes.** Render the success line for the point and
109
+ **skip the confirmation prompt**. A gate already enforced deterministically does not also need a
110
+ human to agree with it, and asking would train operators to press through gates — the habit this
111
+ phase exists to break.
112
+
113
+ Invocation mode never changes any of the above. `--auto` / `--chain` selects how the state is
114
+ surfaced, never whether the run proceeds. Mode-dependent gate behaviour was the precise shape of the
115
+ hole this phase closes, so it is stated here rather than left to be re-derived.
116
+
117
+ ---
118
+
119
+ ## Practice clause (STRICT-04)
120
+
121
+ Appended to the **success** messages below when `VAULT_REQUIRED` is `true`. It is part of the
122
+ enforced gate's success line; with the policy off the messages are unchanged, which is what keeps
123
+ the byte-identical promise.
124
+
125
+ Compute it as follows, then substitute it as `${VAULT_PRACTICE_CLAUSE}`:
126
+
127
+ **Standards mode is decided first** (Phase 15 D-22). Ask the matcher once, before reading
128
+ `VAULT_PRACTICES_ENABLED`:
129
+
130
+ ```bash
131
+ VAULT_PRACTICE_NA_JSON=$(pp_run vault practices match --raw 2>/dev/null || echo '{}')
132
+ VAULT_PRACTICE_NA_LINE=$(printf '%s' "$VAULT_PRACTICE_NA_JSON" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const r=JSON.parse(d);process.stdout.write(r.status==='not_applicable'&&typeof r.message==='string'?r.message:'');}catch{process.stdout.write('');}")
16
133
  ```
17
134
 
18
- **If the fence exits non-zero** (a command failed, the action is empty, or it is `stop`): STOP —
19
- do not plan or build, in any mode. The gate is `onError: halt`: a read that crashed is never a pass.
135
+ **If `VAULT_PRACTICE_NA_LINE` is non-empty** (standards mode: `vault practices match` answers
136
+ not-applicable whenever the vault gate is active, whatever `VAULT_PRACTICES_ENABLED` holds): print
137
+ `${VAULT_PRACTICE_NA_LINE}` verbatim as the card line, print NO number, and skip the rest of this
138
+ clause. The line comes from the engine and names the standards record and the gate that checks it.
139
+ "Practice matching is not enabled" would be a practices-mode sentence on a standards project, which
140
+ is the default standards shape (`vault.practices_enabled` off).
141
+
142
+ Otherwise (practices mode) continue below.
143
+
144
+ **If `VAULT_PRACTICES_ENABLED` != `true`:**
145
+
146
+ > practice matching is not enabled for this project
147
+
148
+ Emit no number on this branch. Zero-matched and matching-not-running are different facts, and
149
+ reporting `0` for the second is the reassuring-but-false signal this phase removes.
150
+
151
+ **Otherwise, at `VAULT_GATE_POINT` == `execute`** — the phase's plans exist, so the count is
152
+ phase-scoped, which is what STRICT-04 asks for:
153
+
154
+ ```bash
155
+ VAULT_PRACTICE_PATHS=$(pp_run query phase-plan-index "${PHASE_NUMBER}" --raw 2>/dev/null | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const s=new Set();for(const p of (JSON.parse(d).plans||[]))for(const f of (p.files_modified||[]))s.add(f);process.stdout.write(Array.from(s).slice(0,200).join('\n'));}catch{process.stdout.write('');}")
156
+ VAULT_PRACTICE_PATH_COUNT=$(printf '%s' "$VAULT_PRACTICE_PATHS" | grep -c . || echo 0)
157
+ ```
158
+
159
+ Pass those paths **positionally** to the match arm — never `--changed`, which would scope to the
160
+ working tree rather than to the phase:
161
+
162
+ `pp_run` is a shell FUNCTION, and `xargs` execs a binary — it can never invoke one. The previous
163
+ form here piped into `xargs -r pp_run …`, whose failure was swallowed by `2>/dev/null` and answered
164
+ by the `|| echo '{}'` fallback, so this clause reported **`0` practices matched on every run**, in
165
+ bash and zsh alike, with nothing to indicate it had not looked. That is the reassuring-but-false
166
+ signal the branch above exists to remove, arriving by a different door: STRICT-04 asks how many
167
+ practices applied to this phase, and a false zero answers it wrongly. The paths therefore go
168
+ straight to the function as positionals, the way `code-review.md` already calls the same arm
169
+ (02-SECURITY T-02-36).
170
+
171
+ ```bash
172
+ VAULT_PRACTICE_ARGS=()
173
+ while IFS= read -r _p; do [ -n "$_p" ] && VAULT_PRACTICE_ARGS+=("$_p"); done <<< "$VAULT_PRACTICE_PATHS"
174
+ if [ ${#VAULT_PRACTICE_ARGS[@]} -gt 0 ]; then
175
+ VAULT_PRACTICE_JSON=$(pp_run vault practices match "${VAULT_PRACTICE_ARGS[@]}" --raw 2>/dev/null || echo '{}')
176
+ else
177
+ VAULT_PRACTICE_JSON=$(pp_run vault practices match --raw 2>/dev/null | node -e "const d=require('fs').readFileSync(0,'utf-8');try{process.stdout.write(JSON.parse(d).status==='not_applicable'?d:'{}');}catch{process.stdout.write('{}');}")
178
+ fi
179
+ VAULT_PRACTICE_MATCHED=$(printf '%s' "$VAULT_PRACTICE_JSON" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const r=JSON.parse(d);process.stdout.write(r.status==='not_applicable'?'NA':String((r.matched||[]).length+(r.global||[]).length));}catch{process.stdout.write('0');}")
180
+ VAULT_PRACTICE_NA_LINE=$(printf '%s' "$VAULT_PRACTICE_JSON" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const r=JSON.parse(d);process.stdout.write(r.status==='not_applicable'&&typeof r.message==='string'?r.message:'');}catch{process.stdout.write('');}")
181
+ ```
182
+
183
+ The path list is capped at **200** entries. When `VAULT_PRACTICE_PATH_COUNT` is `200`, the phase is
184
+ wider than the bound and the clause must say so rather than present a truncated number as complete:
185
+
186
+ > more than 200 files in scope — practice count not computed for this phase
187
+
188
+ Otherwise:
189
+
190
+ > `${VAULT_PRACTICE_MATCHED}` client practices apply to this phase
191
+
192
+ **Otherwise, at `VAULT_GATE_POINT` == `plan`** — no phase scope exists yet, because the plans this
193
+ count would be scoped to are the ones about to be written. Report what is true at this point and
194
+ name when the scoped number arrives; do not fake a phase-scoped count and do not silently omit it:
195
+
196
+ ```bash
197
+ VAULT_PRACTICE_JSON=$(pp_run vault practices match --raw 2>/dev/null || echo '{}')
198
+ VAULT_PRACTICE_TOTAL=$(printf '%s' "$VAULT_PRACTICE_JSON" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const r=JSON.parse(d);process.stdout.write(r.status==='not_applicable'?'NA':String(r.practicesProcessed??0));}catch{process.stdout.write('0');}")
199
+ VAULT_PRACTICE_NA_LINE=$(printf '%s' "$VAULT_PRACTICE_JSON" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const r=JSON.parse(d);process.stdout.write(r.status==='not_applicable'&&typeof r.message==='string'?r.message:'');}catch{process.stdout.write('');}")
200
+ ```
201
+
202
+ > the vault carries `` `${VAULT_PRACTICE_TOTAL}` `` practices at this ref; the phase-scoped matched count is reported at execute:pre once plans exist
203
+
204
+ Each fence above assigns `VAULT_PRACTICE_JSON` itself before reading it — the two points never run
205
+ in one gate, so neither fence reads the other's envelope. With no phase paths the execute fence
206
+ still asks the matcher but keeps only a `not_applicable` answer, so practices mode still counts `0`.
207
+
208
+ **If `VAULT_PRACTICE_MATCHED` (at execute) or `VAULT_PRACTICE_TOTAL` (at plan) is `NA`:** print
209
+ `${VAULT_PRACTICE_NA_LINE}` verbatim as the card line, instead of the count line or the 200-file
210
+ line, and print NO number — the envelope names the mode (`vault.mode = standards`); a count here would be the false
211
+ "0 client practices apply" STRICT-04 forbids.
212
+
213
+ ---
214
+
215
+ ## §8 Message table — plan:pre column (`VAULT_GATE_POINT` == `plan`)
216
+
217
+ **`fetched` + `tier: client`:**
218
+
219
+ > Client vault loaded from `` `${VAULT_REPO}@${VAULT_REF}` `` (`` `${VAULT_FILE_COUNT}` `` files, sha `` `${VAULT_SHA}` ``). Planning will apply client standards.
220
+
221
+ When `VAULT_REQUIRED` is `true`, append ` — ${VAULT_PRACTICE_CLAUSE}.` and skip the confirmation.
222
+ Otherwise surface this message and require an explicit `confirmed` acknowledgment
223
+ (`AskUserQuestion`, or a typed `confirmed` in TEXT_MODE).
224
+
225
+ **`fetched` + `tier: fallback` (intentionally more prominent — design §8):**
226
+
227
+ > ℹ Loaded a **fallback-tier** vault (tier `` `${VAULT_TIER}` ``), read from `` `${VAULT_REPO}@${VAULT_REF}` `` (`` `${VAULT_FILE_COUNT}` `` files) — *${VAULT_FALLBACK_REASON}*. The configured client ref `` `${VAULT_CONFIGURED_REF_TEXT}` `` did not load. Planning will apply this fallback vault's defaults, not the client's current standards. Proceed?
228
+
229
+ Surface this message and require an explicit `confirmed` acknowledgment (`AskUserQuestion`, or a typed `confirmed` in TEXT_MODE). The fallback confirmation is deliberately more prominent than the client tier: proceeding on a fallback vault's defaults instead of the client's own standards is a state the user should consciously acknowledge. With `VAULT_REQUIRED` on, this state has already been stopped by the enforcer and is unreachable here. (D-08-27: the line names the tier that loaded and the ref that failed; it no longer calls the loaded vault "shared" or presents the client's own repo as evidence that a shared vault loaded. On this fork's ladder the fallback source can be the client repo itself at another ref, which is exactly how the retired wording read as a client-vault success.)
230
+
231
+ **`cache` (all tiers failed, using cached copy):**
232
+
233
+ > ⚠ Could not reach client or shared vault — using cached copy from `` `${VAULT_FETCHED_AT}` `` (tier `` `${VAULT_TIER}` ``, may be stale). Proceed?
234
+
235
+ Surface this warning and require an explicit `confirmed` acknowledgment (`AskUserQuestion`, or a typed `confirmed` in TEXT_MODE). With `VAULT_REQUIRED` on, a stale cache has already been stopped by the enforcer; a TTL-fresh cache passes and is rendered without a prompt.
236
+
237
+ **`miss` (no vault reachable, no cached copy):**
238
+
239
+ > ⚠ No vault reachable (client or shared) and no cached copy. Planning will proceed WITHOUT vault context.
240
+
241
+ Surface this warning and require an explicit `confirmed` acknowledgment (`AskUserQuestion`, or a typed `confirmed` in TEXT_MODE). With `VAULT_REQUIRED` on, this state has already been stopped by the enforcer and is unreachable here.
242
+
243
+ **`degraded`** (standards mode — the vault loaded but its `INDEX.md` is missing, unparseable, empty or names no file in the fetched tree, or the engine could not check it at all (`vault_index_unchecked`: the cached manifest records no file list, or the check faulted — this says nothing about `INDEX.md` itself); `check vault.status` sets `status: degraded` and a `reason` of the form `vault_index_*`):
244
+
245
+ Print `${VAULT_MESSAGE}` verbatim — the engine built it; do not restate or paraphrase it here. With `VAULT_REQUIRED` false it is the degraded-index warning, naming `INDEX.md`, what was measured and the repair: surface it and require an explicit `confirmed` acknowledgment (`AskUserQuestion`, or a typed `confirmed` in TEXT_MODE). With `VAULT_REQUIRED` true this row is reachable only after an accepted attributed override (`PP_VAULT_OVERRIDE_REASON`) suppressed the block at the enforcer (D-25), and the message is the override notice naming the suppressed `vault_index_*` reason (or the block text naming `INDEX.md`, if the override reached only the enforcer's own call): print it and the envelope's `override` record when present, and continue without a prompt — the override is the acknowledgment and is already recorded in the envelope the enforcer acted on. The standards-mode practice clause is unchanged from Phase 13. Planning proceeds without a usable index; the `## INDEX` section of `.planning/vault/SUMMARY.md` names the degradation.
246
+
247
+ ---
248
+
249
+ ## §8 Message table — execute:pre column (`VAULT_GATE_POINT` == `execute`)
250
+
251
+ Dispatched from `pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md` after the enforcer.
252
+ Until this phase, nothing dispatched this column at all — it was written and never read.
253
+
254
+ **`fetched` + `tier: client`:**
255
+
256
+ > Client vault present from `` `${VAULT_REPO}@${VAULT_REF}` `` (`` `${VAULT_FILE_COUNT}` `` files, sha `` `${VAULT_SHA}` ``). Build will apply client patterns.
257
+
258
+ When `VAULT_REQUIRED` is `true`, append ` — ${VAULT_PRACTICE_CLAUSE}.` and skip the confirmation.
259
+ Otherwise surface this message and require an explicit `confirmed` acknowledgment
260
+ (`AskUserQuestion`, or a typed `confirmed` in TEXT_MODE).
261
+
262
+ **`fetched` + `tier: fallback` (intentionally more prominent — design §8):**
263
+
264
+ > ℹ Using a **fallback-tier** vault (tier `` `${VAULT_TIER}` ``), read from `` `${VAULT_REPO}@${VAULT_REF}` `` (`` `${VAULT_FILE_COUNT}` `` files, sha `` `${VAULT_SHA}` ``) — *${VAULT_FALLBACK_REASON}*. The configured client ref `` `${VAULT_CONFIGURED_REF_TEXT}` `` did not load. Build applies this fallback vault's defaults, not the client's current patterns. Proceed?
265
+
266
+ Surface this message and require an explicit `confirmed` acknowledgment (`AskUserQuestion`, or a typed `confirmed` in TEXT_MODE). The fallback confirmation is deliberately more prominent than the client tier. With `VAULT_REQUIRED` on, this state has already been stopped by the enforcer and is unreachable here. (D-08-27: the line names the tier that loaded and the ref that failed; it no longer calls the loaded vault "shared" or presents the client's own repo as evidence that a shared vault loaded. On this fork's ladder the fallback source can be the client repo itself at another ref, which is exactly how the retired wording read as a client-vault success.)
267
+
268
+ **`cache` (all tiers failed, using cached copy):**
269
+
270
+ > ⚠ Could not reach client or shared vault — using cached copy from `` `${VAULT_FETCHED_AT}` `` (tier `` `${VAULT_TIER}` ``, `` `${VAULT_FILE_COUNT}` `` files, sha `` `${VAULT_SHA}` ``, may be stale). Proceed?
271
+
272
+ Surface this warning and require an explicit `confirmed` acknowledgment (`AskUserQuestion`, or a typed `confirmed` in TEXT_MODE). With `VAULT_REQUIRED` on, a stale cache has already been stopped by the enforcer; a TTL-fresh cache passes and is rendered without a prompt.
273
+
274
+ **`miss` (no vault reachable, no cached copy):**
275
+
276
+ > ⚠ No vault reachable (client or shared) and no cached copy. Build will proceed WITHOUT vault context.
277
+
278
+ Surface this warning and require an explicit `confirmed` acknowledgment (`AskUserQuestion`, or a typed `confirmed` in TEXT_MODE). With `VAULT_REQUIRED` on, this state has already been stopped by the enforcer and is unreachable here.
279
+
280
+ **`degraded`** (standards mode — the vault loaded but its `INDEX.md` is missing, unparseable, empty or names no file in the fetched tree, or the engine could not check it at all (`vault_index_unchecked`: the cached manifest records no file list, or the check faulted — this says nothing about `INDEX.md` itself); `check vault.status` sets `status: degraded` and a `reason` of the form `vault_index_*`):
281
+
282
+ Print `${VAULT_MESSAGE}` verbatim — the engine built it; do not restate or paraphrase it here. With `VAULT_REQUIRED` false it is the degraded-index warning, naming `INDEX.md`, what was measured and the repair: surface it and require an explicit `confirmed` acknowledgment (`AskUserQuestion`, or a typed `confirmed` in TEXT_MODE). With `VAULT_REQUIRED` true this row is reachable only after an accepted attributed override (`PP_VAULT_OVERRIDE_REASON`) suppressed the block at the enforcer (D-25), and the message is the override notice naming the suppressed `vault_index_*` reason (or the block text naming `INDEX.md`, if the override reached only the enforcer's own call): print it and the envelope's `override` record when present, and continue without a prompt — the override is the acknowledgment and is already recorded in the envelope the enforcer acted on. The standards-mode practice clause is unchanged from Phase 13. Build proceeds without a usable index; the `## INDEX` section of `.planning/vault/SUMMARY.md` names the degradation.
283
+
284
+ ---
285
+
286
+ ## Ref drift (policy off) — applies to the client-tier arm of both columns
287
+
288
+ When `VAULT_REF_DRIFT` is `true`, append the following paragraph to the selected client-tier
289
+ message before surfacing it (D-08-24, rendered here per D-08-29 so the JSON envelope stays
290
+ byte-identical with the policy off):
291
+
292
+ > ⚠ Ref drift: this cache was fetched from `` `${VAULT_REF}` `` but `.platform-bindings.json` configures `` `client.vaultRef: ${VAULT_CONFIGURED_REF}` `` — what loaded is not the configured ref, so the standards applied may not be the client's current ones. With `vault.required` off this is a warning; run `pp-tools vault fetch` to load the configured ref, then re-run `pp-tools check vault.status`.
293
+
294
+ Reachability: with `VAULT_REQUIRED` on, drift has already stopped the run at the enforcer
295
+ (`client_vault_ref_drifted_from_configured`, plan 08-01), so this paragraph is reached **only**
296
+ with the policy off — which is the schema default, and therefore the state most operators are in.
297
+ The facts it names come from `VAULT_CONFIGURED_REF` (a `.platform-bindings.json` read) and
298
+ `VAULT_REF` (an existing envelope field); no new envelope key is consulted, and none is added.
299
+ Appending this paragraph is the only change the client-tier arms take — their success wording is
300
+ unchanged, because plan 08-02's router success line is the gate SURFACE for a clean pass (D-08-20)
301
+ and a second rendering of it here would be a copy that can drift.
302
+
303
+ ---
304
+
305
+ ## Stale suffix (applies to all statuses)
306
+
307
+ When `VAULT_STALE` is `true`, append ` (cache is \`${VAULT_AGE}\` old)` to the selected message before surfacing it to the user.
308
+
309
+ ---
310
+
311
+ ## Surfacing mode (`--auto` / `--chain`)
312
+
313
+ When `AUTO_MODE` is `true`: **do not display an interactive prompt.** Print the selected message
314
+ (including the stale suffix when applicable) as a log line, so a non-interactive pipeline run leaves
315
+ an auditable record of the load state that was in effect.
316
+
317
+ This is a **presentation choice only**. It does not decide anything: whether the run proceeds was
318
+ settled by the enforcer before this file was read, identically in interactive and unattended runs.
319
+
320
+ ---
321
+
322
+ ## Confirmation (interactive mode)
323
+
324
+ When `AUTO_MODE` is `false` and `VAULT_REQUIRED` is `false`:
325
+
326
+ Use `AskUserQuestion` to surface the selected message and await `confirmed`. In TEXT_MODE (`--text` in `$ARGUMENTS`), replace `AskUserQuestion` with a plain-text prompt and accept a typed `confirmed`. With the policy off the gate is advisory: proceeding after `confirmed` (or with a timeout / empty input) is allowed, exactly as before.
20
327
 
21
- **Otherwise relay the printed card to the user verbatim**, then act on `VAULT_ACTION` only:
328
+ When `VAULT_REQUIRED` is `true`, do not prompt at all — see the policy section above.
22
329
 
23
- | `VAULT_ACTION` | What to do |
24
- |---|---|
25
- | `none` | The vault is disabled — continue silently. |
26
- | `proceed` | The client vault is loaded — show the card and continue. |
27
- | `confirm` | Degraded and `vault.required` is false. Interactive: ask for an explicit `confirmed` (`AskUserQuestion`; in TEXT_MODE — `--text` in `$ARGUMENTS` — a plain-text prompt accepting a typed `confirmed`), then continue. With `--auto` / `--chain` in `$ARGUMENTS`: log the card and continue without prompting. |
28
- | `stop` | The fence already exited non-zero: `vault.required` is true and the client vault is not loaded. **STOP** in every mode (including `--auto` / `--chain`); the operator fixes what the card names and re-runs. |
330
+ `TEXT_MODE` follows the same rule as the host workflow (`--text` in `$ARGUMENTS`): replace `AskUserQuestion` with a plain-text numbered prompt.