@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
@@ -89,12 +89,22 @@ Verify the work is ready to ship:
89
89
 
90
90
  Resolve active `ship:pre` gate hooks from the capability registry — the registry evaluates each hook's `when` condition, so do **not** read `workflow.security_enforcement` directly:
91
91
 
92
+ Admission first (D-28, D-05): a config the gate cannot read renders no ship:pre vault gate at all, so this runs before the render; without bindings it is inert.
93
+
94
+ ```bash
95
+ VAULT_ADMIT_PRESENT=$(pp_run query platform-bindings --pick present 2>/dev/null || echo true)
96
+ VAULT_ADMIT=inert
97
+ if [ "$VAULT_ADMIT_PRESENT" = true ]; then VAULT_ADMIT=$(pp_run vault admission 2>/dev/null || echo unrecognised); fi
98
+ 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
99
+ ```
100
+
92
101
  ```bash
93
- SHIP_PRE_HOOKS_JSON=$(pp_run loop render-hooks ship:pre --raw --phase "${phase_number}") || exit 1
102
+ SHIP_PRE_HOOKS_JSON=$(pp_run loop render-hooks ship:pre --raw)
103
+ printf '%s' "$SHIP_PRE_HOOKS_JSON" | node -e "let h=[];try{h=JSON.parse(require('fs').readFileSync(0,'utf-8')).activeHooks||[]}catch{}console.log('SHIP_PRE_HOOKS_JSON='+JSON.stringify({activeHooks:h}))"
94
104
  SECURITY_FILE=$(ls "${PHASE_DIR}"/*-SECURITY.md 2>/dev/null | head -1)
95
105
  ```
96
106
 
97
- Read the `activeHooks` array from `SHIP_PRE_HOOKS_JSON` in-context (do NOT pipe it through a shell parser).
107
+ Read the `activeHooks` array from the `SHIP_PRE_HOOKS_JSON=` line the fence printed, in-context (do NOT pipe it through a shell parser).
98
108
 
99
109
  If an active entry exists with `kind == "gate"`, `capId == "security"`, and `blocking == true`, enforce its predicate (`SECURITY.md` frontmatter `threats_open == 0`) before shipping:
100
110
 
@@ -113,27 +123,50 @@ Verify the work is ready to ship:
113
123
 
114
124
  **Generic `check.query` gate enforcement (capability-driven).**
115
125
 
116
- For each active entry in `activeHooks` where `kind == "gate"` and a `check.query` is present (covers any capability-registered query gate, e.g. the `pareto-stack` platform-drift gates — skip entries already handled by the `security` predicate branch above):
117
-
118
- ```bash
119
- GATE_RESULT=$(pp_run check ${hook.check.query} --raw)
120
- CHECK_EXIT=$?
121
- ```
122
-
123
- - If `CHECK_EXIT` is non-zero or output is unparseable: if `hook.onError == "skip"` (the default for all capability-registered query gates), log a warning and continue to the next hook.
124
- - Read `status` from `GATE_RESULT`:
125
- - If `hook.blocking == true` and `status == "block"` → halt the ship with `PARETO_DRIFT_SHIP_GATE_BLOCKED`:
126
- ```
127
- ⚠ Ship gate blocked: {hook.check.query} reports a drift condition that exceeds the configured threshold.
128
- Pinned: {GATE_RESULT.pinned} — Latest: {GATE_RESULT.latest}
129
- Update .platform-bindings.json or adjust the block threshold before shipping.
130
- ```
131
- - If `hook.blocking == false`, or `status` is `warn`, `pass`, or `inert` → emit an advisory line for non-trivial statuses (`warn` only) and continue:
132
- ```
133
- ℹ Ship gate advisory ({hook.check.query}): {status}{GATE_RESULT.severity ? ` — ${GATE_RESULT.severity} drift detected` : ''} (non-blocking or below blocking threshold). Continuing.
134
- ```
135
- - If `status` is `pass` or `inert`: continue silently.
136
- - Independently of the status arms above, if `GATE_RESULT.message` is a non-empty string, print it verbatim — it is already operator-ready, possibly multi-line text — then continue: a gate that has something to tell the operator says it on every run, not only when its status is a warning.
126
+ > **Two behaviour changes landed here (GATE-02).** (1) Ship's gate enforcement now reads the
127
+ > boolean `block` field instead of `status == "block"`; every declared `check.query` producer in
128
+ > the repo was audited for that field and the audit table lives in the enforcer sub-file.
129
+ > (2) `onError: halt` now actually works at `ship:pre` — it was declared-but-unimplemented here
130
+ > before.
131
+
132
+ The `security` predicate branch above is handled *before* this generic loop, so the enforcer
133
+ never sees that entry.
134
+
135
+ For every remaining active entry in `activeHooks` where `kind == "gate"` and a `check.query` is
136
+ present (covers any capability-registered query gate, e.g. the `pareto-stack` platform-drift
137
+ gates), set `GATE_POINT=ship:pre`, `GATE_HOOKS_JSON=$SHIP_PRE_HOOKS_JSON`,
138
+ `GATE_POSITIONAL_ARG="${PHASE_DIR}"` and `GATE_UNPAIRED=strict`, then Read and follow
139
+ `pp-core/workflows/execute-phase/steps/check-query-gate.md` — the single generic `check.query`
140
+ gate enforcer (GATE-02, D-VH-7). It owns the two-step contract (did the command succeed? → then
141
+ read the boolean `block`), the halt, and the unconditional `message` passthrough. Do not restate
142
+ its loop here.
143
+
144
+ **The positional is `${PHASE_DIR}`, and emptying it again is an outage.** It was empty until
145
+ 04-09. A gate declared `blocking: true` and handed no positional answers its `no-argument`
146
+ verdict with `block: true`, so an empty positional halts EVERY ship on EVERY vault-enabled
147
+ project, unconditionally and for every phase — the denial of service the gate exists to prevent,
148
+ shipped by the gate itself. `PHASE_DIR` is already parsed from `INIT` above and already used
149
+ twice before this loop, which is why the correction costs one token.
150
+
151
+ Set **no** `GATE_WS` here. A phase **directory** already carries the workstream partition — the
152
+ enforcer's own parameter table says so — so adding a workstream flag would not make this call
153
+ safer; it would add a fifth opportunity for the omitted-workstream defect this project has
154
+ already shipped four times. `GATE_UNPAIRED=strict` because ship is one of the two points at
155
+ which every plan must have an executed counterpart; mid-wave points stay tolerant.
156
+
157
+ **After the gate loop, certify the phase's end anchors.** The enforcer halts on a blocking
158
+ verdict, so this is reached only when it let the ship go on; nothing here assumes the phase is
159
+ clean from that. Run `pp_run vault anchor certify "${PHASE_DIR}" --raw` with the same
160
+ `${PHASE_DIR}` positional the gate loop used, print its one-line result, and continue regardless.
161
+ A ship run is a full-phase run by construction (see above), so there is no partial-run condition
162
+ here. The verb re-runs `check vault.trace "${PHASE_DIR}" --unpaired strict` itself and records
163
+ each SUMMARY's end anchor in the directory's `.vault-certificates.json` only when that check
164
+ answers clean with no override, so this step can never certify what the gate did not pass
165
+ (D-107, D-108). A skipped or failed step certifies nothing, which only widens the next phase's
166
+ SEAL-01 reach and never hides a commit (D-110). The verb prints nothing unless `vault admission`
167
+ answers `on`, so print nothing yourself then either. Every result (a certificate, `kept`, or a
168
+ refusal such as `not-clean` or `overridden`) is printed, never retried, and never halts the ship.
169
+ No bash fence.
137
170
  </step>
