@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
@@ -0,0 +1,98 @@
1
+ # `execute:pre` capability dispatch — fetch, then decide (GATE-03)
2
+
3
+ Extracted from `execute-phase.md` so the host loop body keeps shrinking, per ADR-857 Phase 6.
4
+ `steps/` files are outside `tests/workflow-size-baseline.json` by design; the host is not.
5
+
6
+ This step is dispatched from `execute-phase.md`'s `execute_pre_dispatch` step, which sits *before*
7
+ `safe_resume_gate` deliberately: that is the first step that can spawn an executor, and the gate must
8
+ run before any code is written, not after.
9
+
10
+ Read the `activeHooks` array from the `EXECUTE_PRE_HOOKS_JSON=` line the host's render fence printed, in-context (do NOT pipe it through a
11
+ shell parser).
12
+
13
+ **Admission first (D-28, D-05).** `execute-phase.md` Reads this file unconditionally, so this is the
14
+ first point on the execute path that can refuse a config the gate cannot read. It precedes the inert
15
+ branch below because that branch is exactly what a broken config reaches: no hook renders at all.
16
+ Without `.platform-bindings.json` it is inert and prints nothing.
17
+
18
+ ```bash
19
+ _PP_SHIM_NAME="pp-tools.cjs"; _pp_root() { ( for _v in $(env | sed -n 's/^\(GIT_[A-Za-z0-9_]*\)=.*/\1/p'); do unset "$_v"; done; _c=$(git rev-parse --git-common-dir 2>/dev/null); if [ "${_c##*/}" = .git ] && cd "$_c/.." 2>/dev/null; then pwd -P; else git rev-parse --show-toplevel 2>/dev/null || pwd; fi ); }; pp_run() { case "$PP_TOOLS" in *.cjs) node "$PP_TOOLS" "$@";; *) "$PP_TOOLS" "$@";; esac; }; _PP_RUNTIME_ROOT="${RUNTIME_DIR:-$(_pp_root)}"; PP_TOOLS="${_PP_RUNTIME_ROOT}/pp-core/bin/${_PP_SHIM_NAME}"; if [ -f "$PP_TOOLS" ]; then :; elif [ -f "${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}"; elif command -v pp-tools >/dev/null 2>&1; then PP_TOOLS="$(command -v pp-tools)"; else echo "ERROR: pp-tools.cjs not found at $PP_TOOLS and pp-tools is not on PATH. Run: npx -y @pareto4/pp-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${PP_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${PP_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
20
+ VAULT_ADMIT_PRESENT=$(pp_run query platform-bindings --pick present 2>/dev/null || echo true)
21
+ VAULT_ADMIT=inert
22
+ if [ "$VAULT_ADMIT_PRESENT" = true ]; then VAULT_ADMIT=$(pp_run vault admission 2>/dev/null || echo unrecognised); fi
23
+ case "$VAULT_ADMIT" in on) echo "VAULT_ADMIT=on" ;; off|inert) : ;; *) pp_run vault admission --explain; echo "PARETO_GATE_BLOCKED:vault.admission"; exit 1 ;; esac
24
+ ```
25
+
26
+ **If `activeHooks` is empty or absent:** skip the rest of this file silently. This branch is what
27
+ keeps a project with no active `execute:pre` capability structurally inert.
28
+
29
+ **Otherwise, in this order — the order is the requirement:**
30
+
31
+ 1. **Fetch first.** For any active entry where `kind == "step"` and `capId == "vault"`, set
32
+ `VAULT_FETCH_HOOKS_JSON=$EXECUTE_PRE_HOOKS_JSON`, then Read and
33
+ execute `pp-core/workflows/execute-phase/steps/vault-fetch.md`. The fetch is attempted *before*
34
+ any block decision so a fresh clone or a CI runner with a reachable vault fetches and passes,
35
+ rather than being blocked for a cache it never had a chance to populate (GATE-03).
36
+
37
+ The binding is not decoration and it is not the same shape as step 2's: `vault-fetch.md` is
38
+ Read from three call sites at two loop points, so it guards on the **parameter**
39
+ `VAULT_FETCH_HOOKS_JSON` and names no caller's own payload variable. Until 02-10 it named
40
+ `plan-phase.md`'s, which this dispatcher never sets and never aliased — and the instruction
41
+ immediately after the guard is *"if no active vault step hook exists: skip"*. A literal
42
+ reading therefore skipped the `execute:pre` fetch outright and let step 2 decide on whatever
43
+ cache happened to exist, which is exactly the fresh-clone false block this ordering exists to
44
+ prevent (02-VERIFICATION, G-2). Binding it here is the whole fix at this call site.
45
+
46
+ 2. **Then gate.** Set `GATE_POINT=execute:pre`, `GATE_HOOKS_JSON=$EXECUTE_PRE_HOOKS_JSON`,
47
+ `GATE_POSITIONAL_ARG="${PHASE_NUMBER}"` and `GATE_WS` to the active workstream name (empty in flat
48
+ mode) — a phase-number positional cannot resolve without it on a partitioned project — then Read and follow
49
+ `pp-core/workflows/execute-phase/steps/check-query-gate.md` — the single generic `check.query`
50
+ gate enforcer (GATE-02, D-VH-7). It owns the halt decision; do not restate its loop here.
51
+
52
+ 3. **Then render.** If the workflow is still running, set `VAULT_GATE_POINT=execute` and Read and
53
+ execute `pp-core/workflows/execute-phase/steps/vault-load-gate.md`. It reports the load state for
54
+ the operator and decides nothing — the decision was step 2's.
55
+
56
+ This line is what makes the load-gate file's `execute:pre` message column live. Until this phase
57
+ that column was written and never dispatched: the execute half of a file that claimed to serve
58
+ both call sites was dead code.
59
+
60
+ 4. **Then anchor the phase.** If the workflow is still running, run
61
+ `pp_run vault manifest start "${PHASE_DIR}" --raw` (the host's initialize step already binds
62
+ `pp_run` and parses `phase_dir` from `INIT`) and print its one-line result to the operator.
63
+
64
+ It records `phases[<phase>].startCommit = HEAD` in `.phase-manifest.json` — the anchor both
65
+ `check vault.audit` and `check vault.review` scope the phase from on a project that commits
66
+ straight to its base branch (UAT-6-F1). It writes only when no anchor exists, so a resume or a
67
+ `--gaps-only` run keeps the original one. On a phase that was already executed without an anchor
68
+ (its directory holds a `*-SUMMARY.md`) it refuses with `phase-already-executed` and writes nothing:
69
+ HEAD there sits after the phase's own commits, and the audit scopes such a phase from its SUMMARY
70
+ trace digests instead. An entry left under this phase number by a different phase directory (a
71
+ number reused after a milestone reset) is stale, and `start` replaces it with a fresh anchor.
72
+ The verb is inert without the practices capability,
73
+ exits 0 on every path, and reports a failure as `ok: false` with a reason that is printed and
74
+ never halts — the audit's SUMMARY trace-digest fallback and its stated fallback ground are the
75
+ degraded path.
76
+
77
+ 4b. **Then record the mode-neutral start anchor (D-31).** If the workflow is still running and the
78
+ `activeHooks` array holds an entry with `capId == "vault"` (the same mechanical check step 1 uses
79
+ for the fetch), run `pp_run vault anchor start "${PHASE_DIR}" --raw` and print its one-line result
80
+ to the operator. Otherwise skip it.
81
+
82
+ It writes `${PHASE_DIR}/.vault-anchors.json` with `start` = HEAD. Every record the phase's
83
+ executors write later (the execute-stage `vault trace scaffold` ledger, the `vault standards
84
+ consulted` anchor line) reads it as its START anchor, beside the END anchor (HEAD at write time)
85
+ the CLI records itself. A commit is then covered only when it falls inside some record's
86
+ start..end range, so an ungoverned commit followed by any record can no longer hide behind that
87
+ record's end. The practices-only manifest start in step 4 is not enough: it exists only when
88
+ `vault.practices_enabled` is on, so a standards-mode phase would have no start at all. Re-running
89
+ at a new HEAD (a resumed run, `--wave N`, `--gaps-only`) records that HEAD as the start of every
90
+ record written from now on (D-98), so nothing committed between two runs is claimed by a later
91
+ record; the earlier start stays in the file's history, where a worktree executor's end-only record
92
+ written before the resume still finds the start that preceded its own end. The phase's own commits
93
+ made between its last record and the resume are then uncovered, and the strict gate names them with
94
+ their repair: `vault anchor backfill <summary> --from <a recorded start> --attest <commit>`, the
95
+ operator's attestation of each commit by its own id; nothing is widened, and every check lists it.
96
+ The verb prints nothing and writes nothing unless `vault admission` answers
97
+ `on`, exits 0 on every path, and reports a refusal as `ok: false` with a reason that is printed
98
+ and never halts. A phase that has no start anchor fails at the strict points instead.
@@ -0,0 +1,94 @@
1
+ # Per-plan worktree decision (#2772)
2
+
3
+ Run this for **each plan in the current wave** before its `Agent()` dispatch. The output `USE_WORKTREES_FOR_PLAN` gates the dispatch branch (worktree mode vs sequential mode) for that plan only — other plans in the same wave can still take the worktree path.
4
+
5
+ `SUBMODULE_PATHS` is computed once in the `initialize` step (parsed from `.gitmodules`).
6
+
7
+ `PLAN_FILES` is the whitespace-separated list of paths the plan declared it will touch, extracted from the `phase-plan-index` JSON loaded in `discover_and_group_plans`:
8
+
9
+ ```bash
10
+ # plan_json is the JSON object for this plan from PLAN_INDEX.plans[]
11
+ # files_modified is an array of strings (repo-relative paths or globs)
12
+ PLAN_FILES=$(jq -r '.files_modified // [] | join(" ")' <<<"$plan_json")
13
+ plan_id=$(jq -r '.id' <<<"$plan_json")
14
+ ```
15
+
16
+ Then run the per-plan gate:
17
+
18
+ ```bash
19
+ USE_WORKTREES_FOR_PLAN="$USE_WORKTREES"
20
+
21
+ if [ -n "$SUBMODULE_PATHS" ] && [ "$USE_WORKTREES_FOR_PLAN" != "false" ]; then
22
+ if [ -z "$PLAN_FILES" ]; then
23
+ # Fallback: planned paths are unknown/unparseable — fall back to the safe
24
+ # behavior (disable worktree isolation for this plan) and log why.
25
+ echo "[worktree] Plan ${plan_id}: files_modified missing/unparseable — disabling worktree isolation as a safety fallback (submodule project)"
26
+ USE_WORKTREES_FOR_PLAN=false
27
+ else
28
+ # Compute intersection with glob-safe normalization. Both sides are
29
+ # normalized (strip leading "./", strip trailing "/") and matched
30
+ # bidirectionally so a globby planned path like "vendor/**/*.c" still
31
+ # matches submodule "vendor/foo", and "./vendor/foo/bar.c" matches
32
+ # submodule "vendor/foo".
33
+ INTERSECT=""
34
+ set -f # disable globbing while iterating literal patterns
35
+ for sm_raw in $SUBMODULE_PATHS; do
36
+ # Normalize submodule path: strip ./ prefix and trailing /
37
+ sm="${sm_raw#./}"
38
+ sm="${sm%/}"
39
+ [ -z "$sm" ] && continue
40
+ for pf_raw in $PLAN_FILES; do
41
+ # Normalize planned path the same way
42
+ pf="${pf_raw#./}"
43
+ pf="${pf%/}"
44
+ [ -z "$pf" ] && continue
45
+ matched=0
46
+ # Direction 1: planned path is the submodule or lies inside it
47
+ case "$pf" in
48
+ "$sm"|"$sm"/*) matched=1 ;;
49
+ esac
50
+ # Direction 2: submodule lies inside the planned path (e.g. plan
51
+ # declares "vendor" or a glob expanding to a directory containing
52
+ # the submodule).
53
+ if [ "$matched" -eq 0 ]; then
54
+ case "$sm" in
55
+ "$pf"|"$pf"/*) matched=1 ;;
56
+ esac
57
+ fi
58
+ # Direction 3: planned path uses a glob — strip glob wildcards
59
+ # and check whether the resulting prefix overlaps the submodule
60
+ # path in either direction.
61
+ if [ "$matched" -eq 0 ]; then
62
+ case "$pf" in
63
+ *'*'*|*'?'*|*'['*)
64
+ # Take the literal prefix before the first glob metachar.
65
+ prefix="${pf%%[*?[]*}"
66
+ prefix="${prefix%/}"
67
+ if [ -n "$prefix" ]; then
68
+ case "$sm" in
69
+ "$prefix"|"$prefix"/*) matched=1 ;;
70
+ esac
71
+ if [ "$matched" -eq 0 ]; then
72
+ case "$prefix" in
73
+ "$sm"|"$sm"/*) matched=1 ;;
74
+ esac
75
+ fi
76
+ fi
77
+ ;;
78
+ esac
79
+ fi
80
+ if [ "$matched" -eq 1 ]; then
81
+ INTERSECT="$INTERSECT $pf_raw"
82
+ fi
83
+ done
84
+ done
85
+ set +f
86
+ if [ -n "$INTERSECT" ]; then
87
+ echo "[worktree] Plan ${plan_id}: planned paths intersect submodule paths (${INTERSECT# }) — disabling worktree isolation for this plan"
88
+ USE_WORKTREES_FOR_PLAN=false
89
+ fi
90
+ fi
91
+ fi
92
+ ```
93
+
94
+ After running this for the plan, the dispatch branches in `execute_waves` step 3 MUST gate on `USE_WORKTREES_FOR_PLAN` for the current plan, not on the project-level `USE_WORKTREES`. Track which plans in this wave actually used worktrees (append `plan_id` to a `WAVE_WORKTREE_PLANS` accumulator when `USE_WORKTREES_FOR_PLAN != false`) — the post-wave cleanup step (5.5) uses this to decide whether worktree-merge cleanup is needed at all.
@@ -1,13 +1,11 @@
1
1
  # Step: post_merge_gate
