@pareto4/pp-core 1.13.0 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/.claude-plugin/plugin.json +3 -3
  2. package/README.md +26 -9
  3. package/agents/pp-code-reviewer.md +2 -1
  4. package/agents/pp-debug-session-manager.md +9 -6
  5. package/agents/pp-executor.md +10 -13
  6. package/agents/pp-plan-checker.md +4 -4
  7. package/agents/pp-planner.md +23 -9
  8. package/agents/pp-vault-reviewer.md +244 -21
  9. package/agents/pp-verifier.md +7 -3
  10. package/bin/install.js +10 -3
  11. package/commands/pp-execute-phase.md +1 -3
  12. package/commands/pp-ns-review.md +1 -1
  13. package/commands/pp-plan-phase.md +1 -2
  14. package/commands/pp-vault-review.md +5 -3
  15. package/commands/pp-verify-work.md +1 -2
  16. package/gemini-extension.json +1 -1
  17. package/hooks/dist/pp-check-update.js +0 -0
  18. package/hooks/dist/pp-cursor-session-start.js +1 -1
  19. package/hooks/dist/pp-statusline.js +1 -1
  20. package/hooks/dist/pp-update-banner.js +0 -0
  21. package/hooks/pp-check-update.js +0 -0
  22. package/hooks/pp-cursor-session-start.js +1 -1
  23. package/hooks/pp-statusline.js +1 -1
  24. package/hooks/pp-update-banner.js +0 -0
  25. package/package.json +14 -19
  26. package/pareto-skills/_shared/create-project-board.sh +0 -0
  27. package/pareto-skills/pp-project-setup/SKILL.md +1 -1
  28. package/pp-core/bin/check-latest-version.cjs +0 -0
  29. package/pp-core/bin/detect-build-commands.sh +2 -2
  30. package/pp-core/bin/lib/capability-activation.cjs +83 -9
  31. package/pp-core/bin/lib/capability-registry.cjs +396 -82
  32. package/pp-core/bin/lib/capability-state.cjs +160 -4
  33. package/pp-core/bin/lib/capability-writer.cjs +19 -5
  34. package/pp-core/bin/lib/check-command-router.cjs +3334 -946
  35. package/pp-core/bin/lib/clusters.cjs +3 -0
  36. package/pp-core/bin/lib/command-aliases.cjs +0 -35
  37. package/pp-core/bin/lib/commands.cjs +9 -4
  38. package/pp-core/bin/lib/compose-bindings.cjs +251 -88
  39. package/pp-core/bin/lib/config-loader.cjs +7 -103
  40. package/pp-core/bin/lib/config-schema.cjs +1 -1
  41. package/pp-core/bin/lib/config.cjs +154 -62
  42. package/pp-core/bin/lib/decisions.cjs +4 -4
  43. package/pp-core/bin/lib/discovery-vault-check.cjs +246 -0
  44. package/pp-core/bin/lib/frontmatter.cjs +1 -38
  45. package/pp-core/bin/lib/git-base-branch.cjs +13 -11
  46. package/pp-core/bin/lib/git-provider.cjs +4 -4
  47. package/pp-core/bin/lib/init-command-router.cjs +5 -8
  48. package/pp-core/bin/lib/init-contract.cjs +648 -738
  49. package/pp-core/bin/lib/init-interactive.cjs +76 -4
  50. package/pp-core/bin/lib/init.cjs +24 -51
  51. package/pp-core/bin/lib/install-profiles.cjs +1 -1
  52. package/pp-core/bin/lib/io.cjs +8 -0
  53. package/pp-core/bin/lib/loop-resolver.cjs +189 -129
  54. package/pp-core/bin/lib/org-profile.cjs +469 -152
  55. package/pp-core/bin/lib/package-identity.cjs +3 -3
  56. package/pp-core/bin/lib/phase-command-router.cjs +1 -29
  57. package/pp-core/bin/lib/phase-scope.cjs +1 -1
  58. package/pp-core/bin/lib/phase.cjs +11 -253
  59. package/pp-core/bin/lib/platform-bindings.cjs +40 -19
  60. package/pp-core/bin/lib/practices-review-check.cjs +15 -9
  61. package/pp-core/bin/lib/profile-output.cjs +1 -1
  62. package/pp-core/bin/lib/shell-command-projection.cjs +104 -1
  63. package/pp-core/bin/lib/state.cjs +11 -85
  64. package/pp-core/bin/lib/template.cjs +1 -1
  65. package/pp-core/bin/lib/vault-audit.cjs +650 -126
  66. package/pp-core/bin/lib/vault-command-router.cjs +2604 -367
  67. package/pp-core/bin/lib/vault-coordinates.cjs +13 -4
  68. package/pp-core/bin/lib/vault-index.cjs +699 -0
  69. package/pp-core/bin/lib/vault-mode.cjs +235 -0
  70. package/pp-core/bin/lib/vault-practices.cjs +139 -72
  71. package/pp-core/bin/lib/vault-review.cjs +194 -451
  72. package/pp-core/bin/lib/vault-seal.cjs +1936 -0
  73. package/pp-core/bin/lib/vault-standards.cjs +1505 -0
  74. package/pp-core/bin/lib/vault-trace.cjs +607 -254
  75. package/pp-core/bin/lib/vault-update.cjs +9 -33
  76. package/pp-core/bin/lib/vault-write-back.cjs +70 -42
  77. package/pp-core/bin/lib/vault.cjs +2734 -920
  78. package/pp-core/bin/lib/verify.cjs +0 -11
  79. package/pp-core/bin/portable-timeout.sh +1 -1
  80. package/pp-core/bin/pp-tools.cjs +184 -54
  81. package/pp-core/bin/shared/config-defaults.manifest.json +0 -7
  82. package/pp-core/bin/shared/config-schema.manifest.json +0 -3
  83. package/pp-core/bin/shared/model-catalog.json +2 -2
  84. package/pp-core/bin/verify-reapply-patches.cjs +0 -0
  85. package/pp-core/org-profile.json +26 -0
  86. package/pp-core/references/autonomous-best-effort.md +8 -7
  87. package/pp-core/references/loop-hook-dispatch.md +0 -25
  88. package/pp-core/references/planner-gap-closure.md +0 -1
  89. package/pp-core/references/planner-tdd.md +0 -1
  90. package/pp-core/references/planner-vault-practices.md +17 -34
  91. package/pp-core/references/planner-vault-standards.md +320 -0
  92. package/pp-core/references/planning-config.md +0 -8
  93. package/pp-core/references/practice-arithmetic-contract.md +121 -0
  94. package/pp-core/references/tdd.md +0 -1
  95. package/pp-core/references/verifier-practice-verdicts.md +263 -0
  96. package/pp-core/templates/README.md +1 -0
  97. package/pp-core/templates/VAULT-REVIEW.md +202 -0
  98. package/pp-core/templates/phase-prompt.md +2 -6
  99. package/pp-core/templates/practices/README.md +20 -23
  100. package/pp-core/workflows/_runtime-launcher.snippet.sh +0 -0
  101. package/pp-core/workflows/add-tests.md +55 -4
  102. package/pp-core/workflows/audit-fix.md +63 -2
  103. package/pp-core/workflows/audit-milestone.md +1 -1
  104. package/pp-core/workflows/autonomous.md +85 -11
  105. package/pp-core/workflows/check-todos.md +13 -2
  106. package/pp-core/workflows/code-review-fix.md +59 -3
  107. package/pp-core/workflows/code-review.md +10 -1
  108. package/pp-core/workflows/complete-milestone.md +1 -1
  109. package/pp-core/workflows/debug.md +54 -4
  110. package/pp-core/workflows/discovery-sprint/prefill.md +19 -7
  111. package/pp-core/workflows/discovery-sprint/prototype.md +46 -49
  112. package/pp-core/workflows/discovery-sprint/session-13-gates.md +2 -0
  113. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +1 -3
  114. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +1 -3
  115. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +1 -3
  116. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +20 -24
  117. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +1 -3
  118. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +1 -3
  119. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +1 -3
  120. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +1 -3
  121. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +1 -3
  122. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +1 -6
  123. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +1 -3
  124. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +1 -3
  125. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -3
  126. package/pp-core/workflows/discovery-sprint/templates/context-template.md +1 -1
  127. package/pp-core/workflows/discovery-sprint/vault-context.md +213 -0
  128. package/pp-core/workflows/discovery-sprint.md +7 -3
  129. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +4 -4
  130. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +43 -1
  131. package/pp-core/workflows/discovery-to-build.md +10 -2
  132. package/pp-core/workflows/discuss-phase.md +3 -10
  133. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +329 -0
  134. package/pp-core/workflows/execute-phase/steps/client-context-gate.md +3 -2
  135. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -1
  136. package/pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md +98 -0
  137. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
  138. package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +6 -8
  139. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +115 -14
  140. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +322 -20
  141. package/pp-core/workflows/execute-phase/steps/verifier-practice-payload.md +165 -0
  142. package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +163 -0
  143. package/pp-core/workflows/execute-phase.md +155 -128
  144. package/pp-core/workflows/execute-plan.md +56 -6
  145. package/pp-core/workflows/fast.md +53 -4
  146. package/pp-core/workflows/help/modes/full.md +3 -3
  147. package/pp-core/workflows/init/steps/client-context-capture.md +101 -0
  148. package/pp-core/workflows/init/steps/gitlab-provisioning.md +130 -0
  149. package/pp-core/workflows/init/steps/project-md-deferral-narration.md +66 -0
  150. package/pp-core/workflows/init/steps/reachability-outcome.md +70 -0
  151. package/pp-core/workflows/init.md +232 -272
  152. package/pp-core/workflows/new-project.md +31 -23
  153. package/pp-core/workflows/plan-phase.md +98 -45
  154. package/pp-core/workflows/quick.md +117 -3
  155. package/pp-core/workflows/secure-phase.md +1 -1
  156. package/pp-core/workflows/ship.md +57 -28
  157. package/pp-core/workflows/undo.md +1 -1
  158. package/pp-core/workflows/update.md +3 -3
  159. package/pp-core/workflows/validate-phase.md +58 -3
  160. package/pp-core/workflows/vault-review.md +282 -22
  161. package/pp-core/workflows/verify-work.md +12 -14
  162. package/scripts/changeset/cli.cjs +6 -5
  163. package/scripts/changeset/lint.cjs +0 -0
  164. package/scripts/changeset/new.cjs +33 -50
  165. package/scripts/dev-env.cjs +25 -85
  166. package/scripts/gen-capability-registry.cjs +199 -11
  167. package/scripts/gen-practice-arithmetic-contract.cjs +412 -0
  168. package/scripts/lib/allowlist-ratchet.cjs +1 -103
  169. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -2
  170. package/scripts/lint-docs-required.cjs +0 -0
  171. package/scripts/lint-pr-check-project-dir.cjs +1 -3
  172. package/scripts/lint-test-file-count.allowlist.json +12 -10
  173. package/scripts/practice-arithmetic-contract.allow.json +35 -0
  174. package/scripts/prompt-injection-scan.sh +22 -17
  175. package/scripts/record-vault-mode-golden.cjs +89 -0
  176. package/scripts/release-channel.cjs +0 -0
  177. package/scripts/sync-manifest-versions.cjs +5 -4
  178. package/scripts/sync-rulesets.sh +1 -1
  179. package/scripts/update-size-baseline.cjs +30 -274
  180. package/scripts/vault-calibrate.cjs +996 -0
  181. package/scripts/verify-opengsd-sweep.cjs +3 -5
  182. package/scripts/workflow-size.cjs +9 -61
  183. package/pp-core/bin/lib/dispatch-plan.cjs +0 -483
  184. package/pp-core/bin/lib/install-run.cjs +0 -536
  185. package/pp-core/bin/lib/loop-gates.cjs +0 -213
  186. package/pp-core/bin/lib/phase-manifest.cjs +0 -527
  187. package/pp-core/bin/lib/phase-tier.cjs +0 -480
  188. package/pp-core/bin/lib/plan-graph.cjs +0 -368
  189. package/pp-core/references/executor-vault-trace.md +0 -47
  190. package/pp-core/references/planner-plan-graph.md +0 -66
  191. package/pp-core/workflows/discovery-sprint/vault-fetch.md +0 -37
  192. package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +0 -45
  193. package/pp-core/workflows/execute-phase/steps/install-run.md +0 -29
  194. package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +0 -23
  195. package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +0 -73
  196. package/pp-core/workflows/execute-phase/steps/vault-door-gate.md +0 -19
  197. package/pp-core/workflows/plan-phase/steps/phase-tier.md +0 -43
  198. package/scripts/lib/size-budget.cjs +0 -311
  199. package/scripts/lint-neutrality-names.cjs +0 -694
  200. package/scripts/lint-org-coordinates.allowlist.json +0 -35
  201. package/scripts/lint-org-coordinates.cjs +0 -426