138
171
 
139
172
  <step name="push_branch">
@@ -448,7 +481,7 @@ pp_run query commit "docs(${padded_phase}): ship phase ${PHASE_NUMBER} — PR #$
448
481
  > Capability-driven dispatch. Resolves active `ship:post` hooks via the capability registry; each hook's `when` is evaluated by the registry — no inline `config-get`. All `ship:post` hooks are post-ship and additive (`onError: skip`); a failure here never affects the already-created PR.
449
482
 
450
483
  ```bash
451
- SHIP_POST_HOOKS_JSON=$(pp_run loop render-hooks ship:post --raw) || exit 1
484
+ SHIP_POST_HOOKS_JSON=$(pp_run loop render-hooks ship:post --raw)
452
485
  ```
453
486
 
454
487
  Read the `activeHooks` array directly from `SHIP_POST_HOOKS_JSON` in-context (do NOT pipe it through a shell parser).
@@ -192,8 +192,8 @@ Exit.
192
192
 
193
193
  ```bash
194
194
  CHANGELOG_TMP="/tmp/pp-changelog-$$.md"
195
- curl -fsSL "https://gitlab.com/pareto4/pp-core/-/raw/main/CHANGELOG.md" -o "$CHANGELOG_TMP" 2>/dev/null \
196
- || wget -qO "$CHANGELOG_TMP" "https://gitlab.com/pareto4/pp-core/-/raw/main/CHANGELOG.md" 2>/dev/null
195
+ curl -fsSL "https://raw.githubusercontent.com/pareto000/pp-core/main/CHANGELOG.md" -o "$CHANGELOG_TMP" 2>/dev/null \
196
+ || wget -qO "$CHANGELOG_TMP" "https://raw.githubusercontent.com/pareto000/pp-core/main/CHANGELOG.md" 2>/dev/null
197
197
 
198
198
  GSD_CHANGESET_CLI="$GSD_DIR/scripts/changeset/cli.cjs"
199
199
  if [ ! -f "$GSD_CHANGESET_CLI" ]; then
@@ -468,7 +468,7 @@ Format completion message (changelog was already shown in confirmation step):
468
468
 
469
469
  ⚠️ Restart your runtime to pick up the new commands.
470
470
 
471
- [View full changelog](https://gitlab.com/pareto4/pp-core/-/blob/main/CHANGELOG.md)
471
+ [View full changelog](https://github.com/pareto000/pp-core/blob/main/CHANGELOG.md)
472
472
  ```
473
473
  </step>
474
474
 
@@ -30,7 +30,7 @@ Parse: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`.
30
30
 
31
31
  ```bash
