@pareto4/pp-core 1.13.0 → 1.18.0

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 (204) hide show
  1. package/.claude-plugin/plugin.json +3 -3
  2. package/README.md +31 -12
  3. package/agents/pp-code-fixer.md +0 -11
  4. package/agents/pp-code-reviewer.md +1 -77
  5. package/agents/pp-debug-session-manager.md +9 -6
  6. package/agents/pp-executor.md +12 -29
  7. package/agents/pp-plan-checker.md +4 -4
  8. package/agents/pp-planner.md +34 -20
  9. package/agents/pp-vault-reviewer.md +100 -23
  10. package/agents/pp-verifier.md +1 -3
  11. package/bin/install.js +10 -3
  12. package/commands/pp-execute-phase.md +1 -3
  13. package/commands/pp-ns-manage.md +1 -1
  14. package/commands/pp-ns-review.md +1 -1
  15. package/commands/pp-plan-phase.md +1 -2
  16. package/commands/pp-vault-promote.md +7 -8
  17. package/commands/pp-vault-review.md +6 -5
  18. package/commands/pp-verify-work.md +1 -2
  19. package/gemini-extension.json +1 -1
  20. package/hooks/dist/pp-check-update.js +0 -0
  21. package/hooks/dist/pp-cursor-session-start.js +1 -1
  22. package/hooks/dist/pp-statusline.js +1 -1
  23. package/hooks/dist/pp-update-banner.js +0 -0
  24. package/hooks/pp-check-update.js +0 -0
  25. package/hooks/pp-cursor-session-start.js +1 -1
  26. package/hooks/pp-statusline.js +1 -1
  27. package/hooks/pp-update-banner.js +0 -0
  28. package/package.json +10 -18
  29. package/pareto-skills/_shared/create-project-board.sh +0 -0
  30. package/pareto-skills/pp-project-setup/SKILL.md +1 -1
  31. package/pp-core/bin/check-latest-version.cjs +0 -0
  32. package/pp-core/bin/detect-build-commands.sh +2 -2
  33. package/pp-core/bin/lib/capability-activation.cjs +83 -9
  34. package/pp-core/bin/lib/capability-registry.cjs +201 -229
  35. package/pp-core/bin/lib/capability-state.cjs +93 -1
  36. package/pp-core/bin/lib/capability-writer.cjs +3 -3
  37. package/pp-core/bin/lib/check-command-router.cjs +1906 -1514
  38. package/pp-core/bin/lib/clusters.cjs +3 -0
  39. package/pp-core/bin/lib/command-aliases.cjs +0 -35
  40. package/pp-core/bin/lib/commands.cjs +9 -4
  41. package/pp-core/bin/lib/compose-bindings.cjs +251 -88
  42. package/pp-core/bin/lib/config-loader.cjs +7 -103
  43. package/pp-core/bin/lib/config-schema.cjs +1 -1
  44. package/pp-core/bin/lib/config.cjs +151 -62
  45. package/pp-core/bin/lib/decisions.cjs +4 -4
  46. package/pp-core/bin/lib/discovery-vault-check.cjs +246 -0
  47. package/pp-core/bin/lib/frontmatter.cjs +1 -38
  48. package/pp-core/bin/lib/git-base-branch.cjs +11 -12
  49. package/pp-core/bin/lib/git-provider.cjs +9 -10
  50. package/pp-core/bin/lib/init-command-router.cjs +5 -8
  51. package/pp-core/bin/lib/init-contract.cjs +620 -738
  52. package/pp-core/bin/lib/init-interactive.cjs +62 -4
  53. package/pp-core/bin/lib/init.cjs +24 -51
  54. package/pp-core/bin/lib/install-profiles.cjs +1 -1
  55. package/pp-core/bin/lib/io.cjs +4 -2
  56. package/pp-core/bin/lib/loop-resolver.cjs +165 -129
  57. package/pp-core/bin/lib/org-profile.cjs +402 -161
  58. package/pp-core/bin/lib/package-identity.cjs +3 -3
  59. package/pp-core/bin/lib/phase-command-router.cjs +1 -29
  60. package/pp-core/bin/lib/phase-scope.cjs +1 -1
  61. package/pp-core/bin/lib/phase.cjs +11 -253
  62. package/pp-core/bin/lib/platform-bindings.cjs +40 -19
  63. package/pp-core/bin/lib/profile-output.cjs +1 -1
  64. package/pp-core/bin/lib/shell-command-projection.cjs +104 -1
  65. package/pp-core/bin/lib/state.cjs +11 -85
  66. package/pp-core/bin/lib/template.cjs +1 -1
  67. package/pp-core/bin/lib/vault-command-router.cjs +1682 -477
  68. package/pp-core/bin/lib/vault-coordinates.cjs +16 -7
  69. package/pp-core/bin/lib/vault-index.cjs +656 -0
  70. package/pp-core/bin/lib/vault-seal.cjs +1851 -0
  71. package/pp-core/bin/lib/vault-standards-promote.cjs +577 -0
  72. package/pp-core/bin/lib/vault-standards-review.cjs +293 -0
  73. package/pp-core/bin/lib/vault-standards.cjs +1504 -0
  74. package/pp-core/bin/lib/vault-trace.cjs +155 -2778
  75. package/pp-core/bin/lib/vault-update.cjs +10 -59
  76. package/pp-core/bin/lib/vault.cjs +2570 -950
  77. package/pp-core/bin/lib/verify.cjs +0 -11
  78. package/pp-core/bin/portable-timeout.sh +1 -1
  79. package/pp-core/bin/pp-tools.cjs +157 -54
  80. package/pp-core/bin/shared/config-defaults.manifest.json +0 -7
  81. package/pp-core/bin/shared/config-schema.manifest.json +0 -3
  82. package/pp-core/bin/shared/model-catalog.json +2 -2
  83. package/pp-core/bin/verify-reapply-patches.cjs +0 -0
  84. package/pp-core/org-profile.json +26 -0
  85. package/pp-core/references/autonomous-best-effort.md +8 -7
  86. package/pp-core/references/loop-hook-dispatch.md +0 -25
  87. package/pp-core/references/planner-gap-closure.md +0 -1
  88. package/pp-core/references/planner-tdd.md +0 -1
  89. package/pp-core/references/planner-vault-standards.md +311 -0
  90. package/pp-core/references/planning-config.md +0 -8
  91. package/pp-core/references/tdd.md +0 -1
  92. package/pp-core/templates/README.md +1 -0
  93. package/pp-core/templates/VAULT-REVIEW.md +56 -0
  94. package/pp-core/templates/phase-prompt.md +2 -6
  95. package/pp-core/workflows/_runtime-launcher.snippet.sh +0 -0
  96. package/pp-core/workflows/add-tests.md +55 -4
  97. package/pp-core/workflows/audit-fix.md +63 -2
  98. package/pp-core/workflows/audit-milestone.md +1 -1
  99. package/pp-core/workflows/autonomous.md +85 -11
  100. package/pp-core/workflows/check-todos.md +13 -2
  101. package/pp-core/workflows/code-review-fix.md +59 -3
  102. package/pp-core/workflows/code-review.md +10 -57
  103. package/pp-core/workflows/complete-milestone.md +1 -1
  104. package/pp-core/workflows/debug.md +54 -4
  105. package/pp-core/workflows/discovery-sprint/prefill.md +19 -7
  106. package/pp-core/workflows/discovery-sprint/prototype.md +46 -49
  107. package/pp-core/workflows/discovery-sprint/session-13-gates.md +2 -0
  108. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +1 -3
  109. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +1 -3
  110. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +1 -3
  111. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +20 -24
  112. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +1 -3
  113. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +1 -3
  114. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +1 -3
  115. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +1 -3
  116. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +1 -3
  117. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +1 -6
  118. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +1 -3
  119. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +1 -3
  120. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -3
  121. package/pp-core/workflows/discovery-sprint/templates/context-template.md +1 -1
  122. package/pp-core/workflows/discovery-sprint/vault-context.md +207 -0
  123. package/pp-core/workflows/discovery-sprint.md +7 -3
  124. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +4 -4
  125. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +43 -1
  126. package/pp-core/workflows/discovery-to-build.md +10 -2
  127. package/pp-core/workflows/discuss-phase.md +3 -10
  128. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +327 -0
  129. package/pp-core/workflows/execute-phase/steps/client-context-gate.md +3 -2
  130. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -1
  131. package/pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md +78 -0
  132. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
  133. package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +6 -8
  134. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +115 -14
  135. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +219 -20
  136. package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +147 -0
  137. package/pp-core/workflows/execute-phase.md +152 -128
  138. package/pp-core/workflows/execute-plan.md +54 -6
  139. package/pp-core/workflows/fast.md +53 -4
  140. package/pp-core/workflows/help/modes/full.md +3 -3
  141. package/pp-core/workflows/init/steps/client-context-capture.md +101 -0
  142. package/pp-core/workflows/init/steps/gitlab-provisioning.md +130 -0
  143. package/pp-core/workflows/init/steps/project-md-deferral-narration.md +66 -0
  144. package/pp-core/workflows/init/steps/reachability-outcome.md +70 -0
  145. package/pp-core/workflows/init.md +232 -272
  146. package/pp-core/workflows/new-project.md +31 -23
  147. package/pp-core/workflows/plan-phase.md +98 -45
  148. package/pp-core/workflows/quick.md +117 -3
  149. package/pp-core/workflows/secure-phase.md +1 -1
  150. package/pp-core/workflows/ship.md +57 -28
  151. package/pp-core/workflows/undo.md +1 -1
  152. package/pp-core/workflows/update.md +3 -3
  153. package/pp-core/workflows/validate-phase.md +58 -3
  154. package/pp-core/workflows/vault-review.md +161 -23
  155. package/pp-core/workflows/vault-write-back.md +73 -110
  156. package/pp-core/workflows/verify-work.md +12 -14
  157. package/scripts/changeset/cli.cjs +6 -5
  158. package/scripts/changeset/lint.cjs +0 -0
  159. package/scripts/changeset/new.cjs +33 -50
  160. package/scripts/dev-env.cjs +25 -85
  161. package/scripts/gen-capability-registry.cjs +120 -13
  162. package/scripts/lib/allowlist-ratchet.cjs +1 -103
  163. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -2
  164. package/scripts/lint-docs-required.cjs +0 -0
  165. package/scripts/lint-pr-check-project-dir.cjs +1 -3
  166. package/scripts/lint-test-file-count.allowlist.json +3 -19
  167. package/scripts/lint-test-file-count.cjs +5 -6
  168. package/scripts/prompt-injection-scan.sh +19 -17
  169. package/scripts/release-channel.cjs +0 -0
  170. package/scripts/sync-manifest-versions.cjs +5 -4
  171. package/scripts/sync-rulesets.sh +1 -1
  172. package/scripts/update-size-baseline.cjs +30 -274
  173. package/scripts/verify-opengsd-sweep.cjs +3 -5
  174. package/scripts/workflow-size.cjs +9 -61
  175. package/pp-core/bin/lib/dispatch-plan.cjs +0 -483
  176. package/pp-core/bin/lib/install-run.cjs +0 -536
  177. package/pp-core/bin/lib/loop-gates.cjs +0 -213
  178. package/pp-core/bin/lib/phase-manifest.cjs +0 -527
  179. package/pp-core/bin/lib/phase-tier.cjs +0 -480
  180. package/pp-core/bin/lib/plan-graph.cjs +0 -368
  181. package/pp-core/bin/lib/practices-review-check.cjs +0 -938
  182. package/pp-core/bin/lib/vault-audit.cjs +0 -2070
  183. package/pp-core/bin/lib/vault-practices.cjs +0 -1990
  184. package/pp-core/bin/lib/vault-review.cjs +0 -2359
  185. package/pp-core/bin/lib/vault-write-back.cjs +0 -877
  186. package/pp-core/references/executor-vault-trace.md +0 -47
  187. package/pp-core/references/planner-plan-graph.md +0 -66
  188. package/pp-core/references/planner-vault-practices.md +0 -112
  189. package/pp-core/templates/practices/README.md +0 -251
  190. package/pp-core/templates/practices/error-handling.md +0 -18
  191. package/pp-core/templates/practices/minimal-comments.md +0 -14
  192. package/pp-core/templates/practices/naming-convention.md +0 -18
  193. package/pp-core/templates/practices/practice-template.md +0 -23
  194. package/pp-core/workflows/discovery-sprint/vault-fetch.md +0 -37
  195. package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +0 -45
  196. package/pp-core/workflows/execute-phase/steps/install-run.md +0 -29
  197. package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +0 -23
  198. package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +0 -73
  199. package/pp-core/workflows/execute-phase/steps/vault-door-gate.md +0 -19
  200. package/pp-core/workflows/plan-phase/steps/phase-tier.md +0 -43
  201. package/scripts/lib/size-budget.cjs +0 -311
  202. package/scripts/lint-neutrality-names.cjs +0 -694
  203. package/scripts/lint-org-coordinates.allowlist.json +0 -35
  204. 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, 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, 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, recordedArtifacts, platformBindingsPresent, } = input;