@@ -13,7 +13,7 @@
13
13
  * Command surface: pp-tools loop render-hooks <point>
14
14
  *
15
15
  * Exports (three things):
16
- * resolveLoopHooks({ point, registry, config }) → { point, activeHooks }
16
+ * resolveLoopHooks({ point, registry, config, vaultMode?, recordedArtifacts? }) → { point, activeHooks }
17
17
  * renderLoopHooks(resolved) → markdown string
18
18
  * cmdLoopRenderHooks(cwd, point, raw, options) — I/O entry point
19
19
  *
@@ -30,19 +30,17 @@
30
30
  */
31
31
  // eslint-disable-next-line @typescript-eslint/no-require-imports
32
32
  const ioMod = require("./io.cjs");
33
- const { output: coreOutput, error: coreError, ERROR_REASON } = ioMod;
33
+ const { output: coreOutput, error: coreError } = ioMod;
34
34
  // eslint-disable-next-line @typescript-eslint/no-require-imports
35
35
  const configLoaderModule = require("./config-loader.cjs");
36
- const { loadConfig, unreadableConfigFiles, describeUnreadableConfig } = configLoaderModule;
36
+ const { loadConfig } = configLoaderModule;
37
37
  // eslint-disable-next-line @typescript-eslint/no-require-imports