2
2
 
3
- Post-merge build & test gate. Runs exactly once per wave, after the wave's merge
4
- step — the wave's one full-suite run (`suite_runs: 1` in the dispatch plan); no
5
- executor runs the full suite. Catches cross-plan integration failures that
6
- per-plan checks cannot detect.
7
-
8
- **Step A — Build gate:** run it only when the dispatch plan's wave `build` is true.
9
- When it is false (no plan in the wave touches source), print
10
- `⚠ Post-merge build gate skipped — no plan in this wave touches source` and go to Step B.
3
+ Post-merge build & test gate. Runs after all worktrees in a wave are merged
4
+ (parallel mode), or after the last plan completes (serial mode). Catches
5
+ cross-plan integration failures that individual worktree self-checks cannot
6
+ detect.
7
+
8
+ **Step A — Build gate:**
11
9
 
12
10
  ```bash
13
11
  _PP_SHIM_NAME="pp-tools.cjs"; _pp_root() { ( for _v in $(env | sed -n 's/^\(GIT_[A-Za-z0-9_]*\)=.*/\1/p'); do unset "$_v"; done; _c=$(git rev-parse --git-common-dir 2>/dev/null); if [ "${_c##*/}" = .git ] && cd "$_c/.." 2>/dev/null; then pwd -P; else git rev-parse --show-toplevel 2>/dev/null || pwd; fi ); }; pp_run() { case "$PP_TOOLS" in *.cjs) node "$PP_TOOLS" "$@";; *) "$PP_TOOLS" "$@";; esac; }; _PP_RUNTIME_ROOT="${RUNTIME_DIR:-$(_pp_root)}"; PP_TOOLS="${_PP_RUNTIME_ROOT}/pp-core/bin/${_PP_SHIM_NAME}"; if [ -f "$PP_TOOLS" ]; then :; elif [ -f "${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}"; elif command -v pp-tools >/dev/null 2>&1; then PP_TOOLS="$(command -v pp-tools)"; else echo "ERROR: pp-tools.cjs not found at $PP_TOOLS and pp-tools is not on PATH. Run: npx -y @pareto4/pp-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${PP_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${PP_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
@@ -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).
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
  ```