109
107
  // Validate point
110
108
  const canonicalPoints = _getCanonicalPoints(registry);
111
109
  if (!canonicalPoints.includes(point)) {
@@ -150,6 +148,59 @@ function resolveLoopHooks(input) {
150
148
  // `=== true` is the correct fail-closed check (not `!== false`).
151
149
  return state.active === true;
152
150
  }
151
+ // Phase 16 D-19: the state the seal rule reads for a gate's capability. With
152
+ // no state map at all, every capability is treated as active, exactly as
153
+ // `isCapabilityActive` does: normal gates keep today's behaviour and a seal
154
+ // gate drops (it never renders while its capability is active). With a map,
155
+ // a missing entry is `undefined`, which the rule drops (fail closed).
156
+ function gateStateOf(capId) {
157
+ if (!capabilityStatesById)
158
+ return { enabled: true, active: true };
159
+ return capabilityStatesById instanceof Map
160
+ ? capabilityStatesById.get(capId)
161
+ : capabilityStatesById[capId];
162
+ }
163
+ // Phase 16 D-19 / T-16-33: the injected predicate is asked at most once per
164
+ // resolve, however many seal gates the point declares. An absent thunk is
165
+ // the inert answer (no seal renders).
166
+ let recordedArtifactsMemo;
167
+ function recordedArtifactsOnce() {
168
+ if (recordedArtifactsMemo === undefined) {
169
+ recordedArtifactsMemo = resolveInjectedRecordedArtifacts(recordedArtifacts);
170
+ }
171
+ return recordedArtifactsMemo;
172
+ }
173
+ // Quick 261015-ect (QD-13): the bindings measurement, asked at most once per
174
+ // resolve and only for a capability that declares it needs the file. A
175
+ // missing thunk is `null` (unmeasured), which drops nothing.
176
+ let bindingsPresentMemo;
177
+ function bindingsPresentOnce() {
178
+ if (bindingsPresentMemo === undefined) {
179
+ bindingsPresentMemo = resolveInjectedPlatformBindingsPresent(platformBindingsPresent);
180
+ }
181
+ return bindingsPresentMemo;
182
+ }
183
+ function requiresBindings(capId) {
184
+ // Prototype-pollution guard (inline literal, CodeQL barrier), as the
185
+ // registry generator guards capability ids.
186
+ if (capId === '' || capId === '__proto__' || capId === 'constructor' || capId === 'prototype')
187
+ return false;
188
+ const caps = registry['capabilities'];
189
+ if (!caps || typeof caps !== 'object' || Array.isArray(caps))
190
+ return false;
191
+ if (!Object.prototype.hasOwnProperty.call(caps, capId))
192
+ return false;
193
+ const cap = caps[capId];
194
+ if (!cap || typeof cap !== 'object')
195
+ return false;
196
+ return cap['requiresPlatformBindings'] === true;
197
+ }
198
+ // A capability that requires bindings renders nothing where the file is
199
+ // MEASURED absent: the project is not governed by it (its own description says
200
+ // it is inert unless the bindings exist).
201
+ function droppedForBindings(capId) {
202
+ return requiresBindings(capId) && bindingsPresentOnce() === false;
203
+ }
153
204
  // Helper: safe string array
154
205
  function toStringArray(v) {
155
206
  if (!Array.isArray(v))
@@ -201,6 +252,8 @@ function resolveLoopHooks(input) {
201
252
  const capId = typeof hook['capId'] === 'string' ? hook['capId'] : '';
202
253
  if (!isCapabilityActive(capId))
203
254
  continue;
255
+ if (droppedForBindings(capId))
256
+ continue;
204
257
  if (!isActive(hook))
205
258
  continue;
206
259
  const ref = (typeof hook['ref'] === 'object' && hook['ref'] !== null)
@@ -235,6 +288,8 @@ function resolveLoopHooks(input) {
235
288
  const capId = typeof hook['capId'] === 'string' ? hook['capId'] : '';
236
289
  if (!isCapabilityActive(capId))
237
290
  continue;
291
+ if (droppedForBindings(capId))
292
+ continue;
238
293
  if (!isActive(hook))
239
294
  continue;
240
295
  const into = typeof hook['into'] === 'string' ? hook['into'] : undefined;
@@ -268,9 +323,18 @@ function resolveLoopHooks(input) {
268
323
  if (!hook || typeof hook !== 'object')
269
324
  continue;
270
325
  const capId = typeof hook['capId'] === 'string' ? hook['capId'] : '';
271
- if (!isCapabilityActive(capId))
326
+ // Quick 261015-ect (QD-13): gates too, seal gates included. A project with
327
+ // no bindings holds no engine-written vault records (D-37/D-42), and every
328
+ // vault gate already answers inert there, so nothing is left ungated.
329
+ if (droppedForBindings(capId))
272
330
  continue;
273
- if (!isActive(hook))
331
+ const verdict = sealRenderVerdict(hook[SEAL_FIELD], gateStateOf(capId), recordedArtifactsOnce);
332
+ if (verdict === 'drop')
333
+ continue;
334
+ // 'normal': today's path, `when` evaluated. 'seal': the `when` IS the
335
+ // switch that is off (D-18), so it is not read; the seal rule already
336
+ // required an enabled, config-inactive capability and recorded artifacts.
337
+ if (verdict === 'normal' && !isActive(hook))
274
338
  continue;
275
339
  const when = typeof hook['when'] === 'string' ? hook['when'] : undefined;
276
340
  const check = hook['check'] !== undefined ? hook['check'] : undefined;
@@ -382,80 +446,6 @@ function renderLoopHooks(resolved) {
382
446
  return lines.join('\n');
383
447
  }
384
448
  // ─── 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
449
  /**
460
450
  * Command entry point: load registry + config, resolve + render, emit envelope.
461
451
  *
@@ -470,31 +460,37 @@ function contributionsInto(resolved, role) {
470
460
  * pre-cutover keys like `workflow.ui_phase` that live in config.json but are not
471
461
  * yet exposed through loadConfig's whitelist.
472
462
  *
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.
463
+ * --active-cap <capId>: when present, resolves hooks for <point> as the normal
464
+ * path does EXCEPT for one input — the bindings measurement (quick 261015-ect,
465
+ * QD-13) is not applied — then prints exactly `true` (if any resolved
466
+ * activeHook has capId === <capId>) or `false` followed by a single newline,
467
+ * and exits 0. So on a project with no `.platform-bindings.json` it still
468
+ * answers the config switch alone: /pp-init Step 2.6's unbound arm ("a
469
+ * resolving profile with no bindings halts loudly") is reachable only because
470
+ * this read ignores bindings (16-16 D-81), and the D-28 parity test states
471
+ * parity on bindings-present shapes only.
480
472
  * No JSON envelope is emitted — output is clean for shell $(…) capture.
481
473
  * Missing <capId> value → coreError + non-zero exit.
482
474
  * Unknown/inactive capId → `false` (not an error).
483
475
  *
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.
476
+ * --into <role> (Phase 15 D-12/D-30): resolves exactly as the normal path does,
477
+ * then prints the `fragment.inline` text of each active contribution whose
478
+ * `into` equals <role>, in resolved order, separated by one blank line, with a
479
+ * trailing newline when anything printed. Nothing at all (empty stdout, exit
480
+ * 0) when no contribution matches. No JSON envelope; `--raw` is ignored. This
481
+ * is the text execute-phase interpolates verbatim into a spawn prompt (15-09).
482
+ * `--active-cap` takes precedence: given both, `--into` is ignored.
483
+
484
+ *
485
+ * options.resolveRecordedArtifacts: an injected `() => boolean` (pp-tools builds
486
+ * it on the seal leaf's hasRecordedVaultArtifacts); only literal `true` lets a
487
+ * seal gate render (Phase 16 D-19). `--active-cap <cap>` also requires the
488
+ * capability's own state to be active (D-27).
490
489
  *
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).
490
+ * options.resolvePlatformBindingsPresent: an injected `() => boolean` (pp-tools
491
+ * builds it on the vault leaf's hasPlatformBindings); only literal `false`
492
+ * drops a `requiresPlatformBindings` capability's hooks (QD-13). `--into` and
493
+ * the envelope read the filtered set; `--active-cap` reads the unfiltered one.
498
494
  */
499
495
  function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
500
496
  if (!point) {
@@ -509,40 +505,82 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
509
505
  }
510
506
  // --into <role> mode: emit matching contribution fragment text only
511
507
  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);
508
+ if (activeCapId === undefined && intoRole !== undefined && intoRole === '') {
509
+ coreError('--into requires a <role> value (e.g. --into executor)');
514
510
  return;
515
511
  }
516
512
  const runtimeConfigDir = typeof options['configDir'] === 'string'
517
513
  ? options['configDir']
518
514
  : 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);
515
+ // Load the config snapshot ONCE and share it with both the capability-state
516
+ // resolver (via configOverride) and loop-hook resolution, so federated keys
517
+ // present in loadConfig resolve identically for `active` and for hook when/
518
+ // configValues — eliminating the previous double loadConfig() call. Note: keys
519
+ // absent from loadConfig still fall through to raw .planning/config.json reads
520
+ // (precedence levels 2-3) in each pass; that residual re-read window is
521
+ // pre-existing (unchanged by this consolidation), not introduced here.
522
+ let config;
523
+ try {
524
+ config = loadConfig(cwd);
525
+ }
526
+ catch {
527
+ config = {};
530
528
  }
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);
529
+ const state = resolveCapabilityRuntimeState(cwd, runtimeConfigDir, config);
530
+ // Registry is the static generated module — same object capability-state uses internally.
531
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
532
+ const registry = require('./capability-registry.cjs');
533
+ const capabilityStatesById = new Map();
534
+ for (const cap of state.capabilities || []) {
535
+ capabilityStatesById.set(cap.id, cap);
536
+ }
537
+ // Phase 16 D-19: the recorded-artifacts predicate arrives through an injected
538
+ // resolver (pp-tools
539
+ // passes one built on the seal leaf's hasRecordedVaultArtifacts). The resolver
540
+ // memoises it and asks it only for an enabled, config-inactive seal gate.
541
+ const resolveRecordedArtifacts = options['resolveRecordedArtifacts'];
542
+ const recordedArtifacts = () => resolveInjectedRecordedArtifacts(resolveRecordedArtifacts);
543
+ // Quick 261015-ect (QD-13): the bindings measurement arrives the same way
544
+ // (pp-tools passes one built on the vault leaf's hasPlatformBindings).
545
+ const resolvePlatformBindingsPresent = options['resolvePlatformBindingsPresent'];
546
+ const platformBindingsPresent = () => resolveInjectedPlatformBindingsPresent(resolvePlatformBindingsPresent);
547
+ let resolved;
548
+ try {
549
+ // --active-cap reads the UNFILTERED resolution (16-16 D-81, see above):
550
+ // without the bindings thunk, so it keeps answering the config switch.
551
+ resolved = resolveLoopHooks({
552
+ point, registry, config, cwd, capabilityStatesById, recordedArtifacts,
553
+ ...(activeCapId === undefined ? { platformBindingsPresent } : {}),
554
+ });
555
+ }
556
+ catch (err) {
557
+ const msg = (err instanceof Error) ? err.message : String(err);
558
+ coreError(msg);
534
559
  return;
535
560
  }
536
- const resolved = outcome.resolved;
537
561
  // --active-cap mode: print exactly 'true' or 'false' with no envelope
538
562
  if (activeCapId !== undefined) {
539
- const isActive = resolved.activeHooks.some((h) => h.capId === activeCapId);
563
+ // Phase 16 D-27 (research C-3): a hook rendering is no longer enough — the
564
+ // capability must itself be active. A seal gate renders for a config-
565
+ // inactive vault, and the doors read `--active-cap` as their
566
+ // switch, so a seal must never make an inactive capability answer `true`.
567
+ // Equivalent to the old answer on every input without a seal: a normal
568
+ // hook renders only for an active capability.
569
+ const isActive = resolved.activeHooks.some((h) => h.capId === activeCapId)
570
+ && capabilityStatesById.get(activeCapId)?.active === true;
540
571
  process.stdout.write(isActive ? 'true\n' : 'false\n');
541
572
  return;
542
573
  }
543
574
  // --into mode: fragment text of the active contributions targeting <role>
544
575
  if (intoRole !== undefined) {
545
- const texts = contributionsInto(resolved, intoRole).map((c) => c.fragment);
576
+ const texts = [];
577
+ for (const h of resolved.activeHooks) {
578
+ if (h.kind !== 'contribution' || h.into !== intoRole)
579
+ continue;
580
+ if (typeof h.fragment?.inline !== 'string')
581
+ continue;
582
+ texts.push(h.fragment.inline);
583
+ }
546
584
  if (texts.length > 0)
547
585
  process.stdout.write(texts.join('\n\n') + '\n');
548
586
  return;
@@ -553,11 +591,8 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
553
591
  activeHooks: resolved.activeHooks,
554
592
  rendered,
555
593
  };
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;
594
+ if (state.warnings && state.warnings.length > 0) {
595
+ envelope.warnings = state.warnings;
561
596
  }
562
597
  // No rawValue third argument, so --raw emits the JSON envelope rather than
563
598
  // plain text. This is deliberate and load-bearing: 23 workflow call sites do
@@ -571,8 +606,6 @@ module.exports = {
571
606
  resolveLoopHooks,
572
607
  renderLoopHooks,
573
608
  cmdLoopRenderHooks,
574
- resolveProjectLoopHooks,
575
- contributionsInto,
576
609
  // Exported for tests
577
610
  _getNestedConfigValue,
578
611
  _resolveActivationValue,
@@ -580,6 +613,9 @@ module.exports = {
580
613
  // Re-exported for identity parity guard (FIX 2: resolveConfigValues in this module
581
614
  // calls resolveConfigKey; exporting it here makes the single-owner contract testable).
582
615
  resolveConfigKey,
616
+ // Re-exported for the same identity pin: the seal rule has one owner,
617
+ // capability-state.cjs (Phase 16 D-19).
618
+ sealRenderVerdict,
583
619
  CANONICAL_POINTS_FALLBACK,
584
620
  CANONICAL_POINTS,
585
621
  };