@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
@@ -118,15 +118,88 @@ function renderComposeConfirm(prompter, info) {
118
118
  async function runInitInteractive(cwd, prompter, opts = {}) {
119
119
  const sessionOpts = { offline: opts.offline === true, _globalRoot: opts._globalRoot };
120
120
  let wroteBinding = false;
121
+ // PBI 74299: answers the session filled in itself are shown once each, as
122
+ // soon as they are decided, so the operator sees every value before the
123
+ // compose confirmation.
124
+ const shownAutoAnswers = new Set();
121
125
  for (let steps = 1; steps <= MAX_SESSION_STEPS; steps++) {
122
126
  const state = (0, init_contract_cjs_1.initSessionNext)(cwd, sessionOpts);
123
127
  if (state.error !== undefined) {
124
128
  prompter.print(`Error: ${state.message ?? state.error}`);
125
129
  return { ok: false, wroteBinding, steps, violations: [] };
126
130
  }
131
+ for (const auto of state.autoAnswered ?? []) {
132
+ if (shownAutoAnswers.has(auto.id))
133
+ continue;
134
+ shownAutoAnswers.add(auto.id);
135
+ const why = auto.source === 'derived' ? 'derived' : 'set by the org profile';
136
+ prompter.print(` ${auto.id} = ${auto.value} (${why})`);
137
+ }
127
138
  if (state.done) {
128
139
  if (wroteBinding) {
129
- prompter.print('Done — .platform-bindings.json was composed and written.');
140
+ // OD-1: the done line names the capability the session just armed and
141
+ // the step that is now live, so an operator is not left to discover
142
+ // either from the config file.
143
+ //
144
+ // 07-09 (WR-07): a READING, not a claim. The arming write happens
145
+ // only on an ABSENT key, so an operator's explicit `false` — and a
146
+ // hand-edited non-boolean — are still there when the session ends.
147
+ // Printing the armed line over either of those would make the last
148
+ // line `pp-tools init` prints contradict the config file it just
149
+ // finished writing, which is precisely the
150
+ // tool-reports-a-state-that-isn't-real failure this phase exists to
151
+ // end.
152
+ const vaultState = (0, init_contract_cjs_1.readVaultEnabledState)(cwd).state;
153
+ if (vaultState === 'off') {
154
+ prompter.print('Done — .platform-bindings.json was composed and written; vault.enabled was left at false ' +
155
+ 'as this project set it, so the vault capability stays off and /pp-init Step 2.6 stays ' +
156
+ 'inert (arm it with pp-tools config-set vault.enabled true).');
157
+ }
158
+ else if (vaultState === 'malformed') {
159
+ prompter.print('Done — .platform-bindings.json was composed and written; vault.enabled holds a non-boolean ' +
160
+ 'value and was left untouched — pp-tools query init-contract --verify reports it as ' +
161
+ 'INIT_VAULT_ENABLED_MALFORMED; store the boolean with pp-tools config-set vault.enabled true.');
162
+ }
163
+ else if (vaultState === 'armed') {
164
+ // Quick 260915-lmc: vault.practices_enabled is armed beside it under
165
+ // the same absent-only rule, so the line names it only when the
166
+ // READING says true — an explicit false is the project's choice and
167
+ // is not contradicted here.
168
+ const practicesArmed = (0, init_contract_cjs_1.readVaultPracticesEnabledState)(cwd).state === 'armed';
169
+ // Quick 260916-s5n: vault.required is named only when the READING
170
+ // says armed, by the same rule — an explicit false is the project's
171
+ // choice and the last line printed must not contradict it.
172
+ const requiredArmed = (0, init_contract_cjs_1.readVaultRequiredState)(cwd).state === 'armed';
173
+ // Built as a head plus two OPTIONAL clauses carrying no terminator
174
+ // of their own, with exactly one full stop appended at the end. The
175
+ // stop used to live inside the practices branch, which made every
176
+ // further clause a sentence continuing past its own period.
177
+ const head = 'Done — .platform-bindings.json was composed and written; vault.enabled is now true ' +
178
+ '(the vault capability is armed — /pp-init Step 2.6 probes reachability next)';
179
+ // 15-REVIEW WR-09: in standards mode no phase records practices (the
180
+ // practices planner fragment and trace gates are filtered out), so the
181
+ // clause names what IS recorded and checked there, whatever the
182
+ // practices switch reads.
183
+ const practicesClause = (0, init_contract_cjs_1.readVaultStandardsModeActive)(cwd)
184
+ ? '; vault.mode is standards, so each plan records which client standards documents governed it ' +
185
+ '(<vault_standards>), checked by the vault.trace gate'
186
+ : practicesArmed
187
+ ? '; vault.practices_enabled is true, so each phase records which client vault practices it applied'
188
+ : '';
189
+ const requiredClause = requiredArmed
190
+ ? '; vault.required is true, so a client vault miss, a failed fetch or an unwritable vault summary ' +
191
+ 'stops the build at plan:pre and execute:pre'
192
+ : '';
193
+ prompter.print(head + practicesClause + requiredClause + '.');
194
+ }
195
+ else {
196
+ // 'absent' — unreachable after an approved compose (the arming
197
+ // write fires on exactly this state), kept defensive for the case
198
+ // where that write itself failed. It says only what is certain:
199
+ // the binding was written, and it does NOT claim the capability is
200
+ // on, because the key is not there to say so.
201
+ prompter.print('Done — .platform-bindings.json was composed and written.');
202
+ }
130
203
  }
131
204
  else if (typeof state.info === 'string') {
132
205
  prompter.print(state.info);
@@ -168,26 +241,9 @@ async function runInitInteractive(cwd, prompter, opts = {}) {
168
241
  preselectedLabel: state.preselectedLabel,
169
242
  optionalAnswer: state.optionalAnswer,
170
243
  });
171
- // INIT-PATCH-C: Enter accepts a carried default, "none" clears it.
172
- // Scoped to an OPTIONAL step carrying a usable `preselected` — today
173
- // that is only `org_profile` when exactly one org-profile-shaped
174
- // package was detected. `provider` also carries a `preselected` but is
175
- // NOT `optionalAnswer`, so the `state.optionalAnswer === true` guard
176
- // below leaves it untouched: a blank answer there still falls through
177
- // to `initSessionAnswer` verbatim, which still rejects it and re-asks
178
- // exactly as today.
179
- const hasUsableDefault = state.optionalAnswer === true && typeof state.preselected === 'string' && state.preselected.trim() !== '';
180
- let submittedAnswer = answer;
181
- if (hasUsableDefault) {
182
- const trimmedLower = answer.trim().toLowerCase();
183
- if (answer.trim() === '') {
184
- submittedAnswer = state.preselected;
185
- }
186
- else if (trimmedLower === 'none') {
187
- submittedAnswer = '';
188
- }
189
- }
190
- const result = (0, init_contract_cjs_1.initSessionAnswer)(cwd, stepId, submittedAnswer, sessionOpts);
244
+ // UAT-13-F2: the driver has no default-acceptance path, on purpose — the
245
+ // answer goes to initSessionAnswer verbatim, which owns the none/blank decline.
246
+ const result = (0, init_contract_cjs_1.initSessionAnswer)(cwd, stepId, answer, sessionOpts);
191
247
  if (result.ok) {
192
248
  if (stepId === 'compose_confirm') {
193
249
  wroteBinding = readBindingsContent(cwd) !== preBindings;
@@ -32,7 +32,6 @@ const worktreeSafety = require("./worktree-safety.cjs");
32
32
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
33
33
  const planningWorkspace = require("./planning-workspace.cjs");
34
34
  const secrets_cjs_1 = require("./secrets.cjs");
35
- const phase_tier_cjs_1 = require("./phase-tier.cjs");
36
35
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-scan.cjs is an export= CommonJS module
37
36
  const scanPhasePlans = require("./plan-scan.cjs");
38
37
  const state_document_cjs_1 = require("./state-document.cjs");
@@ -189,7 +188,8 @@ function getInitGitState(cwd) {
189
188
  * Quick 261007-jkl, D2 (todo "init execute-phase returns null phase_dir for an
190
189
  * archived phase") put the "is this archived match the current milestone's?"
191
190
  * compare HERE, for these four verbs only; every other consumer of the raw
192
- * locator kept the archive fall-through with no compare. The decision now lives in ONE owner, src/phase-scope.cts, which
191
+ * locator kept the archive fall-through with no compare (PBI 75323, quick
192
+ * 261009-gvf). The decision now lives in ONE owner, src/phase-scope.cts, which
193
193
  * every consumer shares: a phase archived in the CURRENT milestone keeps its
194
194
  * match, directory and artifacts; a PRIOR milestone's archived phase whose
195
195
  * number the current ROADMAP reuses is dropped (the pins in tests/init.test.cjs
@@ -222,26 +222,6 @@ function resolvePhaseForInit(cwd, phase) {
222
222
  }
223
223
  return { phaseInfo, roadmapPhase };
224
224
  }
225
- /**
226
- * The phase's resolved ceremony tier for an init payload (src/phase-tier.cts).
227
- * Init's gate booleans (`research_enabled`, `plan_checker_enabled`,
228
- * `verifier_enabled`, …) are read from this, never from raw config, so a
229
- * workflow that reads init reads the tier-resolved set.
230
- */
231
- function initPhaseTier(cwd, phase, options) {
232
- const flag = options['tier'];
233
- let cli = null;
234
- if (typeof flag === 'string') {
235
- const v = flag.trim().toLowerCase();
236
- if (!(0, phase_tier_cjs_1.isPhaseTier)(v))
237
- error(`--tier needs one of ${phase_tier_cjs_1.PHASE_TIERS.join(', ')}`);
238
- cli = v;
239
- }
240
- return (0, phase_tier_cjs_1.resolvePhaseTierForProject)(cwd, phase, cli);
241
- }
242
- function phaseTierSummary(t) {
243
- return { tier: t.tier, source: t.source, gates: t.gates, suppressed: t.suppressed };
244
- }
245
225
  function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
246
226
  if (!phase) {
247
227
  error('phase required for init execute-phase');
@@ -255,7 +235,6 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
255
235
  : null;
256
236
  const phase_req_ids = reqExtracted && reqExtracted !== 'TBD' ? reqExtracted : null;
257
237
  const wf = (config.workflow ?? {});
258
- const tier = initPhaseTier(cwd, phase, options);
259
238
  const result = {
260
239
  executor_model: resolveModelInternal(cwd, 'pp-executor'),
261
240
  verifier_model: resolveModelInternal(cwd, 'pp-verifier'),
@@ -267,12 +246,7 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
267
246
  branching_strategy: config.branching_strategy,
268
247
  phase_branch_template: config.phase_branch_template,
269
248
  milestone_branch_template: config.milestone_branch_template,
270
- verifier_enabled: tier.gates.verifier,
271
- code_review_enabled: tier.gates.code_review,
272
- security_enforcement_enabled: tier.gates.security_enforcement,
273
- nyquist_validation_enabled: tier.gates.nyquist_validation,
274
- install_verification_enabled: tier.gates.install_verification,
275
- phase_tier: phaseTierSummary(tier),
249
+ verifier_enabled: config.verifier,
276
250
  phase_found: !!phaseInfo,
277
251
  phase_dir: phaseInfo?.['directory'] || null,
278
252
  phase_number: phaseInfo?.['phase_number'] || null,
@@ -357,22 +331,15 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
357
331
  assertValidGranularityOverride(granularityOverride, error);
358
332
  const granularity = resolveGranularityInternal(cwd, 'planning', granularityOverride || undefined);
359
333
  const wf = (config.workflow ?? {});
360
- const tier = initPhaseTier(cwd, phase, options);
361
334
  const result = {
362
335
  researcher_model: resolveModelInternal(cwd, 'pp-phase-researcher'),
363
336
  planner_model: resolveModelInternal(cwd, 'pp-planner'),
364
337
  checker_model: resolveModelInternal(cwd, 'pp-plan-checker'),
365
338
  tdd_mode: options['tdd'] || Boolean(wf['tdd_mode']) || false,
366
339
  granularity,
367
- research_enabled: tier.gates.research,
368
- plan_checker_enabled: tier.gates.plan_check,
369
- plan_check_passes: tier.gates.plan_check_passes,
370
- nyquist_validation_enabled: tier.gates.nyquist_validation,
371
- pattern_mapper_enabled: tier.gates.pattern_mapper,
372
- post_planning_gaps_enabled: tier.gates.post_planning_gaps,
373
- security_enforcement_enabled: tier.gates.security_enforcement,
374
- plan_bounce_enabled: tier.gates.plan_bounce,
375
- phase_tier: phaseTierSummary(tier),
340
+ research_enabled: wf['research'],
341
+ plan_checker_enabled: config.plan_checker,
342
+ nyquist_validation_enabled: wf['nyquist_validation'],
376
343
  commit_docs: config.commit_docs,
377
344
  text_mode: config.text_mode,
378
345
  auto_advance: !!(config.auto_advance),
@@ -603,10 +570,14 @@ function cmdInitNewMilestone(cwd, raw) {
603
570
  };
604
571
  output(withProjectRoot(cwd, result), raw);
605
572
  }
606
- function cmdInitQuick(cwd, description, raw) {
607
- const config = loadConfig(cwd);
608
- const now = new Date();
609
- const slug = description ? generateSlugInternal(description)?.substring(0, 40) : null;
573
+ /**
574
+ * The quick-task id for a moment: `yymmdd` + `-` + the two-second block of the
575
+ * local day in base 36, padded to three (`261011-8yt`). A pure function of
576
+ * `now`, extracted verbatim from `cmdInitQuick` (16-05, D-29) so the plan-id
577
+ * owner (`SCAFFOLD_PLAN_ID_RE`, src/vault-standards.cts) is tested over ids the
578
+ * engine itself builds, all-digit and lettered, rather than over hand-typed ones.
579
+ */
580
+ function buildQuickId(now) {
610
581
  const yy = String(now.getFullYear()).slice(-2);
611
582
  const mm = String(now.getMonth() + 1).padStart(2, '0');
612
583
  const dd = String(now.getDate()).padStart(2, '0');
@@ -614,7 +585,13 @@ function cmdInitQuick(cwd, description, raw) {
614
585
  const secondsSinceMidnight = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds();
615
586
  const timeBlocks = Math.floor(secondsSinceMidnight / 2);
616
587
  const timeEncoded = timeBlocks.toString(36).padStart(3, '0');
617
- const quickId = dateStr + '-' + timeEncoded;
588
+ return dateStr + '-' + timeEncoded;
589
+ }
590
+ function cmdInitQuick(cwd, description, raw) {
591
+ const config = loadConfig(cwd);
592
+ const now = new Date();
593
+ const slug = description ? generateSlugInternal(description)?.substring(0, 40) : null;
594
+ const quickId = buildQuickId(now);
618
595
  const branchSlug = slug || 'quick';
619
596
  const quickBranchName = config.quick_branch_template
620
597
  ? config.quick_branch_template
@@ -676,22 +653,16 @@ function cmdInitResume(cwd, raw) {
676
653
  };
677
654
  output(withProjectRoot(cwd, result), raw);
678
655
  }
679
- function cmdInitVerifyWork(cwd, phase, raw, options = {}) {
656
+ function cmdInitVerifyWork(cwd, phase, raw) {
680
657
  if (!phase) {
681
658
  error('phase required for init verify-work');
682
659
  }
683
660
  const config = loadConfig(cwd);
684
661
  const { phaseInfo } = resolvePhaseForInit(cwd, phase);
685
- const tier = initPhaseTier(cwd, phase, options);
686
662
  const result = {
687
663
  planner_model: resolveModelInternal(cwd, 'pp-planner'),
688
664
  checker_model: resolveModelInternal(cwd, 'pp-plan-checker'),
689
665
  commit_docs: config.commit_docs,
690
- plan_checker_enabled: tier.gates.plan_check,
691
- plan_check_passes: tier.gates.plan_check_passes,
692
- security_enforcement_enabled: tier.gates.security_enforcement,
693
- nyquist_validation_enabled: tier.gates.nyquist_validation,
694
- phase_tier: phaseTierSummary(tier),
695
666
  phase_found: !!phaseInfo,
696
667
  phase_dir: phaseInfo?.['directory'] || null,
697
668
  phase_number: phaseInfo?.['phase_number'] || null,
@@ -1882,6 +1853,8 @@ module.exports = {
1882
1853
  cmdInitNewProject,
1883
1854
  cmdInitNewMilestone,
1884
1855
  cmdInitQuick,
1856
+ // 16-05 (D-29): the pure quick-id builder, so the plan-id owner is tested over real ids.
1857
+ buildQuickId,
1885
1858
  cmdInitIngestDocs,
1886
1859
  cmdInitResume,
1887
1860
  cmdInitVerifyWork,
@@ -27,7 +27,7 @@ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs")
27
27
  */
28
28
  const PROFILES = Object.freeze({
29
29
  core: Object.freeze([
30
- // init is the front door every installer/hook/no-project message
30
+ // PBI 75232: init is the front door every installer/hook/no-project message
31
31
  // names, so it ships in every profile — a minimal install must not print a
32
32
  // start command it did not install.
33
33
  'init',
@@ -187,6 +187,12 @@ const ERROR_REASON = Object.freeze({
187
187
  VAULT_PROMOTE_TARGET_REFUSED: 'vault_promote_target_refused',
188
188
  VAULT_PROMOTE_CONCURRENT_EDIT: 'vault_promote_concurrent_edit',
189
189
  VAULT_PROMOTE_TRANSPORT_FAILED: 'vault_promote_transport_failed',
190
+ // vault.mode (Vault Assurance 13-03, D-08/D-14): an unrecognised mode value,
191
+ // and the standards-mode promote refusal
192
+ VAULT_MODE_INVALID: 'vault_mode_invalid',
193
+ // 13-REVIEW WR-01: the mode resolver failed; refused like an invalid value
194
+ VAULT_MODE_UNRESOLVED: 'vault_mode_unresolved',
195
+ VAULT_PROMOTE_NOT_APPLICABLE: 'vault_promote_not_applicable',
190
196
  // discovery rollup (discovery-v2 Phase 1: D-07/D-08 gate)
191
197
  DISCOVERY_ROLLUP_PARSE_FAILED: 'discovery_rollup_parse_failed',
192
198
  DISCOVERY_ROLLUP_MISMATCH: 'discovery_rollup_mismatch',
@@ -201,6 +207,8 @@ const ERROR_REASON = Object.freeze({
201
207
  DISCOVERY_ELEMENT_SWEEP_FAILED: 'discovery_element_sweep_failed',
202
208
  // discovery scope re-baselining (discovery-v2 Phase 5: D-72/D-73 gate)
203
209
  DISCOVERY_REBASELINE_CHECK_FAILED: 'discovery_rebaseline_check_failed',
210
+ // discovery vault context record (PBI 75336)
211
+ DISCOVERY_VAULT_CHECK_FAILED: 'discovery_vault_check_failed',
204
212
  // generic
205
213
  USAGE: 'usage',
206
214
  UNKNOWN: 'unknown',
@@ -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;
@@ -396,20 +476,39 @@ function renderLoopHooks(resolved) {
396
476
  * pre-cutover keys like `workflow.ui_phase` that live in config.json but are not
397
477
  * yet exposed through loadConfig's whitelist.
398
478
  *
399
- * --active-cap <capId>: when present, resolves hooks for <point> exactly as the
400
- * normal path does, then prints exactly `true` (if any resolved activeHook has
401
- * 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.
402
488
  * No JSON envelope is emitted — output is clean for shell $(…) capture.
403
489
  * Missing <capId> value → coreError + non-zero exit.
404
490
  * Unknown/inactive capId → `false` (not an error).
405
491
  *
406
- * --phase <N> [--tier light|standard|heavy] (pack 9): resolve the phase's
407
- * ceremony tier (src/phase-tier.cts) and overlay its gate booleans
408
- * (`workflow.research`, `workflow.pattern_mapper`, …) on the config snapshot
409
- * BEFORE capability state and hook activation read it, so every `when` /
410
- * activation key of those gates answers the tier-resolved value. Without
411
- * `--phase` the project's own booleans are read as before (an explicitly
412
- * invoked gate command such as /pp-secure-phase is not governed by a tier).
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.
499
+ *
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.
413
512
  */
414
513
  function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
415
514
  if (!point) {
@@ -422,18 +521,10 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
422
521
  coreError('--active-cap requires a <capId> value (e.g. --active-cap tdd)');
423
522
  return;
424
523
  }
425
- // Fail closed on a config that exists but cannot be read. Every hook's `when`
426
- // and activation key is read from it, and both readers below swallow a parse
427
- // failure into the defaults, so rendering anyway drops exactly the hooks the
428
- // project switched on — a required client vault's fetch and blocking gate
429
- // among them — and exits 0. A MISSING config is not refused: that is a fresh
430
- // project, and the defaults are its real settings. stdout stays empty so a
431
- // host capturing `$(…)` never reads a refusal as "no hooks".
432
- const unreadable = unreadableConfigFiles(cwd);
433
- if (unreadable.length > 0) {
434
- coreError(`loop render-hooks ${point} refused: the project config cannot be read, so no hook can be resolved ` +
435
- `(rendering anyway would silently drop every hook the config switches on, a required client vault among them). ` +
436
- `${describeUnreadableConfig(unreadable)}. Repair or remove the file, then re-run. Stop this workflow here.`, ERROR_REASON.CONFIG_PARSE_FAILED);
524
+ // --into <role> mode: emit matching contribution fragment text only
525
+ const intoRole = typeof options['into'] === 'string' ? options['into'] : undefined;
526
+ if (activeCapId === undefined && intoRole !== undefined && intoRole === '') {
527
+ coreError('--into requires a <role> value (e.g. --into executor)');
437
528
  return;
438
529
  }
439
530
  const runtimeConfigDir = typeof options['configDir'] === 'string'
@@ -453,17 +544,6 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
453
544
  catch {
454
545
  config = {};
455
546
  }
456
- const tierPhase = typeof options['phase'] === 'string' && options['phase'] !== '' ? options['phase'] : null;
457
- const tierCli = typeof options['tier'] === 'string' && options['tier'] !== '' ? options['tier'] : null;
458
- let phaseTier = null;
459
- if (tierPhase !== null || tierCli !== null) {
460
- if (tierCli !== null && !['light', 'standard', 'heavy'].includes(tierCli)) {
461
- coreError('--tier needs one of light, standard, heavy');
462
- return;
463
- }
464
- phaseTier = (0, phase_tier_cjs_1.resolvePhaseTierForProject)(cwd, tierPhase, tierCli);
465
- config = (0, phase_tier_cjs_1.overlayTierGates)(config, phaseTier);
466
- }
467
547
  const state = resolveCapabilityRuntimeState(cwd, runtimeConfigDir, config);
468
548
  // Registry is the static generated module — same object capability-state uses internally.
469
549
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -472,9 +552,27 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
472
552
  for (const cap of state.capabilities || []) {
473
553
  capabilityStatesById.set(cap.id, cap);
474
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);
475
568
  let resolved;
476
569
  try {
477
- resolved = resolveLoopHooks({ point, registry, config, cwd, capabilityStatesById });
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
+ });
478
576
  }
479
577
  catch (err) {
480
578
  const msg = (err instanceof Error) ? err.message : String(err);
@@ -483,19 +581,37 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
483
581
  }
484
582
  // --active-cap mode: print exactly 'true' or 'false' with no envelope
485
583
  if (activeCapId !== undefined) {
486
- 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;
487
592
  process.stdout.write(isActive ? 'true\n' : 'false\n');
488
593
  return;
489
594
  }
595
+ // --into mode: fragment text of the active contributions targeting <role>
596
+ if (intoRole !== undefined) {
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
+ }
605
+ if (texts.length > 0)
606
+ process.stdout.write(texts.join('\n\n') + '\n');
607
+ return;
608
+ }
490
609
  const rendered = renderLoopHooks(resolved);
491
610
  const envelope = {
492
611
  point: resolved.point,
493
612
  activeHooks: resolved.activeHooks,
494
613
  rendered,
495
614
  };
496
- if (phaseTier) {
497
- envelope.phase_tier = { phase: tierPhase, tier: phaseTier.tier, source: phaseTier.source, suppressed: phaseTier.suppressed };
498
- }
499
615
  if (state.warnings && state.warnings.length > 0) {
500
616
  envelope.warnings = state.warnings;
501
617
  }
@@ -518,6 +634,12 @@ module.exports = {
518
634
  // Re-exported for identity parity guard (FIX 2: resolveConfigValues in this module
519
635
  // calls resolveConfigKey; exporting it here makes the single-owner contract testable).
520
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,
521
643
  CANONICAL_POINTS_FALLBACK,
522
644
  CANONICAL_POINTS,
523
645
  };