38
38
  const capabilityStateModule = require("./capability-state.cjs");
39
- const { resolveCapabilityRuntimeState } = capabilityStateModule;
39
+ const { resolveCapabilityRuntimeState, vaultModeFilterVerdict, resolveInjectedVaultMode, SEAL_FIELD, sealRenderVerdict, resolveInjectedRecordedArtifacts, resolveInjectedPlatformBindingsPresent, } = capabilityStateModule;
40
40
  // ─── Capability-activation engine (single owner for config-key precedence) ────
41
41
  // eslint-disable-next-line @typescript-eslint/no-require-imports
42
42
  const capabilityActivationModule = require("./capability-activation.cjs");
43
43
  const { _getNestedConfigValue, _readRawConfigKey, _resolveActivationValue, resolveConfigKey } = capabilityActivationModule;
44
- // ─── Phase ceremony tier (pack 9): `--phase` / `--tier` overlay ───────────────
45
- const phase_tier_cjs_1 = require("./phase-tier.cjs");
46
44
  // ─── Canonical points (derived from LOOP_HOST_CONTRACT — authoritative 12) ───
47
45
  // FIX 2: Derive the authoritative canonical set from LOOP_HOST_CONTRACT so it
48
46
  // cannot drift from the host contract. CANONICAL_POINTS_FALLBACK is kept as an
