@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
@@ -4,8 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  /**
6
6
  * src/vault-audit.cts — the pure `vault audit` evaluator: the ONE engine both
7
- * surfaces call (`vault audit <phase-dir>` for an operator, `check vault.audit`
8
- * for the blocking gate at verify:post and ship:pre).
7
+ * surfaces in plan 05-06 call (Vault Assurance #71924, phase 05 plan 05;
8
+ * CHECK-03, CHECK-04).
9
9
  *
10
10
  * Responsibility. Given the practices the cached vault holds and the files a
11
11
  * phase actually changed, decide — per practice — one of exactly four verdicts
@@ -20,32 +20,31 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
20
20
  * `MATCH_BUDGET_MS` plus `MATCHED_FILES_CAP`.
21
21
  *
22
22
  * Nothing on the matching path is re-implemented here and no grammar is
23
- * restated in prose: a second matcher would be a copy free to drift. The
24
- * declarative-check rule (docs/practices-format.md) — no client-authored
25
- * byte is ever compiled into a regular expression — is why content tokens
26
- * below are tested by `String.prototype.includes` and nothing else, and
27
- * why `globMatch` runs the polynomial matcher rather than a pattern
28
- * compiler. The verdict half is mechanised, not merely stated:
29
- * `eslint-rules/practices-boundary-inputs.cjs` is wired at this file in
30
- * `eslint.config.mjs`.
31
- * - `./phase-manifest.cjs` (`readPhaseManifestEntry`) — the phase's recorded
32
- * start commit, read from the file `phase manifest start|record` writes.
33
- * - `./vault-trace.cjs` — the ledger API: SUMMARY `gitCommit` digests for the
34
- * trace-digest rung, and the `check:` census the ledgers recorded.
23
+ * restated in prose. ADR-71924 §2 refuses to restate the glob grammar
24
+ * precisely so the two cannot drift, and a second matcher would be the
25
+ * drifting copy it refuses. §3's prohibition — no client-authored byte is
26
+ * ever compiled into a regular expression — is why content tokens below are
27
+ * tested by `String.prototype.includes` and nothing else. That prohibition
28
+ * is mechanised, not merely stated: `eslint-rules/practices-boundary-inputs.cjs`
29
+ * is wired at this file in `eslint.config.mjs`.
35
30
  * - `./git-base-branch.cjs` (`resolveBaseBranchResult`) — the 5-tier ladder is
36
31
  * never re-derived; issue #1146 records what the naive form cost. The RESULT
37
32
  * form, so a REFUSED `git.base_branch` becomes a stated note rather than a
38
33
  * silent change of range (W16).
39
- * - `./shell-command-projection.cjs` (`execGit`) — the mandated, timeout-bounded
34
+ * - `./shell-command-projection.cjs` (`execGitLocal`, D-116) — the mandated, timeout-bounded
40
35
  * git seam. `execFileSync` is not used anywhere in this module.
41
36
  *
42
37
  * WHAT `require` MEANS HERE, stated because a recorded number depends on it.
43
38
  * A `require` token is satisfied when EVERY matched file contains it. This is
44
- * the per-file reading. It is chosen because it is the only reading under which a FAIL can name a SPECIFIC
39
+ * the per-file reading, and it is the reading the CHECK-05 census in
40
+ * `docs/adr/71924-declarative-check-blocks.md` states as its premise ("`require`
41
+ * is read per matched file") and measured its 19-of-55 count under. It is
42
+ * chosen because it is the only reading under which a FAIL can name a SPECIFIC
45
43
  * offending file — and a verdict that cannot name what offended it is a verdict
46
44
  * an auditor cannot act on, which is the failure D-05-03's vocabulary exists to
47
- * prevent. Changing this to "some matched file contains the token" is a
48
- * decision with a paper trail, not an implementation detail.
45
+ * prevent. Changing this to "some matched file contains the token" would move
46
+ * the recorded census; it is a decision with a paper trail, not an
47
+ * implementation detail.
49
48
  *
50
49
  * TWO KNOWN LIMITS OF THE DIFF, recorded here rather than rediscovered later.
51
50
  * Both are inherited deliberately from `cmdShipPractices`' scoping mechanism,
@@ -66,22 +65,17 @@ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs")
66
65
  const git_base_branch_cjs_1 = require("./git-base-branch.cjs");
67
66
  // eslint-disable-next-line @typescript-eslint/no-require-imports
68
67
  const practicesMod = require("./vault-practices.cjs");
69
- // The trace leaf (the ledger API), for the SUMMARY trace-digest rung of
70
- // `resolvePhaseRange` and for the recorded `check:` census. `vault-trace.cts`
71
- // imports `vault.cjs` and the practices leaf only, and neither imports this
72
- // module, so this adds no cycle.
68
+ // 260915-ik2 (UAT-6-F1): the trace leaf, for the SUMMARY trace-digest rung of
69
+ // `resolvePhaseRange`. `vault-trace.cts` imports `vault.cjs` and the practices
70
+ // leaf only, and neither imports this module, so this adds no cycle.
73
71
  // eslint-disable-next-line @typescript-eslint/no-require-imports
74
72
  const traceMod = require("./vault-trace.cjs");
75
- // The phase manifest's reader — the same module owns its writer.
73
+ // 16-03: the one `.planning/` path test. Direction: this module imports the
74
+ // seal leaf statically; the leaf requires this module only LAZILY, inside
75
+ // `collectRecordedArtifacts`, so there is no load-time cycle (D-41).
76
76
  // eslint-disable-next-line @typescript-eslint/no-require-imports
77
- const phaseManifestMod = require("./phase-manifest.cjs");
78
- // The one remote/credential redactor, applied to vault-authored text before it
79
- // reaches the gate message.
80
- // eslint-disable-next-line @typescript-eslint/no-require-imports
81
- const vaultMod = require("./vault.cjs");
82
- const { parseVaultTraceBlock, readRecordedCensus } = traceMod;
83
- const { readPhaseManifestEntry, PHASE_MANIFEST_READ_MISS, COMMIT_SHA_SHAPE } = phaseManifestMod;
84
- const { redactRemoteText } = vaultMod;
77
+ const vaultSeal = require("./vault-seal.cjs");
78
+ const { parseVaultTraceBlock } = traceMod;
85
79
  const { globMatch, isSupportedGlob, boundedToken, discoverPracticeFiles, MATCH_PAIR_CAP, MATCH_BUDGET_MS, MATCHED_FILES_CAP,
86
80
  // 05-REVIEW CR-02/CR-03: the shared fencing idiom and the errno redactor —
87
81
  // reused, never re-authored, so the two modules cannot disagree about how a
@@ -123,7 +117,12 @@ const AUDIT_VERDICT = Object.freeze({
123
117
  * verdict would let a one-character typo buy a non-blocking outcome, which is
124
118
  * the same trade `CHECK_INVALID` already refuses.
125
119
  *
126
- * EIGHT members, and all eight block.
120
+ * Quick 261015-at3 added `VAULT_STANDARDS_ONLY`: a standards vault (INDEX.md,
121
+ * no practice files) read in practices mode. A FAIL and not a NO_CHECK on the
122
+ * `PRACTICE_UNLOADABLE` argument — a practice layer the project is not reading
123
+ * has unknown check-bearing-ness, so it cannot buy a non-blocking outcome.
124
+ *
125
+ * NINE members, and all nine block.
127
126
  */