32
32
  AUDITOR_MODEL=$(pp_run query resolve-model pp-nyquist-auditor --raw)
33
- VERIFY_POST_HOOKS_JSON=$(pp_run loop render-hooks verify:post --raw) || exit 1
33
+ VERIFY_POST_HOOKS_JSON=$(pp_run loop render-hooks verify:post --raw)
34
34
  ```
35
35
 
36
36
  Resolve active step hooks from `VERIFY_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "validate-phase"`.
@@ -39,6 +39,36 @@ If no active validate-phase step hook exists: exit with "Nyquist validation is d
39
39
 
40
40
  Display banner: `pp-core > VALIDATE PHASE {N}: {name}`
41
41
 
42
+ ## 0.5 Vault admission and load gate
43
+
44
+ Vault admission and load gate (D-11, D-12, D-28): runs before the auditor writes or commits a test. A config the gate cannot
45
+ read halts here with the bindings present (D-05); without bindings, or with `vault.enabled` false,
46
+ nothing below prints or writes anything (D-13).
47
+
48
+ ```bash
49
+ VAULT_ADMIT_PRESENT=$(pp_run query platform-bindings --pick present 2>/dev/null || echo true)
50
+ VAULT_ADMIT=inert
51
+ if [ "$VAULT_ADMIT_PRESENT" = true ]; then VAULT_ADMIT=$(pp_run vault admission 2>/dev/null || echo unrecognised); fi
52
+ 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
53
+ ```
54
+
55
+ **If the fence printed no `VAULT_ADMIT=on` line** (`off` and `inert` print nothing): skip the rest of this block silently. Otherwise run the load
56
+ gate on the `execute:pre` payload, filtered in the fence to the `vault` and `vault-practices`
57
+ capabilities (D-30):
58
+
59
+ ```bash
60
+ VP_EXECUTE_PRE_HOOKS_JSON=$(pp_run loop render-hooks execute:pre --raw | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const j=JSON.parse(d);j.activeHooks=(j.activeHooks||[]).filter((x)=>x&&(x.capId==='vault'||x.capId==='vault-practices'));process.stdout.write(JSON.stringify(j));}catch{process.stdout.write('');}")
61
+ printf '%s' "$VP_EXECUTE_PRE_HOOKS_JSON" | node -e "let h=[];try{h=JSON.parse(require('fs').readFileSync(0,'utf-8')).activeHooks||[]}catch{}console.log('VP_EXECUTE_PRE_HOOKS_JSON='+JSON.stringify({activeHooks:h}))"
62
+ ```
63
+
64
+ Set `VAULT_FETCH_HOOKS_JSON=$VP_EXECUTE_PRE_HOOKS_JSON`, then Read and execute
65
+ `pp-core/workflows/execute-phase/steps/vault-fetch.md` (hook mode). Then, in the
66
+ `VP_EXECUTE_PRE_HOOKS_JSON=` line that fence printed, find active entries where `kind == "gate"` and `check.query ==
67
+ "vault.status"`; if none, skip silently. Otherwise set `GATE_POINT=execute:pre` and
68
+ `GATE_HOOKS_JSON=$VP_EXECUTE_PRE_HOOKS_JSON`, leave `GATE_POSITIONAL_ARG`, `GATE_WS` and
69
+ `GATE_STAGE` empty, then Read and follow `pp-core/workflows/execute-phase/steps/check-query-gate.md`;
70
+ it owns the halt.
71
+
42
72
  ## 1. Detect Input State
43
73
 
44
74
  ```bash
@@ -97,6 +127,18 @@ Call AskUserQuestion with gap table and options:
97
127
  2. "Skip — mark manual-only" → add to Manual-Only, Step 6
98
128
  3. "Cancel" → exit
99
129
 
130
+ ### Door START anchor (before the auditor spawn)
131
+
132
+ Then record the door's START anchor, before the `pp-nyquist-auditor` spawn (D-31):
133
+
134
+ ```bash
135
+ pp_run vault door-record --begin --door validate-phase --raw
136
+ ```
137
+
138
+ It records HEAD in `.planning/.vault-door-starts.json`, where the record step reads it back: a shell
139
+ variable would not survive into that later step, and the agent never types a sha. It prints nothing
140
+ and writes nothing unless `vault admission` answers `on`.
141
+
100
142
  ## 5. Spawn pp-nyquist-auditor
101
143
 
102
144
  Print: `◆ Spawning nyquist auditor... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)`
@@ -151,6 +193,23 @@ git commit -m "test(phase-${PHASE}): add Nyquist validation tests"
151
193
  pp_run query commit "docs(phase-${PHASE}): add/update validation strategy"