@@ -105,7 +103,7 @@ function _getCanonicalPoints(_registry) {
105
103
  * resolved against `config`; active iff truthy. Inactive hooks are filtered out.
106
104
  */
107
105
  function resolveLoopHooks(input) {
108
- const { point, registry, config, cwd, capabilityStatesById } = input;
106
+ const { point, registry, config, cwd, capabilityStatesById, vaultMode, recordedArtifacts, platformBindingsPresent, } = input;
109
107
  // Validate point
110
108
  const canonicalPoints = _getCanonicalPoints(registry);
111
109
  if (!canonicalPoints.includes(point)) {
@@ -124,6 +122,12 @@ function resolveLoopHooks(input) {
124
122
  }
125
123
  const entryMap = entry;
126
124
  const activeHooks = [];
125
+ // Phase 15 D-02/D-28: the per-hook `vaultMode` filter, one rule owned by
126
+ // capability-state (`vaultModeFilterVerdict`), evaluated against the injected
127
+ // mode VALUE. The field is read here and never copied into an ActiveHook.
128
+ function modeOk(hook, kind) {
129
+ return vaultModeFilterVerdict(hook['vaultMode'], kind, vaultMode);
130
+ }
127
131
  // Helper: check activation using single-key precedence resolver (FIX 1 + FIX 3)
128
132
  function isActive(hook) {
129
133
  const when = hook['when'];
@@ -150,6 +154,59 @@ function resolveLoopHooks(input) {
150
154
  // `=== true` is the correct fail-closed check (not `!== false`).
151
155
  return state.active === true;
152
156
  }
157
+ // Phase 16 D-19: the state the seal rule reads for a gate's capability. With
158
+ // no state map at all, every capability is treated as active, exactly as
159
+ // `isCapabilityActive` does: normal gates keep today's behaviour and a seal
160
+ // gate drops (it never renders while its capability is active). With a map,
161
+ // a missing entry is `undefined`, which the rule drops (fail closed).
162
+ function gateStateOf(capId) {
163
+ if (!capabilityStatesById)
164
+ return { enabled: true, active: true };
165
+ return capabilityStatesById instanceof Map
166
+ ? capabilityStatesById.get(capId)
167
+ : capabilityStatesById[capId];
168
+ }
169
+ // Phase 16 D-19 / T-16-33: the injected predicate is asked at most once per
170
+ // resolve, however many seal gates the point declares. An absent thunk is
171
+ // the inert answer (no seal renders).
172
+ let recordedArtifactsMemo;
173
+ function recordedArtifactsOnce() {
174
+ if (recordedArtifactsMemo === undefined) {
175
+ recordedArtifactsMemo = resolveInjectedRecordedArtifacts(recordedArtifacts);
176
+ }
177
+ return recordedArtifactsMemo;
178
+ }
179
+ // Quick 261015-ect (QD-13): the bindings measurement, asked at most once per
180
+ // resolve and only for a capability that declares it needs the file. A
181
+ // missing thunk is `null` (unmeasured), which drops nothing.
182
+ let bindingsPresentMemo;
183
+ function bindingsPresentOnce() {
184
+ if (bindingsPresentMemo === undefined) {
185
+ bindingsPresentMemo = resolveInjectedPlatformBindingsPresent(platformBindingsPresent);
186
+ }
187
+ return bindingsPresentMemo;
188
+ }
189
+ function requiresBindings(capId) {
190
+ // Prototype-pollution guard (inline literal, CodeQL barrier), as the
191
+ // registry generator guards capability ids.
192
+ if (capId === '' || capId === '__proto__' || capId === 'constructor' || capId === 'prototype')
193
+ return false;
194
+ const caps = registry['capabilities'];
195
+ if (!caps || typeof caps !== 'object' || Array.isArray(caps))
196
+ return false;
197
+ if (!Object.prototype.hasOwnProperty.call(caps, capId))
198
+ return false;
199
+ const cap = caps[capId];
200
+ if (!cap || typeof cap !== 'object')
201
+ return false;
202
+ return cap['requiresPlatformBindings'] === true;
203
+ }
204
+ // A capability that requires bindings renders nothing where the file is
205
+ // MEASURED absent: the project is not governed by it (its own description says
206
+ // it is inert unless the bindings exist).
207
+ function droppedForBindings(capId) {
208
+ return requiresBindings(capId) && bindingsPresentOnce() === false;
209
+ }
153
210
  // Helper: safe string array
154
211
  function toStringArray(v) {
155
212
  if (!Array.isArray(v))
@@ -201,6 +258,10 @@ function resolveLoopHooks(input) {
201
258
  const capId = typeof hook['capId'] === 'string' ? hook['capId'] : '';
202
259
  if (!isCapabilityActive(capId))
203
260
  continue;
261
+ if (droppedForBindings(capId))
262
+ continue;
263
+ if (!modeOk(hook, 'step'))
264
+ continue;
204
265
  if (!isActive(hook))
205
266
  continue;
206
267
  const ref = (typeof hook['ref'] === 'object' && hook['ref'] !== null)
@@ -235,6 +296,10 @@ function resolveLoopHooks(input) {
235
296
  const capId = typeof hook['capId'] === 'string' ? hook['capId'] : '';
236
297
  if (!isCapabilityActive(capId))
237
298
  continue;
299
+ if (droppedForBindings(capId))
300
+ continue;
301
+ if (!modeOk(hook, 'contribution'))
302
+ continue;
238
303
  if (!isActive(hook))
239
304
  continue;
240
305
  const into = typeof hook['into'] === 'string' ? hook['into'] : undefined;
@@ -268,9 +333,24 @@ function resolveLoopHooks(input) {
268
333
  if (!hook || typeof hook !== 'object')
269
334
  continue;
270
335
  const capId = typeof hook['capId'] === 'string' ? hook['capId'] : '';
271
- if (!isCapabilityActive(capId))
336
+ // Quick 261015-ect (QD-13): gates too, seal gates included. A project with
337
+ // no bindings holds no engine-written vault records (D-37/D-42), and every
338
+ // vault gate already answers inert there, so nothing is left ungated.
339
+ if (droppedForBindings(capId))
272
340
  continue;
273
- if (!isActive(hook))
341
+ // Phase 16 D-19 / research C-1: the mode filter runs FIRST, so a
342
+ // mode-filtered seal gate never consults the recorded-artifacts predicate
343
+ // (a default standards project pays no planning-tree read for a seal it
344
+ // cannot render). `modeOk` is pure, so the order changes no other result.
345
+ if (!modeOk(hook, 'gate'))
346
+ continue;
347
+ const verdict = sealRenderVerdict(hook[SEAL_FIELD], gateStateOf(capId), recordedArtifactsOnce);
348
+ if (verdict === 'drop')
349
+ continue;
350
+ // 'normal': today's path, `when` evaluated. 'seal': the `when` IS the
351
+ // switch that is off (D-18), so it is not read; the seal rule already
352
+ // required an enabled, config-inactive capability and recorded artifacts.
353
+ if (verdict === 'normal' && !isActive(hook))
274
354
  continue;
275
355
  const when = typeof hook['when'] === 'string' ? hook['when'] : undefined;
276
356
  const check = hook['check'] !== undefined ? hook['check'] : undefined;
@@ -382,80 +462,6 @@ function renderLoopHooks(resolved) {
382
462
  return lines.join('\n');
383
463
  }
384
464
  // ─── I/O command handler ───────────────────────────────────────────────────────
385
- /**
386
- * The refusal text for a caller that resolves hooks over an unreadable config.
387
- * One wording for every such caller (`loop render-hooks`, `phase.dispatch-plan`).
388
- */
389
- function unreadableConfigRefusal(what, unreadable) {
390
- return (`${what} refused: the project config cannot be read, so no hook can be resolved ` +
391
- `(rendering anyway would silently drop every hook the config switches on, a required client vault among them). ` +
392
- `${describeUnreadableConfig(unreadable)}. Repair or remove the file, then re-run. Stop this workflow here.`);
393
- }
394
- /**
395
- * Resolve the active hooks at `point` for the project at `cwd`, exactly as
396
- * `loop render-hooks` does: refuse an unreadable config, load the config
397
- * snapshot once, overlay the phase tier's gate booleans when one is given,
398
- * resolve capability state over the same snapshot, then resolve the hooks.
399
- * The ONE owner of "which hooks are active here" for every caller that needs
400
- * the answer in code (`loop render-hooks`, `phase.dispatch-plan`'s executor
401
- * contributions). Never exits; the caller decides how to report a refusal.
402
- */
403
- function resolveProjectLoopHooks(cwd, point, opts = {}) {
404
- // Fail closed on a config that exists but cannot be read. Every hook's `when`
405
- // and activation key is read from it, and both readers below swallow a parse
406
- // failure into the defaults, so rendering anyway drops exactly the hooks the
407
- // project switched on — a required client vault's fetch and blocking gate
408
- // among them. A MISSING config is not refused: that is a fresh project, and
409
- // the defaults are its real settings.
410
- const unreadable = unreadableConfigFiles(cwd);
411
- if (unreadable.length > 0) {
412
- return { ok: false, reason: 'config_unreadable', message: unreadableConfigRefusal(opts.what ?? `loop render-hooks ${point}`, unreadable) };
413
- }
414
- // Load the config snapshot ONCE and share it with both the capability-state
415
- // resolver (via configOverride) and loop-hook resolution, so federated keys
416
- // present in loadConfig resolve identically for `active` and for hook when/
417
- // configValues. Keys absent from loadConfig still fall through to raw
418
- // .planning/config.json reads (precedence levels 2-3) in each pass.
419
- let config;
420
- try {
421
- config = loadConfig(cwd);
422
- }
423
- catch {
424
- config = {};
425
- }
426
- if (opts.phaseTier)
427
- config = (0, phase_tier_cjs_1.overlayTierGates)(config, opts.phaseTier);
428
- const state = resolveCapabilityRuntimeState(cwd, opts.configDir, config);
429
- // Registry is the static generated module — same object capability-state uses internally.
430
- // eslint-disable-next-line @typescript-eslint/no-require-imports
431
- const registry = require('./capability-registry.cjs');
432
- const capabilityStatesById = new Map();
433
- for (const cap of state.capabilities || []) {
434
- capabilityStatesById.set(cap.id, cap);
435
- }
436
- try {
437
- const resolved = resolveLoopHooks({ point, registry, config, cwd, capabilityStatesById });
438
- return { ok: true, resolved, warnings: state.warnings ?? [] };
439
- }
440
- catch (err) {
441
- return { ok: false, reason: 'invalid_point', message: (err instanceof Error) ? err.message : String(err) };
442
- }
443
- }
444
- /**
445
- * The `fragment.inline` text of every active contribution into `role`, in
446
- * resolved order, with the capability that contributed it.
447
- */
448
- function contributionsInto(resolved, role) {
449
- const out = [];
450
- for (const h of resolved.activeHooks) {
451
- if (h.kind !== 'contribution' || h.into !== role)
452
- continue;
453
- if (typeof h.fragment?.inline !== 'string')
454
- continue;
455
- out.push({ capId: h.capId, fragment: h.fragment.inline });
456
- }
457
- return out;
458
- }
459
465
  /**
460
466
  * Command entry point: load registry + config, resolve + render, emit envelope.
461
467
  *
@@ -470,31 +476,39 @@ function contributionsInto(resolved, role) {
470
476
  * pre-cutover keys like `workflow.ui_phase` that live in config.json but are not
471
477
  * yet exposed through loadConfig's whitelist.
472
478
  *
473
- * Unreadable config: when any project config file exists but cannot be parsed
474
- * (`unreadableConfigFiles`), every mode refuses before rendering — exit 1,
475
- * empty stdout, one stderr line naming each file and its parse error.
476
- *
477
- * --active-cap <capId>: when present, resolves hooks for <point> exactly as the
478
- * normal path does, then prints exactly `true` (if any resolved activeHook has
479
- * capId === <capId>) or `false` followed by a single newline, and exits 0.
479
+ * --active-cap <capId>: when present, resolves hooks for <point> as the normal
480
+ * path does EXCEPT for one input — the bindings measurement (quick 261015-ect,
481
+ * QD-13) is not applied — then prints exactly `true` (if any resolved
482
+ * activeHook has capId === <capId>) or `false` followed by a single newline,
483
+ * and exits 0. So on a project with no `.platform-bindings.json` it still
484
+ * answers the config switch alone: /pp-init Step 2.6's unbound arm ("a
485
+ * resolving profile with no bindings halts loudly") is reachable only because
486
+ * this read ignores bindings (16-16 D-81), and the D-28 parity test states
487
+ * parity on bindings-present shapes only.
480
488
  * No JSON envelope is emitted — output is clean for shell $(…) capture.
481
489
  * Missing <capId> value → coreError + non-zero exit.
482
490
  * Unknown/inactive capId → `false` (not an error).
483
491
  *
484
- * --into <role>: resolves exactly as the normal path does, then prints the
485
- * `fragment.inline` text of each active contribution whose `into` equals
486
- * <role>, in resolved order, separated by one blank line, with a trailing
487
- * newline when anything printed. Nothing at all (empty stdout, exit 0) when no
488
- * contribution matches. No JSON envelope; `--raw` is ignored. `--active-cap`
489
- * takes precedence: given both, `--into` is ignored.
492
+ * --into <role> (Phase 15 D-12/D-30): resolves exactly as the normal path does,
493
+ * then prints the `fragment.inline` text of each active contribution whose
494
+ * `into` equals <role>, in resolved order, separated by one blank line, with a
495
+ * trailing newline when anything printed. Nothing at all (empty stdout, exit
496
+ * 0) when no contribution matches. No JSON envelope; `--raw` is ignored. This
497
+ * is the text execute-phase interpolates verbatim into a spawn prompt (15-09).
498
+ * `--active-cap` takes precedence: given both, `--into` is ignored.
490
499
  *
491
- * --phase <N> [--tier light|standard|heavy] (pack 9): resolve the phase's
492
- * ceremony tier (src/phase-tier.cts) and overlay its gate booleans
493
- * (`workflow.research`, `workflow.pattern_mapper`, …) on the config snapshot
494
- * BEFORE capability state and hook activation read it, so every `when` /
495
- * activation key of those gates answers the tier-resolved value. Without
496
- * `--phase` the project's own booleans are read as before (an explicitly
497
- * invoked gate command such as /pp-secure-phase is not governed by a tier).
500
+ * options.resolveVaultMode: an injected `() => mode | null` (pp-tools builds it
501
+ * on vaultModeVerdict). This module never imports vault code (D-02).
502
+ *
503
+ * options.resolveRecordedArtifacts: an injected `() => boolean` (pp-tools builds
504
+ * it on the seal leaf's hasRecordedVaultArtifacts); only literal `true` lets a
505
+ * seal gate render (Phase 16 D-19). `--active-cap <cap>` also requires the
506
+ * capability's own state to be active (D-27).
507
+ *
508
+ * options.resolvePlatformBindingsPresent: an injected `() => boolean` (pp-tools
509
+ * builds it on the vault leaf's hasPlatformBindings); only literal `false`
510
+ * drops a `requiresPlatformBindings` capability's hooks (QD-13). `--into` and
511
+ * the envelope read the filtered set; `--active-cap` reads the unfiltered one.
498
512
  */
499
513
  function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
500
514
  if (!point) {
@@ -509,40 +523,85 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
509
523
  }
510
524
  // --into <role> mode: emit matching contribution fragment text only
511
525
  const intoRole = typeof options['into'] === 'string' ? options['into'] : undefined;
512
- if (activeCapId === undefined && intoRole !== undefined && intoRole.trim() === '') {
513
- coreError('Missing value for --into (e.g. --into executor)', ERROR_REASON.USAGE);
526
+ if (activeCapId === undefined && intoRole !== undefined && intoRole === '') {
527
+ coreError('--into requires a <role> value (e.g. --into executor)');
514
528
  return;
515
529
  }
516
530
  const runtimeConfigDir = typeof options['configDir'] === 'string'
517
531
  ? options['configDir']
518
532
  : undefined;
519
- const tierPhase = typeof options['phase'] === 'string' && options['phase'] !== '' ? options['phase'] : null;
520
- const tierCli = typeof options['tier'] === 'string' && options['tier'] !== '' ? options['tier'] : null;
521
- let phaseTier = null;
522
- if (tierPhase !== null || tierCli !== null) {
523
- if (tierCli !== null && !['light', 'standard', 'heavy'].includes(tierCli)) {
524
- coreError('--tier needs one of light, standard, heavy');
525
- return;
526
- }
527
- // The refusal below must come first: never resolve a tier from a config it cannot read.
528
- if (unreadableConfigFiles(cwd).length === 0)
529
- phaseTier = (0, phase_tier_cjs_1.resolvePhaseTierForProject)(cwd, tierPhase, tierCli);
533
+ // Load the config snapshot ONCE and share it with both the capability-state
534
+ // resolver (via configOverride) and loop-hook resolution, so federated keys
535
+ // present in loadConfig resolve identically for `active` and for hook when/
536
+ // configValues — eliminating the previous double loadConfig() call. Note: keys
537
+ // absent from loadConfig still fall through to raw .planning/config.json reads
538
+ // (precedence levels 2-3) in each pass; that residual re-read window is
539
+ // pre-existing (unchanged by this consolidation), not introduced here.
540
+ let config;
541
+ try {
542
+ config = loadConfig(cwd);
530
543
  }
531
- const outcome = resolveProjectLoopHooks(cwd, point, { configDir: runtimeConfigDir, phaseTier });
532
- if (!outcome.ok) {
533
- coreError(outcome.message, outcome.reason === 'config_unreadable' ? ERROR_REASON.CONFIG_PARSE_FAILED : undefined);
544
+ catch {
545
+ config = {};
546
+ }
547
+ const state = resolveCapabilityRuntimeState(cwd, runtimeConfigDir, config);
548
+ // Registry is the static generated module — same object capability-state uses internally.
549
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
550
+ const registry = require('./capability-registry.cjs');
551
+ const capabilityStatesById = new Map();
552
+ for (const cap of state.capabilities || []) {
553
+ capabilityStatesById.set(cap.id, cap);
554
+ }
555
+ // Phase 15 D-02: the vault mode arrives through an injected resolver (pp-tools
556
+ // passes one built on vaultModeVerdict). A missing resolver, a throw or a
557
+ // non-mode answer is `null`, the fail-closed undefined mode (D-28).
558
+ const vaultMode = resolveInjectedVaultMode(options['resolveVaultMode']);
559
+ // Phase 16 D-19: the recorded-artifacts predicate arrives the same way (pp-tools
560
+ // passes one built on the seal leaf's hasRecordedVaultArtifacts). The resolver
561
+ // memoises it and asks it only for an enabled, config-inactive seal gate.
562
+ const resolveRecordedArtifacts = options['resolveRecordedArtifacts'];
563
+ const recordedArtifacts = () => resolveInjectedRecordedArtifacts(resolveRecordedArtifacts);
564
+ // Quick 261015-ect (QD-13): the bindings measurement arrives the same way
565
+ // (pp-tools passes one built on the vault leaf's hasPlatformBindings).
566
+ const resolvePlatformBindingsPresent = options['resolvePlatformBindingsPresent'];
567
+ const platformBindingsPresent = () => resolveInjectedPlatformBindingsPresent(resolvePlatformBindingsPresent);
568
+ let resolved;
569
+ try {
570
+ // --active-cap reads the UNFILTERED resolution (16-16 D-81, see above):
571
+ // without the bindings thunk, so it keeps answering the config switch.
572
+ resolved = resolveLoopHooks({
573
+ point, registry, config, cwd, capabilityStatesById, vaultMode, recordedArtifacts,
574
+ ...(activeCapId === undefined ? { platformBindingsPresent } : {}),
575
+ });
576
+ }
577
+ catch (err) {
578
+ const msg = (err instanceof Error) ? err.message : String(err);
579
+ coreError(msg);
534
580
  return;
535
581
  }
536
- const resolved = outcome.resolved;
537
582
  // --active-cap mode: print exactly 'true' or 'false' with no envelope
538
583
  if (activeCapId !== undefined) {
539
- const isActive = resolved.activeHooks.some((h) => h.capId === activeCapId);
584
+ // Phase 16 D-27 (research C-3): a hook rendering is no longer enough — the
585
+ // capability must itself be active. A seal gate renders for a config-
586
+ // inactive vault-practices, and the doors read `--active-cap` as their
587
+ // switch, so a seal must never make an inactive capability answer `true`.
588
+ // Equivalent to the old answer on every input without a seal: a normal
589
+ // hook renders only for an active capability.
590
+ const isActive = resolved.activeHooks.some((h) => h.capId === activeCapId)
591
+ && capabilityStatesById.get(activeCapId)?.active === true;
540
592
  process.stdout.write(isActive ? 'true\n' : 'false\n');
541
593
  return;
542
594
  }
543
595
  // --into mode: fragment text of the active contributions targeting <role>
544
596
  if (intoRole !== undefined) {
545
- const texts = contributionsInto(resolved, intoRole).map((c) => c.fragment);
597
+ const texts = [];
598
+ for (const h of resolved.activeHooks) {
599
+ if (h.kind !== 'contribution' || h.into !== intoRole)
600
+ continue;
601
+ if (typeof h.fragment?.inline !== 'string')
602
+ continue;
603
+ texts.push(h.fragment.inline);
604
+ }
546
605
  if (texts.length > 0)
547
606
  process.stdout.write(texts.join('\n\n') + '\n');
548
607
  return;
@@ -553,11 +612,8 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
553
612
  activeHooks: resolved.activeHooks,
554
613
  rendered,
555
614
  };
556
- if (phaseTier) {
557
- envelope.phase_tier = { phase: tierPhase, tier: phaseTier.tier, source: phaseTier.source, suppressed: phaseTier.suppressed };
558
- }
559
- if (outcome.warnings.length > 0) {
560
- envelope.warnings = outcome.warnings;
615
+ if (state.warnings && state.warnings.length > 0) {
616
+ envelope.warnings = state.warnings;
561
617
  }
562
618
  // No rawValue third argument, so --raw emits the JSON envelope rather than
563
619
  // plain text. This is deliberate and load-bearing: 23 workflow call sites do
@@ -571,8 +627,6 @@ module.exports = {
571
627
  resolveLoopHooks,
572
628
  renderLoopHooks,
573
629
  cmdLoopRenderHooks,
574
- resolveProjectLoopHooks,
575
- contributionsInto,
576
630
  // Exported for tests
577
631
  _getNestedConfigValue,
578
632
  _resolveActivationValue,
@@ -580,6 +634,12 @@ module.exports = {
580
634
  // Re-exported for identity parity guard (FIX 2: resolveConfigValues in this module
581
635
  // calls resolveConfigKey; exporting it here makes the single-owner contract testable).
582
636
  resolveConfigKey,
637
+ // Re-exported for the same identity pin: the mode-filter rule has one owner,
638
+ // capability-state.cjs (Phase 15 D-02/D-28).
639
+ vaultModeFilterVerdict,
640
+ // Re-exported for the same identity pin: the seal rule has one owner,
641
+ // capability-state.cjs (Phase 16 D-19).
642
+ sealRenderVerdict,
583
643
  CANONICAL_POINTS_FALLBACK,
584
644
  CANONICAL_POINTS,
585
645
  };