128
127
  const AUDIT_FAIL_REASON = Object.freeze({
129
128
  UNSATISFIED: 'unsatisfied',
@@ -134,11 +133,12 @@ const AUDIT_FAIL_REASON = Object.freeze({
134
133
  DIFF_UNREADABLE: 'diff_unreadable',
135
134
  VAULT_UNREADABLE: 'vault_unreadable',
136
135
  PRACTICE_UNLOADABLE: 'practice_unloadable',
136
+ VAULT_STANDARDS_ONLY: 'vault_standards_only',
137
137
  });
138
138
  /**
139
139
  * Where the compared range came from — recorded beside the range itself.
140
140
  *
141
- * In ladder order: the phase's recorded start commit,
141
+ * In ladder order (260915-ik2, UAT-6-F1): the phase's recorded start commit,
142
142
  * the parent of its first recorded commit, the parent of the ancestry-earliest
143
143
  * SUMMARY trace digest, and only then the base-branch fallback. Every source
144
144
  * but the last is resolved FROM THE PHASE, which is what WR-04's block predicate
@@ -162,9 +162,11 @@ const AUDIT_RANGE_SOURCE = Object.freeze({
162
162
  * gate block vacuously had no way to ask it about a range they knew was the
163
163
  * phase's. This is the diagnostic channel that answer needs.
164
164
  *
165
- * A review tier that maps audit sources should leave this one unmapped, for
166
- * the same reason as `base-branch`: a review that accepts no range flag can
167
- * never be handed an operator range. Kebab-cased like its four siblings — it IS a source, unlike
165
+ * DELIBERATELY UNMAPPED in the review tier's
166
+ * `REVIEW_SOURCE_FOR_AUDIT_SOURCE` (`src/vault-review.cts:450-454`), for the
167
+ * same reason `base-branch` is: the review never accepts a range flag, so an
168
+ * operator range can never reach it and a mapping would assert a path that
169
+ * does not exist. Kebab-cased like its four siblings — it IS a source, unlike
168
170
  * `AUDIT_RANGE_OUTCOME`'s members.
169
171
  */
170
172
  OPERATOR_RANGE: 'operator-range',
@@ -218,7 +220,7 @@ function isValidOperatorRange(value) {
218
220
  * `phaseCommitsFor` returned `[]` for five structurally different grounds and
219
221
  * `resolvePhaseRange` distinguished none of them, so `rangeSource:
220
222
  * 'base-branch'` was the whole story an operator got. The executing checkout
221
- * writes the manifest itself, but a checkout that never ran
223
+ * writes the manifest itself (260915-ik2), but a checkout that never ran
222
224
  * `/pp-execute-phase` — a fresh clone or a CI runner where `.planning/` is
223
225
  * gitignored — has no manifest to read, and there the fallback fires whenever
224
226
  * no SUMMARY trace digest verifies either. That is precisely the case where
@@ -228,15 +230,15 @@ function isValidOperatorRange(value) {
228
230
  */
229
231
  const AUDIT_FALLBACK_REASON = Object.freeze({
230
232
  /** The phase directory's name does not begin with an `NN` or `NN.M` token. */
231
- PHASE_SLUG_UNRECOGNISED: PHASE_MANIFEST_READ_MISS.PHASE_SLUG_UNRECOGNISED,
232
- /** No `.phase-manifest.json` at the location `phase manifest start` writes for this phase. */
233
- MANIFEST_NOT_FOUND: PHASE_MANIFEST_READ_MISS.MANIFEST_NOT_FOUND,
233
+ PHASE_SLUG_UNRECOGNISED: 'phase-slug-unrecognised',
234
+ /** No `.phase-manifest.json` exists anywhere on the walk up from the phase dir. */
235
+ MANIFEST_NOT_FOUND: 'manifest-not-found',
234
236
  /** A manifest was found but is not parseable JSON of the expected shape. */
235
- MANIFEST_MALFORMED: PHASE_MANIFEST_READ_MISS.MANIFEST_MALFORMED,
236
- /** A manifest parsed, but holds no usable entry for this phase under either key spelling. */
237
- PHASE_ENTRY_MISSING: PHASE_MANIFEST_READ_MISS.PHASE_ENTRY_MISSING,
238
- /** The phase's entry exists and records neither a start commit nor any commit. */
239
- NO_COMMITS_RECORDED: PHASE_MANIFEST_READ_MISS.NO_COMMITS_RECORDED,
237
+ MANIFEST_MALFORMED: 'manifest-malformed',
238
+ /** A manifest parsed, but holds no entry for this phase under either key spelling. */
239
+ PHASE_ENTRY_MISSING: 'phase-entry-missing',
240
+ /** The phase's entry exists and records no commits. */
241
+ NO_COMMITS_RECORDED: 'no-commits-recorded',
240
242
  /** Commits were recorded, and git verified none of them. */
241
243
  COMMIT_UNVERIFIABLE: 'commit-unverifiable',
242
244
  });
@@ -256,7 +258,7 @@ const AUDIT_FALLBACK_REASON = Object.freeze({
256
258
  *
257
259
  * `range_unresolvable` is deliberately NOT a fifth `AUDIT_RANGE_SOURCE` member
258
260
  * (RESEARCH R7, Open Question 2): nothing was resolved, so there is no source
259
- * to name, and a consumer's source map would then owe it a mapping it
261
+ * to name, and `REVIEW_SOURCE_FOR_AUDIT_SOURCE` would then owe it a mapping it
260
262
  * has no meaning for.
261
263
  *
262
264
  * CASING. These three values are underscore-cased while the kebab-cased
@@ -331,6 +333,37 @@ function classifyRangeOutcome(input) {
331
333
  function isPhaseScopedSource(rangeSource) {
332
334
  return rangeSource !== AUDIT_RANGE_SOURCE.BASE_BRANCH;
333
335
  }
336
+ /**
337
+ * Why `recordPhaseStart` / `recordPhaseCommits` wrote nothing (260915-ik2).
338
+ *
339
+ * Two members REUSE the reader's strings, deliberately: a phase slug the
340
+ * writer cannot key and a manifest it refuses to overwrite are the same facts
341
+ * the reader reports on the same file, and two spellings of one fact is the
342
+ * drift this module's frozen vocabularies exist to prevent.
343
+ */
344
+ const PHASE_MANIFEST_WRITE_REASON = Object.freeze({
345
+ /** The phase directory is not an existing directory inside the project root. */
346
+ PHASE_DIR_UNRESOLVABLE: 'phase-dir-unresolvable',
347
+ /** The phase directory's name does not begin with an `NN` or `NN.M` token. */
348
+ PHASE_SLUG_UNRECOGNISED: AUDIT_FALLBACK_REASON.PHASE_SLUG_UNRECOGNISED,
349
+ /** git could not name the commit HEAD points at (an unborn branch, or no repository). */
350
+ HEAD_UNRESOLVABLE: 'head-unresolvable',
351
+ /** The manifest exists and is not a JSON object with an object `phases`; it is left untouched. */
352
+ MANIFEST_MALFORMED: AUDIT_FALLBACK_REASON.MANIFEST_MALFORMED,
353
+ /** `record` found no start commit for this phase — `start` never ran for it. */
354
+ START_NOT_RECORDED: 'start-not-recorded',
355
+ /** `record` found a start commit that git does not verify. */
356
+ START_UNVERIFIABLE: 'start-unverifiable',
357
+ /**
358
+ * `start` found no anchor for this phase, and its directory already holds a
359
+ * `*-SUMMARY.md`: the phase has been executed, so HEAD sits AFTER its own
360
+ * commits and an anchor there would scope the audit past them (R2). Nothing
361
+ * is written; the SUMMARY trace-digest rung is the range source instead.
362
+ */
363
+ PHASE_ALREADY_EXECUTED: 'phase-already-executed',
364
+ /** The atomic write (temp file + rename) failed; the temp file was removed. */
365
+ WRITE_FAILED: 'write-failed',
366
+ });
334
367
  /**
335
368
  * The bounded WHOLE-file read for a content check (1 MiB).
336
369
  *
@@ -389,6 +422,13 @@ const AUDIT_OPERATOR_RANGE_REPAIR_SENTENCE = 'Repair: re-run the audit with a we
389
422
  * and nothing else (T-4-75).
390
423
  */
391
424
  const AUDIT_VAULT_REPAIR_SENTENCE = 'Repair: re-fetch the client vault with `pp-tools vault fetch`, confirm `.planning/vault` is readable, then run this audit again.';
425
+ /**
426
+ * The standards-only arm's repair (quick 261015-at3). A sibling of the vault
427
+ * arm's sentence and deliberately not that sentence: re-fetching reproduces
428
+ * the same standards vault, so the only repair is the mode. A repair, never a
429
+ * removal (T-4-75).
430
+ */
431
+ const AUDIT_STANDARDS_ONLY_REPAIR_SENTENCE = 'Repair: set "vault": {"mode": "standards"} in .planning/config.json (`pp-tools config-set vault.mode standards`, pp-core 1.16.0 or later), re-plan the phase, then run this audit again.';
392
432
  // ─── Shape predicates ─────────────────────────────────────────────────────────
393
433
  //
394
434
  // SHAPE-PREDICATES-BEGIN
@@ -397,7 +437,7 @@ const AUDIT_VAULT_REPAIR_SENTENCE = 'Repair: re-fetch the client vault with `pp-
397
437
  // module's own frozen verdict constants and nothing else. No regular
398
438
  // expression, no text-search primitive, no character-code probe, no inline
399
439
  // literal may appear here or reach a boundary decision anywhere in this file —
400
- // The declarative-check rule states the prohibition and `eslint-rules/practices-boundary-inputs.cjs`,
440
+ // ADR-71924 §3 states the prohibition and `eslint-rules/practices-boundary-inputs.cjs`,
401
441
  // wired at this file in `eslint.config.mjs`, is what keeps it: a future edit
402
442
  // reintroducing one fails `npm run lint`, not a review.
403
443
  //
@@ -520,7 +560,7 @@ const AUDIT_COVERAGE_OUTCOME = Object.freeze({
520
560
  });
521
561
  /**
522
562
  * The counts-only coverage line (D-10-03, D-10-18), modelled on
523
- * the practice review's rate line and inheriting its two rules
563
+ * `renderRateLine` (`src/vault-review.cts:845`) and inheriting its two rules
524
564
  * verbatim:
525
565
  *
526
566
  * - the figures are DERIVED from the four verdict counts and a supplied `n`
@@ -544,25 +584,6 @@ function renderMechanicalCoverageLine(counts) {
544
584
  return (`${checked} of ${applicable} practices mechanically checked · ` +
545
585
  `${pass} PASS · ${fail} FAIL · ${notMatched} NOT_MATCHED · ${noCheck} NO_CHECK`);
546
586
  }
547
- /**
548
- * The one sentence every surface states about the recorded `check:` census.
549
- *
550
- * The census is the vault's, taken when the ledger was written; this audit's
551
- * counts are the run's. The sentence keeps the two apart: it says what the
552
- * LEDGER records, and never that the audit found it. Engine-composed apart
553
- * from the artifact name, which is a file name in the phase directory and is
554
- * bounded.
555
- */
556
- function renderRecordedCensusLine(census) {
557
- if (census === null)
558
- return "No check: census is recorded in this phase's trace ledgers.";
559
- const sha = census.resolvedSha ? census.resolvedSha.slice(0, 12) : 'an unrecorded sha';
560
- const moved = census.distinct > 1
561
- ? ` The phase's ledgers record ${census.distinct} different censuses, so the vault changed during the phase.`
562
- : '';
563
- return (`The phase's trace ledger (${boundedToken(census.artifact)}) records ${census.checkBearingPractices} of ` +
564
- `${census.practiceUniverse} practices carrying a check: block, at vault ${sha}.${moved}`);
565
- }
566
587
  /**
567
588
  * Does this verdict set halt the gate? A blocking decision made on client data,
568
589
  * so it is audited: only a FAIL blocks, and it is emitted as a strict boolean
@@ -589,7 +610,7 @@ function blocksOnVerdicts(entries) {
589
610
  * gate rejects matches nothing here rather than being interpreted by some
590
611
  * second, more permissive reading of the same string. `globMatch` does the
591
612
  * matching; this module never writes one. That holds for the applies-to side
592
- * exactly as it already held for the check side (the declarative-check rule).
613
+ * exactly as it already held for the check side (ADR-71924 §2 and §3).
593
614
  */
594
615
  function matchCheckFiles(check, files, appliesTo) {
595
616
  const scope = Array.isArray(appliesTo) ? appliesTo : [];
@@ -634,7 +655,7 @@ function matchCheckFiles(check, files, appliesTo) {
634
655
  * Does `content` contain the literal token?
635
656
  *
636
657
  * Plain substring containment, and nothing else, ever. The token is a
637
- * client-authored byte sequence from the vault repository; the declarative-check rule bars it
658
+ * client-authored byte sequence from the vault repository; ADR-71924 §3 bars it
638
659
  * from every pattern compiler, so a token that LOOKS like a pattern is tested
639
660
  * as the literal text it is.
640
661
  */
@@ -932,21 +953,173 @@ function evaluatePractices(opts) {
932
953
  budgetExhausted,
933
954
  };
934
955
  }
956
+ // ─── Diff scoping (D-05-08) ───────────────────────────────────────────────────
957
+ /** How far up from a phase directory the manifest walk may climb. */
958
+ const MANIFEST_WALK_MAX = 12;
959
+ /** The manifest file `/pp-undo` already reads for exactly this purpose. */
960
+ const PHASE_MANIFEST_FILE = '.phase-manifest.json';
935
961
  /**
936
- * The commit list and start commit this phase's manifest entry records, plus
937
- * the NAMED ground on which there was none.
938
- *
939
- * The manifest is written by `pp-tools phase manifest start|record`
940
- * (src/phase-manifest.cts): `start` at execute:pre anchors the phase at HEAD,
941
- * `record` after every wave lists the commits since. This reads it through the
942
- * same module, from the same file. The shas it returns are engine-written but
943
- * sit in editable planning data, so the resolver re-checks their shape before
944
- * any reaches a git argv. Never throws: every failure is a fallback to the next
945
- * rung, which is a WIDER comparison, never a narrower one.
962
+ * An engine-authored guard over engine-written values.
963
+ *
964
+ * The shas come from `.phase-manifest.json` — written by `recordPhaseStart` /
965
+ * `recordPhaseCommits` below, reached through `pp-tools vault manifest
966
+ * start|record` from the execute-phase step files — and, on the trace-digest
967
+ * rung, from the `gitCommit` digests in a phase's SUMMARY `vault:` blocks.
968
+ * Both files are editable planning data and every sha is interpolated into a
969
+ * git ARGV, where a leading `-` would be read as a flag rather than as a
970
+ * revision. This is the one shape a sha may have. It is NOT the client-authored
971
+ * path: no vault byte reaches this, so ADR-71924 §3 is untouched (it bans
972
+ * compiling a CLIENT-authored byte into a pattern, which nothing here does).
973
+ */
974
+ const COMMIT_SHA_SHAPE = /^[0-9a-fA-F]{7,64}$/;
975
+ /** The leading `NN` or `NN.M` token of a phase directory slug. */
976
+ const PHASE_SLUG_SHAPE = /^(\d+(?:\.\d+)?)/;
977
+ /**
978
+ * Is this manifest entry stamped for a DIFFERENT phase directory? (R1)
979
+ *
980
+ * Entries are keyed by phase NUMBER, and `/pp-new-milestone
981
+ * --reset-phase-numbers` reuses numbers, so a new milestone's `01-new` would
982
+ * otherwise inherit `01-old`'s anchor and commits. The writers stamp
983
+ * `phaseDir` with the directory basename; an entry whose stamp names another
984
+ * basename belongs to another phase. An entry with NO stamp (written before
985
+ * the stamp existed, or by hand) is not stale — it keeps the behaviour it had.
986
+ */
987
+ function isStaleEntry(entry, slug) {
988
+ return typeof entry.phaseDir === 'string' && entry.phaseDir !== slug;
989
+ }
990
+ /** The one sentence a stale entry is disclosed with, shared by the reader and nothing else. */
991
+ function staleEntryNote(key, recorded, slug) {
992
+ return (`the phase manifest entry "${boundedToken(key)}" is stale: it was recorded for phase directory ` +
993
+ `"${boundedToken(recorded)}", not "${boundedToken(slug)}" (a phase number reused after a milestone reset), ` +
994
+ 'so its startCommit and commits were ignored');
995
+ }
996
+ /**
997
+ * The commit list this phase's manifest entry records, plus the NAMED ground
998
+ * on which there was none (05-10).
999
+ *
1000
+ * Walks UP from the phase directory and tries the manifest at each ancestor,
1001
+ * nearest first, which covers both the flat `.planning/phases/NN-slug` layout
1002
+ * and the partitioned `.planning/workstreams/<name>/phases/NN-slug` one without
1003
+ * either being special-cased. Never throws: every failure at every step is a
1004
+ * fallback to the base-branch range, which is a WIDER comparison, never a
1005
+ * narrower one.
1006
+ *
1007
+ * The miss ground is recorded NEAREST-FIRST and never overwritten by a further
1008
+ * ancestor: the manifest closest to the phase directory is the one an operator
1009
+ * would go and look at, so it is the one worth naming.
1010
+ *
1011
+ * `startCommit` is ADDITIVE (260915-ik2): an entry may carry it beside
1012
+ * `commits`, and the `{ phases: { "05": { commits: [...] } } }` shape
1013
+ * `/pp-undo` reads stays valid — an entry with only `commits` resolves exactly
1014
+ * as it did before the field existed.
946
1015
  */
947
1016
  function phaseCommitsFor(root, phaseDir) {
948
- const read = readPhaseManifestEntry({ root, phaseDir });
949
- return { commits: read.commits, miss: read.miss, startCommit: read.startCommit, notes: read.notes };
1017
+ const phaseDirAbs = node_path_1.default.resolve(phaseDir);
1018
+ const slug = node_path_1.default.basename(phaseDirAbs);
1019
+ const slugMatch = PHASE_SLUG_SHAPE.exec(slug);
1020
+ if (!slugMatch)
1021
+ return { commits: [], miss: AUDIT_FALLBACK_REASON.PHASE_SLUG_UNRECOGNISED, startCommit: null, notes: [] };
1022
+ const rawToken = slugMatch[1];
1023
+ const notes = [];
1024
+ // A directory named `05-x` and a manifest keyed `5` are the same phase; try
1025
+ // both spellings rather than guessing which writer produced the file.
1026
+ const keys = [rawToken, String(Number(rawToken))];
1027
+ let current = node_path_1.default.dirname(phaseDirAbs);
1028
+ const rootAbs = node_path_1.default.resolve(root);
1029
+ // Nearest-first, and never widened once set — see the header note.
1030
+ let miss = AUDIT_FALLBACK_REASON.MANIFEST_NOT_FOUND;
1031
+ const recordMiss = (ground) => {
1032
+ if (miss === AUDIT_FALLBACK_REASON.MANIFEST_NOT_FOUND)
1033
+ miss = ground;
1034
+ };
1035
+ for (let depth = 0; depth < MANIFEST_WALK_MAX; depth += 1) {
1036
+ const candidate = node_path_1.default.join(current, PHASE_MANIFEST_FILE);
1037
+ if (node_fs_1.default.existsSync(candidate)) {
1038
+ try {
1039
+ const parsed = JSON.parse(node_fs_1.default.readFileSync(candidate, 'utf8'));
1040
+ const phases = parsed && typeof parsed === 'object' ? parsed.phases : undefined;
1041
+ if (phases && typeof phases === 'object') {
1042
+ // A key present with an empty commit list is a DIFFERENT operator
1043
+ // problem from a key that is not there at all, so the two grounds
1044
+ // are separated rather than both reading "no entry".
1045
+ // A STALE entry (R1) is another phase's record, so for this phase it
1046
+ // is no entry at all: it counts as missing, and it is disclosed.
1047
+ const usable = (key) => {
1048
+ if (!Object.prototype.hasOwnProperty.call(phases, key))
1049
+ return false;
1050
+ const entry = phases[key];
1051
+ return !(entry && typeof entry === 'object' && isStaleEntry(entry, slug));
1052
+ };
1053
+ const present = keys.some(usable);
1054
+ recordMiss(present ? AUDIT_FALLBACK_REASON.NO_COMMITS_RECORDED : AUDIT_FALLBACK_REASON.PHASE_ENTRY_MISSING);
1055
+ // Both fields are read across both key spellings before returning,
1056
+ // so an anchor under `5` is not hidden by a commit list under `05`.
1057
+ let startCommit = null;
1058
+ let commits = null;
1059
+ for (const key of keys) {
1060
+ const entry = Object.prototype.hasOwnProperty.call(phases, key) ? phases[key] : undefined;
1061
+ if (!entry)
1062
+ continue;
1063
+ if (typeof entry === 'object' && isStaleEntry(entry, slug)) {
1064
+ const note = staleEntryNote(key, entry.phaseDir, slug);
1065
+ if (!notes.includes(note))
1066
+ notes.push(note);
1067
+ continue;
1068
+ }
1069
+ const start = entry.startCommit;
1070
+ if (startCommit === null && typeof start === 'string' && COMMIT_SHA_SHAPE.test(start)) {
1071
+ startCommit = start;
1072
+ }
1073
+ const listed = entry.commits;
1074
+ if (commits === null && Array.isArray(listed) && listed.length > 0) {
1075
+ commits = listed.filter((c) => typeof c === 'string');
1076
+ }
1077
+ }
1078
+ if (commits !== null)
1079
+ return { commits, miss: null, startCommit, notes };
1080
+ // An anchor with no commit list yet (execute:pre ran, verify:post has
1081
+ // not) IS a record, so it carries no miss ground.
1082
+ if (startCommit !== null)
1083
+ return { commits: [], miss: null, startCommit, notes };
1084
+ }
1085
+ else {
1086
+ recordMiss(AUDIT_FALLBACK_REASON.MANIFEST_MALFORMED);
1087
+ }
1088
+ }
1089
+ catch {
1090
+ // A malformed manifest is not a reason to invent a range; fall through
1091
+ // to the base-branch fallback, which is over-broad but honest. The
1092
+ // GROUND is recorded, because "the manifest you are looking at is not
1093
+ // valid JSON" is a repairable fact and "there is no manifest" is not
1094
+ // the same one.
1095
+ recordMiss(AUDIT_FALLBACK_REASON.MANIFEST_MALFORMED);
1096
+ }
1097
+ }
1098
+ const parent = node_path_1.default.dirname(current);
1099
+ if (parent === current)
1100
+ break;
1101
+ if (current === rootAbs)
1102
+ break;
1103
+ current = parent;
1104
+ }
1105
+ return { commits: [], miss, startCommit: null, notes };
1106
+ }
1107
+ /**
1108
+ * Does the phase manifest hold a usable entry for this phase directory? (16-03, D-20)
1109
+ *
1110
+ * The seal leaf's `phase_manifest` artifact kind asks THIS reader, so "this
1111
+ * phase has a manifest record" means exactly what the audit's own range ladder
1112
+ * reads: an entry under either key spelling, not stale (R1), carrying a
1113
+ * `commits` list or a `startCommit`. A second walk in the leaf could disagree
1114
+ * with this one about a stale or malformed manifest. Never throws.
1115
+ */
1116
+ function hasPhaseManifestEntry(root, phaseDir) {
1117
+ try {
1118
+ return phaseCommitsFor(root, phaseDir).miss === null;
1119
+ }
1120
+ catch {
1121
+ return false;
1122
+ }
950
1123
  }
951
1124
  /**
952
1125
  * Resolve the git range this audit compares, and record WHERE it came from.
@@ -958,7 +1131,7 @@ function phaseCommitsFor(root, phaseDir) {
958
1131
  * to prevent (D-05-08). And when work is committed straight onto the base
959
1132
  * branch, `base...HEAD` is EMPTY — the UAT-6-F1 halt.
960
1133
  *
961
- * A FOUR-RUNG LADDER, in this order and no other:
1134
+ * A FOUR-RUNG LADDER, in this order and no other (260915-ik2):
962
1135
  *
963
1136
  * 1. `phase-start-commit` — the manifest entry's `startCommit`, the commit
964
1137
  * HEAD sat at when `/pp-execute-phase` began the phase (written at
@@ -996,7 +1169,7 @@ function phaseCommitsFor(root, phaseDir) {
996
1169
  * with it so an operator can see the third rung was tried.
997
1170
  */
998
1171
  function resolvePhaseRange(opts) {
999
- const exec = typeof opts.exec === 'function' ? opts.exec : shell_command_projection_cjs_1.execGit;
1172
+ const exec = typeof opts.exec === 'function' ? opts.exec : shell_command_projection_cjs_1.execGitLocal;
1000
1173
  const found = (() => {
1001
1174
  try {
1002
1175
  return phaseCommitsFor(opts.root, opts.phaseDir);
@@ -1123,7 +1296,7 @@ function resolvePhaseRange(opts) {
1123
1296
  ...withLead(digest.notes),
1124
1297
  };
1125
1298
  }
1126
- // ─── The trace-digest rung ────────────────────────────
1299
+ // ─── The trace-digest rung (260915-ik2, UAT-6-F1) ────────────────────────────
1127
1300
  /** The `gitCommit` shape the trace builder records — 40- or 64-hex, lowercase. */
1128
1301
  const TRACE_DIGEST_SHAPE = /^[0-9a-f]{40}$|^[0-9a-f]{64}$/;
1129
1302
  /** At most this many distinct digests are verified and ranked, with a note when it bites. */
@@ -1136,7 +1309,7 @@ const TRACE_DIGEST_CANDIDATE_CAP = 64;
1136
1309
  * bounded by `AUDIT_FILE_READ_MAX`), parsed with the SHIPPED trace parser, and
1137
1310
  * its subjects' digests collected when they have the builder's own shape; the
1138
1311
  * wider `COMMIT_SHA_SHAPE` guard is applied again before any sha reaches argv
1139
- * (the argv-shape guard). The distinct set is capped, then each is verified with
1312
+ * (T-ik2-02). The distinct set is capped, then each is verified with
1140
1313
  * `rev-parse --verify <sha>^{commit}`: an unverifiable digest is never an
1141
1314
  * anchor.
1142
1315
  *
@@ -1250,6 +1423,333 @@ function traceDigestRange(opts) {
1250
1423
  return none(`the SUMMARY trace digests could not be read: ${errnoOf(err)}`);
1251
1424
  }
1252
1425
  }
1426
+ /**
1427
+ * Where this phase's manifest lives, or the named ground on which it cannot.
1428
+ *
1429
+ * `phaseDir` resolves against `cwd`, then against `root`; the first that is an
1430
+ * existing directory CONTAINED in the root wins (T-ik2-03), lexically or with
1431
+ * symlinks resolved on both sides (a macOS temp root is `/var` and
1432
+ * `/private/var` at once). The manifest sits two levels above the phase
1433
+ * directory: `.planning/.phase-manifest.json` on the flat layout — the file
1434
+ * `/pp-undo` reads — and `.planning/workstreams/<ws>/.phase-manifest.json` on
1435
+ * a partitioned one, which the reader's walk finds at depth one and which keeps
1436
+ * two workstreams' `05` entries apart.
1437
+ */
1438
+ function resolveManifestTarget(opts) {
1439
+ const unresolvable = { ok: false, reason: PHASE_MANIFEST_WRITE_REASON.PHASE_DIR_UNRESOLVABLE };
1440
+ if (typeof opts?.root !== 'string' || opts.root === '')
1441
+ return unresolvable;
1442
+ if (typeof opts?.phaseDir !== 'string' || opts.phaseDir === '' || opts.phaseDir.includes('\0'))
1443
+ return unresolvable;
1444
+ const rootLexical = node_path_1.default.resolve(opts.root);
1445
+ const rootReal = resolvedRootOf(rootLexical);
1446
+ const bases = [typeof opts.cwd === 'string' && opts.cwd !== '' ? opts.cwd : process.cwd(), rootLexical];
1447
+ let phaseDirAbs = null;
1448
+ let rootAbs = rootLexical;
1449
+ for (const base of bases) {
1450
+ const abs = node_path_1.default.resolve(base, opts.phaseDir);
1451
+ try {
1452
+ if (!node_fs_1.default.statSync(abs).isDirectory())
1453
+ continue;
1454
+ }
1455
+ catch {
1456
+ continue;
1457
+ }
1458
+ if (isContainedIn(abs, rootLexical)) {
1459
+ phaseDirAbs = abs;
1460
+ rootAbs = rootLexical;
1461
+ break;
1462
+ }
1463
+ const real = resolvedRootOf(abs);
1464
+ if (isContainedIn(real, rootReal)) {
1465
+ phaseDirAbs = real;
1466
+ rootAbs = rootReal;
1467
+ break;
1468
+ }
1469
+ }
1470
+ if (phaseDirAbs === null)
1471
+ return unresolvable;
1472
+ const slugMatch = PHASE_SLUG_SHAPE.exec(node_path_1.default.basename(phaseDirAbs));
1473
+ if (!slugMatch)
1474
+ return { ok: false, reason: PHASE_MANIFEST_WRITE_REASON.PHASE_SLUG_UNRECOGNISED };
1475
+ const manifestAbs = node_path_1.default.join(node_path_1.default.dirname(node_path_1.default.dirname(phaseDirAbs)), PHASE_MANIFEST_FILE);
1476
+ if (!isContainedIn(manifestAbs, rootAbs))
1477
+ return unresolvable;
1478
+ return {
1479
+ ok: true,
1480
+ target: {
1481
+ rootAbs,
1482
+ manifestAbs,
1483
+ manifestPath: node_path_1.default.relative(rootAbs, manifestAbs).split(node_path_1.default.sep).join('/'),
1484
+ rawToken: slugMatch[1],
1485
+ phaseDirAbs,
1486
+ phaseSlug: node_path_1.default.basename(phaseDirAbs),
1487
+ },
1488
+ };
1489
+ }
1490
+ /**
1491
+ * The existing manifest, parsed for an ADDITIVE write — or `null` when it
1492
+ * must not be overwritten: unparseable JSON, a non-object document, a
1493
+ * non-object `phases`, or a non-object entry for this phase (T-ik2-05). An
1494
+ * absent file is an empty document, not a refusal.
1495
+ */
1496
+ function readManifestForWrite(manifestAbs) {
1497
+ if (!node_fs_1.default.existsSync(manifestAbs))
1498
+ return { doc: {}, phases: {}, raw: null };
1499
+ try {
1500
+ const raw = node_fs_1.default.readFileSync(manifestAbs, 'utf8');
1501
+ const parsed = JSON.parse(raw);
1502
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
1503
+ return null;
1504
+ const doc = parsed;
1505
+ if (doc.phases === undefined)
1506
+ return { doc, phases: {}, raw };
1507
+ if (!doc.phases || typeof doc.phases !== 'object' || Array.isArray(doc.phases))
1508
+ return null;
1509
+ return { doc, phases: doc.phases, raw };
1510
+ }
1511
+ catch {
1512
+ return null;
1513
+ }
1514
+ }
1515
+ /** The key this phase's entry lives under: an existing spelling wins, else the slug's own token. */
1516
+ function manifestKeyFor(phases, rawToken) {
1517
+ const spellings = [rawToken, String(Number(rawToken))];
1518
+ return spellings.find((key) => Object.prototype.hasOwnProperty.call(phases, key)) ?? rawToken;
1519
+ }
1520
+ /** The entry under `key` as a plain object, `{}` when absent, `null` when present and not an object. */
1521
+ function manifestEntryOf(phases, key) {
1522
+ if (!Object.prototype.hasOwnProperty.call(phases, key))
1523
+ return {};
1524
+ const entry = phases[key];
1525
+ return entry && typeof entry === 'object' && !Array.isArray(entry) ? entry : null;
1526
+ }
1527
+ /** Two-space JSON with a trailing newline — the one serialization, so a re-record is byte-stable. */
1528
+ function serializeManifest(doc) {
1529
+ return `${JSON.stringify(doc, null, 2)}\n`;
1530
+ }
1531
+ /**
1532
+ * Write atomically: a sibling temp file named with the pid, then `renameSync`
1533
+ * over the target — the pattern `src/runtime-hooks-surface.cts` uses. The temp
1534
+ * file is removed on any failure. Returns `null` on success, else an errno.
1535
+ */
1536
+ function writeManifestAtomic(manifestAbs, serialized) {
1537
+ const tmp = `${manifestAbs}.${process.pid}.tmp`;
1538
+ try {
1539
+ node_fs_1.default.writeFileSync(tmp, serialized, 'utf8');
1540
+ node_fs_1.default.renameSync(tmp, manifestAbs);
1541
+ return null;
1542
+ }
1543
+ catch (err) {
1544
+ try {
1545
+ node_fs_1.default.unlinkSync(tmp);
1546
+ }
1547
+ catch {
1548
+ // Nothing to remove, or it cannot be removed; the refusal below stands.
1549
+ }
1550
+ return errnoOf(err);
1551
+ }
1552
+ }
1553
+ /**
1554
+ * Has this phase already been executed? (R2) `true` when its directory holds
1555
+ * any `*-SUMMARY.md` — the same file set the trace-digest rung reads — and
1556
+ * `null` when the directory cannot be listed, which the caller refuses on
1557
+ * rather than guessing.
1558
+ */
1559
+ function phaseHasSummary(phaseDirAbs) {
1560
+ try {
1561
+ return node_fs_1.default.readdirSync(phaseDirAbs).some((name) => name.endsWith('-SUMMARY.md'));
1562
+ }
1563
+ catch {
1564
+ return null;
1565
+ }
1566
+ }
1567
+ /** A verified commit sha from a `rev-parse --verify` probe, or `null`. */
1568
+ function verifiedSha(exec, rev, cwd) {
1569
+ const probe = exec(['rev-parse', '--verify', rev], { cwd });
1570
+ if (probe.exitCode !== 0 || probe.error)
1571
+ return null;
1572
+ const sha = String(probe.stdout || '').trim();
1573
+ return COMMIT_SHA_SHAPE.test(sha) ? sha : null;
1574
+ }
1575
+ /**
1576
+ * Anchor the phase: `phases[NN].startCommit = HEAD`, WRITE-ONCE.
1577
+ *
1578
+ * Called at execute:pre, in this order:
1579
+ *
1580
+ * 1. WRITE-ONCE. An entry for THIS phase directory that already carries a
1581
+ * start commit is left byte-identical and reported `unchanged: true`, so
1582
+ * a resume or a `--gaps-only` run keeps the anchor the phase actually
1583
+ * started from. An unstamped (legacy) entry counts as this phase's.
1584
+ * 2. ALREADY EXECUTED (R2). With no usable anchor, a phase directory that
1585
+ * already holds a `*-SUMMARY.md` is refused as `phase-already-executed`
1586
+ * and nothing is written: execute:pre fires on every re-run, and HEAD on
1587
+ * an executed phase sits after its own commits, so an anchor there would
1588
+ * put the phase's work outside its own range. The trace-digest rung is
1589
+ * the range source for such a phase.
1590
+ * 3. STALE (R1). An entry stamped for another phase directory — a number
1591
+ * reused after a milestone reset — is REPLACED by a fresh
1592
+ * `{ startCommit, phaseDir }`; its old commits are dropped. The result
1593
+ * carries `replacedStale: true`.
1594
+ *
1595
+ * Every write stamps `phaseDir` with the directory basename. Any `commits`
1596
+ * beside a non-stale entry are preserved. Never throws; a refusal is
1597
+ * `{ ok: false, action, reason }` with a `PHASE_MANIFEST_WRITE_REASON` member.
1598
+ */
1599
+ function recordPhaseStart(opts) {
1600
+ const action = 'start';
1601
+ try {
1602
+ const exec = typeof opts?.exec === 'function' ? opts.exec : shell_command_projection_cjs_1.execGitLocal;
1603
+ const resolved = resolveManifestTarget(opts);
1604
+ if (!resolved.ok)
1605
+ return { ok: false, action, reason: resolved.reason };
1606
+ const { target } = resolved;
1607
+ const read = readManifestForWrite(target.manifestAbs);
1608
+ if (read === null)
1609
+ return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.MANIFEST_MALFORMED };
1610
+ const key = manifestKeyFor(read.phases, target.rawToken);
1611
+ const entry = manifestEntryOf(read.phases, key);
1612
+ if (entry === null)
1613
+ return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.MANIFEST_MALFORMED };
1614
+ const stale = isStaleEntry(entry, target.phaseSlug);
1615
+ // Write-once: the anchor a phase began from is never moved by a later run.
1616
+ if (!stale && typeof entry.startCommit === 'string' && COMMIT_SHA_SHAPE.test(entry.startCommit)) {
1617
+ return {
1618
+ ok: true,
1619
+ action,
1620
+ manifestPath: target.manifestPath,
1621
+ phase: key,
1622
+ startCommit: entry.startCommit,
1623
+ unchanged: true,
1624
+ };
1625
+ }
1626
+ // Checked before HEAD is read: a refusal asks git nothing (R2).
1627
+ const executed = phaseHasSummary(target.phaseDirAbs);
1628
+ if (executed === null) {
1629
+ return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.PHASE_DIR_UNRESOLVABLE };
1630
+ }
1631
+ if (executed)
1632
+ return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.PHASE_ALREADY_EXECUTED };
1633
+ const head = verifiedSha(exec, 'HEAD^{commit}', target.rootAbs);
1634
+ if (head === null)
1635
+ return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.HEAD_UNRESOLVABLE };
1636
+ const fresh = stale
1637
+ ? { startCommit: head, phaseDir: target.phaseSlug }
1638
+ : { ...entry, startCommit: head, phaseDir: target.phaseSlug };
1639
+ const doc = { ...read.doc, phases: { ...read.phases, [key]: fresh } };
1640
+ const failure = writeManifestAtomic(target.manifestAbs, serializeManifest(doc));
1641
+ if (failure !== null) {
1642
+ return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.WRITE_FAILED, detail: failure };
1643
+ }
1644
+ return {
1645
+ ok: true,
1646
+ action,
1647
+ manifestPath: target.manifestPath,
1648
+ phase: key,
1649
+ startCommit: head,
1650
+ unchanged: false,
1651
+ ...(stale ? { replacedStale: true } : {}),
1652
+ };
1653
+ }
1654
+ catch (err) {
1655
+ return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.WRITE_FAILED, detail: errnoOf(err) };
1656
+ }
1657
+ }
1658
+ /**
1659
+ * Close the phase's range: `phases[NN].commits` =
1660
+ * `git rev-list --reverse --no-merges <startCommit>..HEAD`, oldest first.
1661
+ *
1662
+ * Called at verify:post, before the `vault.audit` gate. Recomputed from the
1663
+ * anchor on every call, so two consecutive records leave the file
1664
+ * byte-identical (and the second writes nothing). An empty list is a valid,
1665
+ * honest record. A phase with no anchor answers `start-not-recorded` — the case
1666
+ * the trace-digest rung covers — and so does an entry stamped for ANOTHER
1667
+ * phase directory (R1): that anchor is another phase's, so nothing is listed
1668
+ * from it and the file is left for `start` to replace. A successful record
1669
+ * stamps `phaseDir`, which also adopts an unstamped (legacy) entry. Never
1670
+ * throws.
1671
+ */
1672
+ function recordPhaseCommits(opts) {
1673
+ const action = 'record';
1674
+ try {
1675
+ const exec = typeof opts?.exec === 'function' ? opts.exec : shell_command_projection_cjs_1.execGitLocal;
1676
+ const resolved = resolveManifestTarget(opts);
1677
+ if (!resolved.ok)
1678
+ return { ok: false, action, reason: resolved.reason };
1679
+ const { target } = resolved;
1680
+ const read = readManifestForWrite(target.manifestAbs);
1681
+ if (read === null)
1682
+ return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.MANIFEST_MALFORMED };
1683
+ const key = manifestKeyFor(read.phases, target.rawToken);
1684
+ const entry = manifestEntryOf(read.phases, key);
1685
+ if (entry === null)
1686
+ return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.MANIFEST_MALFORMED };
1687
+ if (isStaleEntry(entry, target.phaseSlug)) {
1688
+ return {
1689
+ ok: false,
1690
+ action,
1691
+ reason: PHASE_MANIFEST_WRITE_REASON.START_NOT_RECORDED,
1692
+ detail: 'the entry under this phase number was recorded for another phase directory',
1693
+ };
1694
+ }
1695
+ const start = entry.startCommit;
1696
+ if (typeof start !== 'string' || !COMMIT_SHA_SHAPE.test(start)) {
1697
+ return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.START_NOT_RECORDED };
1698
+ }
1699
+ if (verifiedSha(exec, `${start}^{commit}`, target.rootAbs) === null) {
1700
+ return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.START_UNVERIFIABLE };
1701
+ }
1702
+ const listed = exec(['rev-list', '--reverse', '--no-merges', `${start}..HEAD`], { cwd: target.rootAbs });
1703
+ if (listed.exitCode !== 0 || listed.error) {
1704
+ return {
1705
+ ok: false,
1706
+ action,
1707
+ reason: PHASE_MANIFEST_WRITE_REASON.HEAD_UNRESOLVABLE,
1708
+ detail: 'git rev-list could not list the commits since the start commit',
1709
+ };
1710
+ }
1711
+ const commits = String(listed.stdout || '')
1712
+ .split('\n')
1713
+ .map((line) => line.trim())
1714
+ .filter((line) => COMMIT_SHA_SHAPE.test(line));
1715
+ const doc = {
1716
+ ...read.doc,
1717
+ phases: { ...read.phases, [key]: { ...entry, commits, phaseDir: target.phaseSlug } },
1718
+ };
1719
+ const serialized = serializeManifest(doc);
1720
+ if (serialized !== read.raw) {
1721
+ const failure = writeManifestAtomic(target.manifestAbs, serialized);
1722
+ if (failure !== null) {
1723
+ return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.WRITE_FAILED, detail: failure };
1724
+ }
1725
+ }
1726
+ return {
1727
+ ok: true,
1728
+ action,
1729
+ manifestPath: target.manifestPath,
1730
+ phase: key,
1731
+ startCommit: start,
1732
+ commits,
1733
+ commitCount: commits.length,
1734
+ };
1735
+ }
1736
+ catch (err) {
1737
+ return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.WRITE_FAILED, detail: errnoOf(err) };
1738
+ }
1739
+ }
1740
+ /** The one-line operator rendering of a writer result; the step files print it and continue. */
1741
+ function renderManifestResult(result) {
1742
+ const r = (result && typeof result === 'object' ? result : {});
1743
+ if (r.ok !== true)
1744
+ return `Phase manifest not written: ${boundedToken(r.reason ?? 'unknown')}`;
1745
+ const sha7 = typeof r.startCommit === 'string' ? r.startCommit.slice(0, 7) : '';
1746
+ if (r.action === 'record') {
1747
+ return `Phase ${boundedToken(r.phase)}: ${Number(r.commitCount)} commit(s) recorded from ${sha7}..HEAD in ${boundedToken(r.manifestPath)}`;
1748
+ }
1749
+ return (`Phase ${boundedToken(r.phase)} anchored at ${sha7} in ${boundedToken(r.manifestPath)}` +
1750
+ (r.unchanged === true ? ' (already anchored; unchanged)' : '') +
1751
+ (r.replacedStale === true ? ' (replaced a stale entry recorded for another phase directory)' : ''));
1752
+ }
1253
1753
  // ─── The audit envelope ───────────────────────────────────────────────────────