152
194
  ```
153
195
 
196
+ Vault door record (D-12), only when Step 5 ran: after the door's last source commit, set `DOOR_DIR="${PHASE_DIR}"`,
197
+ `DOOR_PADDED="${PADDED_PHASE}"` and `DOOR_PHASE="${PHASE}"`, then run:
198
+
199
+ ```bash
200
+ DOOR_RUN=$(date -u +%Y%m%dT%H%M%SZ)
201
+ pp_run vault door-record --door validate-phase --artifact "${DOOR_DIR}/${DOOR_PADDED}-door-${DOOR_RUN}-ADD-TESTS.md" --phase "$DOOR_PHASE" --raw
202
+ ```
203
+
204
+ The verb writes a `## Vault Door Record` (mode, vault ref and sha, governing phase, the start `--begin`
205
+ recorded and the end commit; the agent never types a sha) into a fresh per-run artifact, because it refuses a second record
206
+ in one file (D-57) and a rewritten report would drop the earlier run's coverage (D-79). It prints
207
+ nothing and writes nothing unless `vault admission` answers `on`. When it wrote the file and
208
+ `commit_docs` is true, commit the `artifact` path it printed: `pp_run query commit "docs(phase-${PHASE}): vault door record for validate-phase" --files "<that artifact>"`.
209
+
210
+ (A per-run `-VALIDATION.md` name would be read as the phase's validation strategy by the `*-VALIDATION.md`
211
+ globs, so validate-phase's record uses the `-ADD-TESTS.md` suffix: what it records is the tests it added.)
212
+
154
213
  ## 8. Results + Routing
155
214
 
156
215
  **Compliant:**
@@ -0,0 +1,325 @@
1
+ <purpose>
2
+ Retroactive client-vault practice review of a completed phase. Standalone command that works on
3
+ any completed phase — traced or untraced. Produces `{padded}-VAULT-REVIEW.md` with one verdict per
4
+ practice and counts by verdict. This workflow reports; it gates nothing.
5
+ </purpose>
6
+
7
+ <required_reading>
8
+ @~/.claude/pp-core/references/verifier-practice-verdicts.md
9
+ </required_reading>
10
+
11
+ <available_agent_types>
12
+ Valid pp-core subagent types (use exact names — do not fall back to 'general-purpose'):
13
+ - pp-vault-reviewer — Judges the practice remainder the CLI derivation could not settle
14
+ </available_agent_types>
15
+
16
+ <process>
17
+
18
+ ## 0. Initialize
19
+
20
+ ```bash
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
22
+ INIT=$(pp_run query init.phase-op "${PHASE_ARG}")
23
+ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
24
+ AGENT_SKILLS_VAULT_REVIEWER=$(pp_run query agent-skills pp-vault-reviewer)
25
+ ```
26
+
27
+ Parse: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `commit_docs`.
28
+
29
+ ### 0a. The vault-off short-circuit — the first act of this workflow
30
+
31
+ Run the derive call now, before any input state is detected, and branch on what the arm prints:
32
+
33
+ ```bash
34
+ REVIEW_PAYLOAD=$(pp_run check vault-review "${PHASE_DIR}" --raw)
35
+ REVIEW_STATUS=$(printf '%s' "${REVIEW_PAYLOAD}" | node -e "let s='';process.stdin.on('data',d=>{s+=d}).on('end',()=>{try{console.log(JSON.parse(s).status||'')}catch{console.log('unparseable')}})")
36
+ ```
37
+
38
+ **If `REVIEW_STATUS` is `inert`:** print the payload's `statement` field verbatim and STOP here.
39
+ Nothing below this line runs: no agent is spawned, no template is read, and no `VAULT-REVIEW.md` is
40
+ written. A project with the vault practices capability off is left byte-identical by this command
41
+ (the CLAUDE.md inertness rule).
42
+
43
+ **If `REVIEW_STATUS` is `not_applicable` or `bad-config`:** print the payload's `statement` field
44
+ verbatim and STOP: nothing below runs, no agent is spawned. `not_applicable` is standards mode
45
+ (`vault.mode = standards`): the practices review is parked, and the consulted documents are recorded
46
+ in each plan's `<vault_standards>` block and its SUMMARY's `Standards Consulted` section.
47
+ `bad-config` is a `vault.mode` the engine cannot read.
48
+
49
+ Why the branch is on the arm's output rather than on the config: **this workflow never reads
50
+ `vault.enabled` or `practices_enabled` itself.** Policy has one home — the CLI arm resolves both keys
51
+ through the shared reader and answers before it touches the vault, the trace or a git subprocess. The
52
+ arm's inert sentence is proven byte-identical between the two spellings of "off" (the key absent, and
53
+ the key present and explicitly `false`) by plan 06-04's integration suite, which compares the two
54
+ outputs byte for byte. Branching here on the arm's output is what makes that proof this workflow's
55
+ proof too; a second config read would be a second policy home that could disagree with the first.
56
+
57
+ **Capability-OFF is not the same input as an absent vault.** They are never collapsed:
58
+
59
+ | Input | What happens |
60
+ |---|---|
61
+ | The capability is off (`status: inert`) | Stop here, inert. The feature does not apply to this project. |
62
+ | Standards mode or an unrecognised mode (`status: not_applicable` / `bad-config`) | Stop here and print the statement. A parked surface must not render as a clean review. |
63
+ | The capability is on but the vault is missing, unreadable or holds no practices | Continue. The review runs and renders **loudly as gaps-only** through the template's not-derived sections. |
64
+
65
+ The second row is a phase that was reviewed and whose review found gaps; the first is a project the
66
+ feature does not apply to. A review that reported the second as the first would be reporting a gap as
67
+ an absence of gaps.
68
+
69
+ **If `REVIEW_STATUS` is `usage`, `no-argument` or `unresolved`:** print the payload's `statement`
70
+ verbatim and stop — the arm reviewed nothing and said which argument it could not use. Re-run with
71
+ the phase directory.
72
+
73
+ Then display the banner:
74
+
75
+ ```
76
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
77
+ pp-core ► VAULT PRACTICE REVIEW — PHASE {N}: {name}
78
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
79
+ ```
80
+
81
+ ## 1. Detect Input State
82
+
83
+ ```bash
84
+ SUMMARY_FILES=$(ls "${PHASE_DIR}"/*-SUMMARY.md 2>/dev/null)
85
+ PLAN_FILES=$(ls "${PHASE_DIR}"/*-PLAN.md 2>/dev/null)
86
+ VAULT_REVIEW_FILE=$(ls "${PHASE_DIR}"/*-VAULT-REVIEW.md 2>/dev/null | head -1)
87
+ ```
88
+
89
+ - **State A** (`VAULT_REVIEW_FILE` non-empty): a review already exists — re-review, and append a
90
+ dated audit-trail block to the artifact rather than discarding the previous one.
91
+ - **State B** (`SUMMARY_FILES` non-empty): the phase is completed — run the review.
92
+ - **State C** (`SUMMARY_FILES` empty): exit cleanly — "Phase {N} not executed. Run
93
+ `/pp:execute-phase {N}` first." This is the honest refusal, and it is the only one. D-06-14 makes
94
+ **any completed phase** eligible, traced or not; State C refuses only a phase that was never built,
95
+ because there is no work to review.
96
+
97
+
98
+ **Text mode (`workflow.text_mode: true` in config or `--text` flag):** Set `TEXT_MODE=true` if `--text` is present in `$ARGUMENTS` OR `text_mode` from init JSON is `true`. When TEXT_MODE is active, replace every `AskUserQuestion` call with a plain-text numbered list and ask the user to type their choice number. This is required for non-Claude runtimes (OpenAI Codex, Gemini CLI, etc.) where `AskUserQuestion` is not available.
99
+ **In State A**, use AskUserQuestion:
100
+ - header: "Existing Vault Review"
101
+ - question: "VAULT-REVIEW.md already exists for Phase {N}."
102
+ - options:
103
+ - "Re-review — run a fresh review and append to the audit trail"
104
+ - "View — display the current review and exit"
105
+
106
+ If "View": display the file, exit. If "Re-review": continue.
107
+
108
+ ## 2. Provenance — `trace_present_and_loaded`
109
+
110
+ Read one flag from the payload, and carry it to the spawn:
111
+
112
+ `trace_present_and_loaded` is the payload's **`trace.loaded`**. The CLI sets it true only when a
113
+ `vault:` trace block exists in this phase's artifacts **and** every parsed block records
114
+ `predicate.vault.state: loaded`. A degraded block sets it **false**, and so does an absent one. Two
115
+ fields beside it say which case this is and why: `trace.state` is `loaded`, `degraded`, or `null`
116
+ when no block was found at all, and `trace.reason` names the artifact that failed and what it
117
+ recorded. Do not recompute the flag here — it is derived once, in the CLI, from data this workflow
118
+ never reads.
119
+
120
+ A degraded block is a valid record of a **failure**: it says no trusted vault load existed when the
121
+ work was planned. It therefore proves nothing about which practices governed that work, which is why
122
+ it is not allowed to stand in for a loaded one. When the flag is false because a trace was found and
123
+ failed, carry `trace.reason` into §5's RECONSTRUCT constraint so the reviewer records why the trace
124
+ could not stand.
125
+
126
+ ## 3. The three-way short-circuit
127
+
128
+ `pp-core/workflows/secure-phase.md` states the rule this step copies, in its own words:
129
+
130
+ > "Empty-by-no-planning must not rubber-stamp a clean SECURITY.md."
131
+
132
+ Restated for this tier: **an empty or absent trace must never produce a clean review. Reconstruct
133
+ first, then judge.**
134
+
135
+ | Provenance | What runs |
136
+ |---|---|
137
+ | `trace_present_and_loaded: true`, remainder empty | The derived rows are the whole review. Render them; the agent judges nothing because nothing was left unsettled. |
138
+ | `trace_present_and_loaded: true`, remainder non-empty | Spawn in **verify mode** — judge the supplied remainder only. |
139
+ | `trace_present_and_loaded: false` | Spawn in **RECONSTRUCT mode** — the applicable practice set was rebuilt by the CLI from this phase's real diff crossed with the vault's own practice list, and that reconstructed set is what gets judged. |
140
+
141
+ **Reconstruct is the primary path, not a degraded one.** A project with no `vault` config key has no
142
+ phase that has or can have a trace, so every review of its own history takes this arm. The payload
143
+ says so in its `statements[]`, and the artifact repeats it.
144
+
145
+ ## 4. The derived payload
146
+
147
+ `REVIEW_PAYLOAD`, captured in step 0a, is the review's whole factual input and is consumed before the
148
+ spawn. It carries, already computed: `range`, `rangeSource`, `filesConsidered`, the input set, both
149
+ derived-`N-A` populations with their own labels and reasons, the partitioned remainder, the derived
150
+ `derivedCounts` / `derivedRateLine` (what those two describe, and what they are never written as,
151
+ is stated once in the contract block carried into the spawn prompt below),
152
+ `practiceEntries` (each practice's prose already fenced and defanged by the
153
+ shipped renderer), `statements[]`, `notes[]` and `refusesCleanRender`.
154
+
155
+ **The clean-render refusal is the CLI's, and this workflow honors it rather than reimplementing it.**
156
+ When `refusesCleanRender` is true (the range considered zero files), or when the payload records that
157
+ the vault was absent or unreadable, the review still renders — as a **loud gaps-only artifact**
158
+ through the template's not-derived sections — and never as a clean review. The rule lives in code, in
159
+ the shipped predicate; nothing here restates it as a second copy that could drift.
160
+
161
+ ## 5. Spawn pp-vault-reviewer
162
+
163
+ ```bash
164
+ REVIEWER_MODEL=$(pp_run query resolve-model pp-vault-reviewer --raw)
165
+ ```
166
+
167
+ ```
168
+ ◆ Spawning vault reviewer... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)
169
+ ```
170
+
171
+ Build the prompt. The CLI payload is carried **verbatim**, and every practice byte inside it is
172
+ already fenced by the shipped renderer — this workflow opens no vault file of its own:
173
+
174
+ ```markdown
175
+ Read ~/.claude/agents/pp-vault-reviewer.md for instructions.
176
+
177
+ <objective>
178
+ Retroactive client-vault practice review of Phase {phase_number}: {phase_name}
179
+ </objective>
180
+
181
+ <constraints>
182
+ {If trace_present_and_loaded: "This phase's trace is present and loaded. Judge the supplied
183
+ remainder only — the derived sets are complete and are reproduced, not recomputed."}
184
+ {If not trace_present_and_loaded: "RECONSTRUCT mode. This phase has no loaded trace, so the
185
+ applicable practice set below was rebuilt from the phase's changed files crossed with the vault's
186
+ practice list. Judge that reconstructed set. A missing trace is itself a finding: record it loudly."}
187
+ Every derived fact below was computed by `pp-tools check vault-review` and is not recomputable by
188
+ you (D-06-09). Copy the range statement and every derived row's supplied reason and arm label
189
+ verbatim; never recount them.
190
+ <!-- gen:practice-arithmetic-contract:start -->
191
+ **TWO COUNT POPULATIONS, and conflating them writes a false section.**
192
+
193
+ `derivedCounts` and `derivedRateLine` on the `check vault-review` payload describe the **derived rows
194
+ alone** — Arm A plus Arm B — which on the reconstruct path is usually none. They are never written as
195
+ the register's own two lines.
196
+
197
+ The register's rate and counts lines describe **the rows you write**, derived and judged together.
198
+ `validateReviewArtifact` recomputes them from exactly those rows and rejects a narrated figure with
199
+ `rate-line-arithmetic` or `counts-line-arithmetic`. Copying a supplied `0 of 0 practices judged …`
200
+ into the register states that nothing was judged over a register that judged something.
201
+
202
+ `judged = total − na`, and INCONCLUSIVE counts *inside* `judged` and is reported separately.
203
+
204
+ **Where a supplied figure disagrees with your rows, record the disagreement and overwrite neither
205
+ side.** Write it as PROSE that names it as the supplied figure — never as a bare line in the
206
+ register's own shape. A second bare shaped line makes the section contradict itself: two rate lines,
207
+ or two counts lines, stating different figures, with nothing in the register saying which of them the
208
+ rows you wrote support.
209
+ <!-- gen:practice-arithmetic-contract:end -->
210
+ ON THIS TIER the disagreement goes on the `| Disagreements between supplied counts and the rows |`
211
+ row of `## Not Derived`.
212
+ Write exactly one file: {phase_dir}/{padded_phase}-VAULT-REVIEW.md
213
+ </constraints>
214
+
215
+ <review_payload>
216
+ {REVIEW_PAYLOAD verbatim}
217
+ </review_payload>
218
+
219
+ <files_to_read>
220
+ - {summary_paths} (Execution summaries)
221
+ - {plan_paths} (Execution plans)
222
+ - {context_path} (User decisions, if it exists)
223
+ </files_to_read>
224
+
225
+ ${AGENT_SKILLS_VAULT_REVIEWER}
226
+
227
+ <config>
228
+ phase_dir: {phase_dir}
229
+ padded_phase: {padded_phase}
230
+ </config>
231
+ ```
232
+
233
+ Omit null file paths.
234
+
235
+ ```
236
+ Agent(
237
+ prompt=vault_review_prompt,
238
+ subagent_type="pp-vault-reviewer",
239
+ model="{REVIEWER_MODEL}",
240
+ description="Vault practice review Phase {N}"
241
+ )
242
+ ```
243
+
244
+ > **ORCHESTRATOR RULE — CODEX RUNTIME**: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.
245
+
246
+ ## 6. Handle Return
247
+
248
+ The agent returns `## REVIEWED`, `## DEGRADED` or `## ESCALATE`. It reads
249
+ `~/.claude/pp-core/templates/VAULT-REVIEW.md`, fills it, and writes
250
+ `${PHASE_DIR}/${PADDED_PHASE}-VAULT-REVIEW.md` (D-06-16).
251
+
252
+ On `REVIEWED` or `DEGRADED`, run the deterministic shape check over what was written:
253
+
254
+ ```bash
255
+ node -e "const m=require((process.env.PP_LIB || './pp-core/bin/lib') + '/vault-review.cjs');const t=require('fs').readFileSync(process.argv[1],'utf8');const r=m.validateReviewArtifact(t);console.log(JSON.stringify(r.problems,null,2));console.log('rate:',r.expectedRateLine);console.log('counts:',r.expectedCountsLine)" "${PHASE_DIR}/${PADDED_PHASE}-VAULT-REVIEW.md"
256
+ ```
257
+
258
+ `PP_LIB` defaults to the repo-root layout above. From an installed runtime, set it first —
259
+ `export PP_LIB=~/.claude/pp-core/bin/lib` — because the bare relative path resolves only when the
260
+ cwd is the repo root, and it silently fails everywhere else.
261
+
262
+ Surface any reported problems to the operator as **repair items**, listed under the artifact's own
263
+ repair heading, and finish with exit 0 either way. A shape complaint is a defect in the artifact to be
264
+ repaired in the artifact — never by editing the validator, and never by deleting the row that produced
265
+ the complaint.
266
+
267
+ `ESCALATE` means no artifact could be written at all (the phase directory does not exist, or the
268
+ payload could not be obtained). Report what the agent said and stop; still exit 0.
269
+
270
+ Display the summary:
271
+
272
+ ```
273
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
274
+ pp-core ► VAULT PRACTICE REVIEW COMPLETE
275
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
276
+
277
+ **Phase {N}: {Name}** — arm: {traced | reconstruct}
278
+
279
+ {the artifact's register rate line — the one `validateReviewArtifact` recomputed and accepted}
280
+ {the artifact's register counts line — the one `validateReviewArtifact` recomputed and accepted}
281
+
282
+ Gaps reported: {none. / the payload's loud statements}
283
+
284
+ Full review: {path to VAULT-REVIEW.md}
285
+
286
+ This review reports and does not gate. No verdict above changes a phase status,
287
+ a score, a gap or any deterministic surface — see "Limits of This Review" in the artifact.
288
+
289
+ ───────────────────────────────────────────────────────────────
290
+
291
+ ## ▶ Next
292
+
293
+ `/clear` then one of:
294
+
295
+ - `/pp:verify-work {N}` — UAT testing
296
+ - `/pp:plan-phase {N+1}` — plan next phase
297
+
298
+ ───────────────────────────────────────────────────────────────
299
+ ```
300
+
301
+ The routing above is offered on **every** path — a review that found `FAIL` rows routes exactly the
302
+ same as one that found none. No step in this workflow reads a verdict, a count or a rate into a halt,
303
+ an exit code, a block or a suppressed route, and nothing here writes to `ROADMAP.md`, `STATE.md`,
304
+ `VERIFICATION.md` or any `SUMMARY.md`. The one file this command produces is the artifact.
305
+
306
+ ## 7. Commit (if configured)
307
+
308
+ ```bash
309
+ pp_run query commit "docs(${PADDED_PHASE}): vault practice review" --files "${PHASE_DIR}/${PADDED_PHASE}-VAULT-REVIEW.md"
310
+ ```
311
+
312
+ </process>
313
+
314
+ <success_criteria>
315
+ - [ ] Step 0a ran first and branched on the arm's own output
316
+ - [ ] Vault-disabled project: inert sentence printed, nothing else run, nothing written
317
+ - [ ] Input state detected (A/B/C) — State C exits cleanly
318
+ - [ ] `trace_present_and_loaded` read from the payload's `trace.loaded`; degraded and absent both false
319
+ - [ ] Reconstruct arm taken whenever the flag is false, and named as such in the artifact
320
+ - [ ] Agent spawned with the CLI payload verbatim and the origin-dependent constraint
321
+ - [ ] `{padded}-VAULT-REVIEW.md` written in the phase directory; shape check run over it
322
+ - [ ] The payload's derived figures are never written as the register's lines; any difference is on the `| Disagreements between supplied counts and the rows |` row
323
+ - [ ] The register's own rate and counts lines agree with `validateReviewArtifact` over the rows written
324
+ - [ ] Routing offered unconditionally; no verdict reached any halt, block or status
325
+ </success_criteria>
@@ -41,16 +41,14 @@ _PP_SHIM_NAME="pp-tools.cjs"; _pp_root() { ( for _v in $(env | sed -n 's/^\(GIT_
41
41
  GSD_WS=""
42
42
  echo "$ARGUMENTS" | grep -qE -- '--ws[[:space:]]+[^[:space:]]+' && GSD_WS=$(echo "$ARGUMENTS" | grep -oE -- '--ws[[:space:]]+[^[:space:]]+')
43
43
  PHASE_ARG=$(echo "$ARGUMENTS" | sed -E 's/--ws[[:space:]]+[^[:space:]]+//g' | xargs)
44
- TIER_FLAG=$(printf '%s\n' "$ARGUMENTS" | grep -oE -- '--tier[[:space:]=]+(light|standard|heavy)' | head -1 | sed -E 's/^--tier[[:space:]=]+/--tier=/')
45
- PHASE_ARG=$(echo "$PHASE_ARG" | sed -E 's/--tier[[:space:]]+[a-z]+//g' | xargs)
46
44
 
47
- INIT=$(pp_run query init.verify-work "${PHASE_ARG}" ${GSD_WS} ${TIER_FLAG})
45
+ INIT=$(pp_run query init.verify-work "${PHASE_ARG}" ${GSD_WS})
48
46
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
49
47
  AGENT_SKILLS_PLANNER=$(pp_run query agent-skills pp-planner)
50
48
  AGENT_SKILLS_CHECKER=$(pp_run query agent-skills pp-plan-checker)
51
49
  ```
52
50
 
53
- Parse JSON for: `planner_model`, `checker_model`, `commit_docs`, `plan_checker_enabled`, `plan_check_passes`, `phase_tier` (the phase's ceremony tier), `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `has_verification`, `uat_path`.
51
+ Parse JSON for: `planner_model`, `checker_model`, `commit_docs`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `has_verification`, `uat_path`.
54
52
 
55
53
  ```bash
56
54
  # MVP mode detection via the centralized phase.mvp-mode resolver.
@@ -113,7 +111,7 @@ Continue to `create_uat_file`.
113
111
  Before UAT, check UI capability activation and whether Playwright/Puppeteer MCP tools are available.
114
112
 
115
113
  ```bash
116
- PLAN_HOOKS_JSON=$(pp_run loop render-hooks plan:pre --raw --phase "${phase_number}" ${TIER_FLAG}) || exit 1
114
+ PLAN_HOOKS_JSON=$(pp_run loop render-hooks plan:pre --raw)
117
115
  UI_SPEC_FILE=$(ls "${PHASE_DIR}"/*-UI-SPEC.md 2>/dev/null | head -1)
118
116
  ```
119
117
 
@@ -460,10 +458,15 @@ Present summary:
460
458
  **If issues == 0:**
461
459
 
462
460
  ```bash
463
- VERIFY_POST_HOOKS_JSON=$(pp_run loop render-hooks verify:post --raw --phase "${phase_number}" ${GSD_WS} ${TIER_FLAG}) || exit 1
461
+ VERIFY_POST_HOOKS_JSON=$(pp_run loop render-hooks verify:post --raw)
462
+ printf '%s' "$VERIFY_POST_HOOKS_JSON" | node -e "let h=[];try{h=JSON.parse(require('fs').readFileSync(0,'utf-8')).activeHooks||[]}catch{}console.log('VERIFY_POST_HOOKS_JSON='+JSON.stringify({activeHooks:h}))"
464
463
  SECURITY_FILE=$(ls "${PHASE_DIR}"/*-SECURITY.md 2>/dev/null | head -1)
465
464
  ```
466
465
 
466
+ **Vault practice gates.** Read `pp-core/workflows/execute-phase/steps/verify-post-gate.md` — it
467
+ dispatches this point's blocking gates to the generic enforcer, which owns the halt. Rendering the
468
+ payload above is not enforcing it, and the branch below marks the phase complete.
469
+
467
470
  Resolve active step hooks from `VERIFY_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "secure-phase"`.
468
471
 
469
472
  If an active secure-phase step hook exists AND `SECURITY_FILE` is empty, dispatch the registry-provided skill stem:
@@ -621,8 +624,6 @@ On return:
621
624
  <step name="verify_gap_plans">
622
625
  **Verify fix plans with checker:**
623
626
 
624
- **Skip if** `plan_checker_enabled` is false (the tier, or the project's explicit `false`) — go to `present_ready`.
625
-
626
627
  Run this Bash command to emit the branded Validate stage banner (renders on stderr, auto-degrades on non-TTY — do NOT print an inline ASCII banner, that would double-print):
627
628
  ```bash
628
629
  pp_run banner validate "VERIFYING"
@@ -674,11 +675,11 @@ On return:
674
675
  </step>
675
676
 
676
677
  <step name="revision_loop">
677
- **Iterate planner ↔ checker until plans pass (max `plan_check_passes`):**
678
+ **Iterate planner ↔ checker until plans pass (max 3):**
678
679
 
679
- **If iteration_count < plan_check_passes:**
680
+ **If iteration_count < 3:**
680
681
 
681
- Display: `Sending back to planner for revision... (iteration {N}/{plan_check_passes})`
682
+ Display: `Sending back to planner for revision... (iteration {N}/3)`
682
683
 
683
684
  Spawn pp-planner with revision context:
684
685
 
@@ -717,7 +718,7 @@ Do NOT replan from scratch unless issues are fundamental.
717
718
  After planner returns → spawn checker again (verify_gap_plans logic)
718
719
  Increment iteration_count
719
720
 
720
- **If iteration_count >= plan_check_passes:**
721
+ **If iteration_count >= 3:**
721
722
 
722
723
  Display: `Max iterations reached. {N} issues remain.`
723
724