1254
1754
  /** Is this resolved absolute path the root itself, or somewhere beneath it? */
1255
1755
  function isContainedIn(abs, rootAbs) {
@@ -1366,7 +1866,8 @@ function diffFilesFor(root, range, exec) {
1366
1866
  .map((f) => f.replace(/\\/g, '/'))
1367
1867
  // Load-bearing: a practice glob like `**/*.md` would otherwise match the
1368
1868
  // phase's own planning artifacts and FAIL a practice about product code.
1369
- .filter((f) => !f.startsWith('.planning/'));
1869
+ // 16-03: the seal leaf owns the `.planning/` path test.
1870
+ .filter((f) => !vaultSeal.isPlanningPath(f));
1370
1871
  const capped = all.length > AUDIT_DIFF_FILE_CAP;
1371
1872
  return { ok: true, files: capped ? all.slice(0, AUDIT_DIFF_FILE_CAP) : all, capped, total: all.length };
1372
1873
  }
@@ -1394,7 +1895,7 @@ function failItemLines(entries) {
1394
1895
  return [];
1395
1896
  // 05-REVIEW CR-02: `id` and `detail` carry vault-authored bytes (the parser
1396
1897
  // pins `id` to the vault filename; `detail` quotes require/forbid tokens),
1397
- // and the gate runner (`loop run-gates`) prints `message` verbatim into an orchestrating
1898
+ // and `check-query-gate.md` prints `message` verbatim into an orchestrating
1398
1899
  // agent's context on every run — the channel this module's own header calls
1399
1900
  // agent-EXECUTABLE. So the itemized region is defanged and fenced with the
1400
1901
  // idiom `renderPracticeEntry` already uses: a planted fence marker cannot
@@ -1403,15 +1904,10 @@ function failItemLines(entries) {
1403
1904
  // stays OUTSIDE the fence — the notice's claim about what is inside must
1404
1905
  // stay true. The repair sentence is appended by the callers, after this
1405
1906
  // return, so it remains the closing line a prepend can never cut off.
1406
- //
1407
- // REDACTED before defanged: a `require`/`forbid` token the vault wrote can
1408
- // carry a credential-bearing remote, and this message is printed verbatim.
1409
- // `redactRemoteText` is the one redactor (src/vault.cts); the fence is the
1410
- // one fence. Both are applied, neither is re-authored here.
1411
1907
  const items = [];
1412
1908
  for (const fail of fails.slice(0, AUDIT_MESSAGE_PROBLEM_CAP)) {
1413
- const detail = fail.detail ? ` — ${defangFenceMarkers(redactRemoteText(fail.detail))}` : '';
1414
- items.push(`- ${defangFenceMarkers(redactRemoteText(fail.id))}: ${String(fail.reason)}${detail}`);
1909
+ const detail = fail.detail ? ` — ${defangFenceMarkers(fail.detail)}` : '';
1910
+ items.push(`- ${defangFenceMarkers(fail.id)}: ${String(fail.reason)}${detail}`);
1415
1911
  }
1416
1912
  const lines = [EXTERNAL_CONTENT_FENCE_START, ...FENCE_NOTICE, '', ...items, EXTERNAL_CONTENT_FENCE_END];
1417
1913
  const omitted = fails.length - Math.min(fails.length, AUDIT_MESSAGE_PROBLEM_CAP);
@@ -1447,7 +1943,7 @@ function noteLine(note) {
1447
1943
  *
1448
1944
  * THE ONE COMPOSITION POINT, used by every return path of `runVaultAudit`
1449
1945
  * (05-10, G-1c). Before it, `notes[]` reached neither `block` nor `message` and
1450
- * the gate runner (`loop run-gates`) reads nothing else — so a truncated diff, a refused
1946
+ * `check-query-gate.md` reads nothing else — so a truncated diff, a refused
1451
1947
  * pair count, an unreadable practices directory and a base-branch fallback were
1452
1948
  * all invisible at the gate. The notes go FIRST because they are the limits
1453
1949
  * that may explain the findings below them, and because putting them first is
@@ -1534,7 +2030,7 @@ function verdictsWithoutComparison(practices, reason, detail) {
1534
2030
  * `vaultUnreadable` is computed from TYPED FIELDS and from the catch, never
1535
2031
  * from note prose. That is the whole point: before 05-10 the only vault-side
1536
2032
  * signal was a string in `notes[]`, `notes[]` reached neither `block` nor
1537
- * `message`, and the gate runner reads nothing else — so
2033
+ * `message`, and the enforcer at check-query-gate.md reads nothing else — so
1538
2034
  * every vault-side miss rendered as a clean pass (G-1a). A `startsWith()` over
1539
2035
  * the note would have re-created the prose-matching this codebase already
1540
2036
  * rejected once (`matchBudgetNotes`).
@@ -1547,6 +2043,7 @@ function runDiscovery(discover, root) {
1547
2043
  notes: [...(discovery?.notes ?? [])],
1548
2044
  skipped: Array.isArray(discovery?.skipped) ? discovery.skipped : [],
1549
2045
  vaultUnreadable: discovery?.readFailed === true || discovery?.vaultPresent === false,
2046
+ vaultStandardsOnly: discovery?.vaultStandardsOnly === true,
1550
2047
  };
1551
2048
  }
1552
2049
  catch {
@@ -1559,6 +2056,7 @@ function runDiscovery(discover, root) {
1559
2056
  notes: ['the vault could not be read; no practice was evaluated'],
1560
2057
  skipped: [],
1561
2058
  vaultUnreadable: true,
2059
+ vaultStandardsOnly: false,
1562
2060
  };
1563
2061
  }
1564
2062
  }
@@ -1641,8 +2139,6 @@ function refusedOperatorRangeResult() {
1641
2139
  mechanicallyChecked: counts.mechanicallyChecked,
1642
2140
  applicablePractices: counts.applicablePractices,
1643
2141
  evaluatedNothing: counts.evaluatedNothing,
1644
- // Nothing is read for a refused input, the ledgers included.
1645
- recordedCensus: null,
1646
2142
  practices: [],
1647
2143
  pairsEvaluated: 0,
1648
2144
  pairCap: MATCH_PAIR_CAP,
@@ -1670,13 +2166,8 @@ function refusedOperatorRangeResult() {
1670
2166
  * blocking gate.
1671
2167
  */
1672
2168
  function runVaultAudit(opts) {
1673
- const exec = typeof opts.exec === 'function' ? opts.exec : shell_command_projection_cjs_1.execGit;
1674
- // The default reads the cache through `resolveVaultCacheDir` — the same
1675
- // worktree-aware answer the practice trace pins against — so the audit and
1676
- // the ledger it is read beside describe one vault.
1677
- const discover = typeof opts.discover === 'function'
1678
- ? opts.discover
1679
- : (root) => discoverPracticeFiles(root, vaultMod.resolveVaultCacheDir(root).dir);
2169
+ const exec = typeof opts.exec === 'function' ? opts.exec : shell_command_projection_cjs_1.execGitLocal;
2170
+ const discover = typeof opts.discover === 'function' ? opts.discover : discoverPracticeFiles;
1680
2171
  const readFile = typeof opts.readFile === 'function' ? opts.readFile : createBoundedFileReader(opts.root);
1681
2172
  // D-08-19. An operator range REPLACES the ladder rather than joining it: the
1682
2173
  // operator is answering the question the ladder failed to answer, so
@@ -1693,15 +2184,6 @@ function runVaultAudit(opts) {
1693
2184
  const { range, rangeSource, fallbackReason, baseBranchRefused, notes: rangeNotes } = resolved;
1694
2185
  const discovery = runDiscovery(discover, opts.root);
1695
2186
  const practices = discovery.practices;
1696
- // Read once, from the ledgers, and carried on every arm below.
1697
- const recordedCensus = (() => {
1698
- try {
1699
- return readRecordedCensus(opts.phaseDir);
1700
- }
1701
- catch {
1702
- return null;
1703
- }
1704
- })();
1705
2187
  const notes = [...discovery.notes];
1706
2188
  if (operatorRange !== null) {
1707
2189
  // THE DISCLOSURE (D-08-19, threat T-08-25 — accepted, on this sentence).
@@ -1733,11 +2215,11 @@ function runVaultAudit(opts) {
1733
2215
  // the ground is a repairable fact rather than an unavoidable one. Stating
1734
2216
  // it is what makes the range record auditable on a checkout that has no
1735
2217
  // manifest to read — a fresh clone or CI runner where `.planning/` is
1736
- // gitignored, since only the executing checkout writes one.
2218
+ // gitignored, since only the executing checkout writes one (260915-ik2).
1737
2219
  notes.push(`phase range fell back to the base-branch form (${range}) because the phase-scoped range could not be ` +
1738
2220
  `resolved: ${fallbackReason}`);
1739
2221
  }
1740
- // The ladder's own notes: a trace-digest range's under-scope
2222
+ // The ladder's own notes (260915-ik2): a trace-digest range's under-scope
1741
2223
  // disclosure, or the statement that the digest rung was tried and found
1742
2224
  // nothing verifiable. Either is a limit on what the range establishes.
1743
2225
  for (const note of rangeNotes ?? [])
@@ -1776,7 +2258,43 @@ function runVaultAudit(opts) {
1776
2258
  mechanicallyChecked: vaultArmCounts.mechanicallyChecked,
1777
2259
  applicablePractices: vaultArmCounts.applicablePractices,
1778
2260
  evaluatedNothing: vaultArmCounts.evaluatedNothing,
1779
- recordedCensus,
2261
+ practices: entries,
2262
+ pairsEvaluated: 0,
2263
+ pairCap: MATCH_PAIR_CAP,
2264
+ budgetMs: MATCH_BUDGET_MS,
2265
+ filesConsidered: 0,
2266
+ filesCapped: false,
2267
+ notes,
2268
+ };
2269
+ }
2270
+ if (discovery.vaultStandardsOnly) {
2271
+ // Quick 261015-at3 (QD-06, Vince 2026-10-15): the client vault WAS read,
2272
+ // and it is a standards vault — INDEX.md at its root, no practice files —
2273
+ // read in practices mode. Discovery yields zero practices by construction,
2274
+ // and before this arm the audit compared nothing, warned, and did not
2275
+ // block. Placed after the vault-unreadable arm (an unread vault is the more
2276
+ // basic fact) and before the diff is read (nothing here needs it).
2277
+ const entries = [
2278
+ ...verdictsWithoutComparison(practices, AUDIT_FAIL_REASON.VAULT_STANDARDS_ONLY, `the client vault holds standards (INDEX.md) and no practice files, so this practice was never compared against ${range}`),
2279
+ ...unloadable,
2280
+ ];
2281
+ const armCounts = countVerdicts(entries);
2282
+ const lead = `Vault audit: the client vault at .planning/vault holds standards (INDEX.md) and no practice files, ` +
2283
+ `but vault.mode is practices, so no practice was compared against ${range} (source: ${rangeSource}) ` +
2284
+ `and nothing was verified. ${failingEntries(entries).length} practice check(s) are recorded as FAILED.`;
2285
+ return {
2286
+ status: 'warn',
2287
+ // EXPLICITLY true, for the vault-unreadable arm's reason verbatim: on this
2288
+ // arm `entries` is empty by construction, `blocksOnVerdicts([])` is false,
2289
+ // and a gate handed no practice layer verified nothing (G-1a).
2290
+ block: true,
2291
+ message: composeWithNotes(notes, [lead, ...failItemLines(entries), AUDIT_STANDARDS_ONLY_REPAIR_SENTENCE].join('\n')),
2292
+ range,
2293
+ rangeSource,
2294
+ rangeOutcome: classifyRangeOutcome({ phaseScoped: isPhaseScopedSource(rangeSource), filesConsidered: 0 }),
2295
+ mechanicallyChecked: armCounts.mechanicallyChecked,
2296
+ applicablePractices: armCounts.applicablePractices,
2297
+ evaluatedNothing: armCounts.evaluatedNothing,
1780
2298
  practices: entries,
1781
2299
  pairsEvaluated: 0,
1782
2300
  pairCap: MATCH_PAIR_CAP,
@@ -1808,7 +2326,6 @@ function runVaultAudit(opts) {
1808
2326
  mechanicallyChecked: diffArmCounts.mechanicallyChecked,
1809
2327
  applicablePractices: diffArmCounts.applicablePractices,
1810
2328
  evaluatedNothing: diffArmCounts.evaluatedNothing,
1811
- recordedCensus,
1812
2329
  practices: entries,
1813
2330
  pairsEvaluated: 0,
1814
2331
  pairCap: MATCH_PAIR_CAP,
@@ -1832,7 +2349,7 @@ function runVaultAudit(opts) {
1832
2349
  //
1833
2350
  // On the FALLBACK range that reasoning does not hold, because nothing
1834
2351
  // established that the range corresponds to this phase. The fallback is the
1835
- // FOURTH rung of `resolvePhaseRange`'s ladder: it is reached
2352
+ // FOURTH rung of `resolvePhaseRange`'s ladder (260915-ik2): it is reached
1836
2353
  // only when the phase has no usable manifest entry (no verifiable startCommit
1837
2354
  // or commit) AND no verifiable SUMMARY trace digest. With
1838
2355
  // `git.branching_strategy: none` — this project's own setting — `main...HEAD`
@@ -1865,7 +2382,7 @@ function runVaultAudit(opts) {
1865
2382
  }
1866
2383
  if (unverifiableEmptyRange) {
1867
2384
  // PREFIXED with the outcome code (08-04, D-08-17). `message` is composed
1868
- // from `notes`, and the gate runner (`loop run-gates`) prints `message` verbatim — so
2385
+ // from `notes`, and `check-query-gate.md` prints `message` verbatim — so
1869
2386
  // putting the code at the head of this sentence is what makes
1870
2387
  // `range_unresolvable` greppable in BOTH of the two fields the enforcer
1871
2388
  // reads, not only in the envelope field a workflow would have to parse.
@@ -1893,7 +2410,7 @@ function runVaultAudit(opts) {
1893
2410
  if (counts.evaluatedNothing) {
1894
2411
  // PREFIXED with the outcome token, following the `range_unresolvable`
1895
2412
  // precedent above: `message` is composed from `notes` and
1896
- // the gate runner (`loop run-gates`) prints `message` verbatim, so putting the token at
2413
+ // `check-query-gate.md` prints `message` verbatim, so putting the token at
1897
2414
  // the head of this sentence is what makes the fact greppable in BOTH of the
1898
2415
  // fields the enforcer reads, not only in an envelope field a workflow would
1899
2416
  // have to parse. Composed entirely from engine strings and integers — no
@@ -1902,7 +2419,7 @@ function runVaultAudit(opts) {
1902
2419
  notes.push(`${AUDIT_COVERAGE_OUTCOME.EVALUATED_NOTHING}: ${counts.mechanicallyChecked} of ` +
1903
2420
  `${counts.applicablePractices} applicable practice(s) were mechanically checked over ${range} ` +
1904
2421
  `(source: ${rangeSource}) — no check block was compared against any file, so this run verified ` +
1905
- `nothing mechanically and no passing claim can be drawn from it. ${renderRecordedCensusLine(recordedCensus)}`);
2422
+ `nothing mechanically and no passing claim can be drawn from it`);
1906
2423
  }
1907
2424
  return {
1908
2425
  // D-10-11: a run in which nothing was mechanically checked is `warn`, never
@@ -1933,7 +2450,6 @@ function runVaultAudit(opts) {
1933
2450
  mechanicallyChecked: counts.mechanicallyChecked,
1934
2451
  applicablePractices: counts.applicablePractices,
1935
2452
  evaluatedNothing: counts.evaluatedNothing,
1936
- recordedCensus,
1937
2453
  practices: audited,
1938
2454
  pairsEvaluated: evaluated.pairsEvaluated,
1939
2455
  pairCap: evaluated.pairCap,
@@ -1985,7 +2501,6 @@ function renderAuditReport(result) {
1985
2501
  // much was audited. The two sentences answer the same question and an
1986
2502
  // operator reads them together; counts only, never a percentage or a score.
1987
2503
  renderMechanicalCoverageLine(counts),
1988
- renderRecordedCensusLine(result.recordedCensus ?? null),
1989
2504
  ];
1990
2505
  for (const section of AUDIT_SECTIONS) {
1991
2506
  lines.push('');
@@ -2021,8 +2536,6 @@ module.exports = {
2021
2536
  // one the day either is reworded.
2022
2537
  AUDIT_COVERAGE_OUTCOME,
2023
2538
  renderMechanicalCoverageLine,
2024
- // The recorded census's one sentence (read through the ledger API).
2025
- renderRecordedCensusLine,
2026
2539
  // 10-06 (MECH-03, D-10-05): the ONE lead this module composes for an
2027
2540
  // `enforced-by:` claim. Exported so the tests — and any surface that must
2028
2541
  // prove it never says "verified by" — assert against the SHIPPED string.
@@ -2033,6 +2546,8 @@ module.exports = {
2033
2546
  AUDIT_OPERATOR_RANGE_SHAPE,
2034
2547
  AUDIT_OPERATOR_RANGE_MAX_LENGTH,
2035
2548
  isValidOperatorRange,
2549
+ // 260915-ik2: why a manifest writer wrote nothing.
2550
+ PHASE_MANIFEST_WRITE_REASON,
2036
2551
  AUDIT_FILE_READ_MAX,
2037
2552
  AUDIT_MESSAGE_PROBLEM_CAP,
2038
2553
  AUDIT_DIFF_FILE_CAP,
@@ -2043,6 +2558,8 @@ module.exports = {
2043
2558
  AUDIT_REPAIR_SENTENCE,
2044
2559
  AUDIT_DIFF_REPAIR_SENTENCE,
2045
2560
  AUDIT_VAULT_REPAIR_SENTENCE,
2561
+ // Quick 261015-at3: the standards-only arm's repair, same harvest rule.
2562
+ AUDIT_STANDARDS_ONLY_REPAIR_SENTENCE,
2046
2563
  // 08-04: the refused-operator-range arm's repair, exported on the same
2047
2564
  // harvest rule as the three above.
2048
2565
  AUDIT_OPERATOR_RANGE_REPAIR_SENTENCE,
@@ -2052,6 +2569,13 @@ module.exports = {
2052
2569
  // The diff scoping, exported separately from the envelope so a surface that
2053
2570
  // only needs to STATE the range never has to run an audit to learn it.
2054
2571
  resolvePhaseRange,
2572
+ // 16-03 (D-20): the manifest-presence half of the seal leaf's artifact predicate.
2573
+ hasPhaseManifestEntry,
2574
+ // 260915-ik2 (UAT-6-F1): the manifest writers `vault manifest start|record`
2575
+ // reaches, and their one-line operator rendering.
2576
+ recordPhaseStart,
2577
+ recordPhaseCommits,
2578
+ renderManifestResult,
2055
2579
  createBoundedFileReader,
2056
2580
  // The whole audit, and its human rendering.
2057
2581
  runVaultAudit,