@pareto4/pp-core 1.17.1 → 1.19.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 (199) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +5 -3
  3. package/agents/pp-code-fixer.md +9 -16
  4. package/agents/pp-code-reviewer.md +2 -78
  5. package/agents/pp-codebase-mapper.md +3 -0
  6. package/agents/pp-debugger.md +3 -2
  7. package/agents/pp-executor.md +25 -30
  8. package/agents/pp-integration-checker.md +1 -0
  9. package/agents/pp-phase-researcher.md +3 -1
  10. package/agents/pp-plan-checker.md +3 -0
  11. package/agents/pp-planner.md +16 -12
  12. package/agents/pp-research-synthesizer.md +1 -1
  13. package/agents/pp-ui-auditor.md +1 -0
  14. package/agents/pp-ui-researcher.md +3 -1
  15. package/agents/pp-vault-reviewer.md +43 -189
  16. package/agents/pp-verifier.md +10 -8
  17. package/commands/pp-add-tests.md +1 -1
  18. package/commands/pp-debug.md +1 -0
  19. package/commands/pp-init.md +4 -3
  20. package/commands/pp-ns-manage.md +1 -1
  21. package/commands/pp-ns-review.md +1 -1
  22. package/commands/pp-quick.md +3 -0
  23. package/commands/pp-review-backlog.md +2 -1
  24. package/commands/pp-vault-promote.md +7 -8
  25. package/commands/pp-vault-review.md +6 -7
  26. package/gemini-extension.json +1 -1
  27. package/package.json +3 -6
  28. package/pp-core/bin/lib/capability-registry.cjs +15 -357
  29. package/pp-core/bin/lib/capability-state.cjs +15 -79
  30. package/pp-core/bin/lib/capability-writer.cjs +2 -16
  31. package/pp-core/bin/lib/check-command-router.cjs +342 -2298
  32. package/pp-core/bin/lib/clusters.cjs +2 -2
  33. package/pp-core/bin/lib/commands.cjs +79 -4
  34. package/pp-core/bin/lib/compose-bindings.cjs +99 -16
  35. package/pp-core/bin/lib/config.cjs +3 -6
  36. package/pp-core/bin/lib/discovery-rollup.cjs +9 -6
  37. package/pp-core/bin/lib/discovery-screens.cjs +20 -2
  38. package/pp-core/bin/lib/discovery-sweep.cjs +12 -3
  39. package/pp-core/bin/lib/discovery-vault-check.cjs +37 -3
  40. package/pp-core/bin/lib/git-base-branch.cjs +10 -13
  41. package/pp-core/bin/lib/git-provider.cjs +5 -6
  42. package/pp-core/bin/lib/init-contract.cjs +55 -42
  43. package/pp-core/bin/lib/init-interactive.cjs +6 -20
  44. package/pp-core/bin/lib/io.cjs +2 -8
  45. package/pp-core/bin/lib/loop-resolver.cjs +8 -32
  46. package/pp-core/bin/lib/org-profile.cjs +16 -92
  47. package/pp-core/bin/lib/vault-command-router.cjs +174 -1206
  48. package/pp-core/bin/lib/vault-coordinates.cjs +4 -4
  49. package/pp-core/bin/lib/vault-index.cjs +44 -74
  50. package/pp-core/bin/lib/vault-seal.cjs +37 -122
  51. package/pp-core/bin/lib/vault-standards-promote.cjs +577 -0
  52. package/pp-core/bin/lib/vault-standards-review.cjs +293 -0
  53. package/pp-core/bin/lib/vault-standards.cjs +15 -16
  54. package/pp-core/bin/lib/vault-trace.cjs +40 -3016
  55. package/pp-core/bin/lib/vault-update.cjs +5 -26
  56. package/pp-core/bin/lib/vault.cjs +122 -316
  57. package/pp-core/bin/pp-tools.cjs +15 -36
  58. package/pp-core/org-profile.json +1 -1
  59. package/pp-core/references/autonomous-best-effort.md +4 -3
  60. package/pp-core/references/autonomous-lifecycle.md +1 -0
  61. package/pp-core/references/autonomous-smart-discuss.md +5 -1
  62. package/pp-core/references/discovery-id-conventions.md +35 -2
  63. package/pp-core/references/execute-mvp-tdd.md +3 -3
  64. package/pp-core/references/git-integration.md +57 -33
  65. package/pp-core/references/git-planning-commit.md +31 -9
  66. package/pp-core/references/phase-argument-parsing.md +1 -0
  67. package/pp-core/references/planner-gap-closure.md +1 -0
  68. package/pp-core/references/planner-revision.md +3 -1
  69. package/pp-core/references/planner-tdd.md +4 -4
  70. package/pp-core/references/planner-vault-standards.md +16 -25
  71. package/pp-core/references/planning-config.md +3 -3
  72. package/pp-core/references/shell-portability.md +28 -0
  73. package/pp-core/references/tdd.md +20 -16
  74. package/pp-core/references/worktree-path-safety.md +1 -0
  75. package/pp-core/templates/README.md +1 -1
  76. package/pp-core/templates/VAULT-REVIEW.md +24 -170
  77. package/pp-core/templates/milestone.md +5 -5
  78. package/pp-core/workflows/add-backlog.md +1 -1
  79. package/pp-core/workflows/add-tests.md +8 -5
  80. package/pp-core/workflows/add-todo.md +3 -1
  81. package/pp-core/workflows/ai-integration-phase.md +2 -1
  82. package/pp-core/workflows/audit-fix.md +7 -6
  83. package/pp-core/workflows/audit-milestone.md +1 -0
  84. package/pp-core/workflows/autonomous.md +6 -0
  85. package/pp-core/workflows/check-todos.md +2 -1
  86. package/pp-core/workflows/cleanup.md +3 -1
  87. package/pp-core/workflows/code-review-fix.md +15 -8
  88. package/pp-core/workflows/code-review.md +16 -58
  89. package/pp-core/workflows/complete-milestone.md +13 -3
  90. package/pp-core/workflows/debug.md +4 -2
  91. package/pp-core/workflows/diagnose-issues.md +1 -1
  92. package/pp-core/workflows/discovery-sprint/prototype.md +2 -2
  93. package/pp-core/workflows/discovery-sprint/session-13-gates.md +1 -1
  94. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +51 -7
  95. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +5 -3
  96. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +10 -8
  97. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +11 -2
  98. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +5 -5
  99. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +3 -3
  100. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +2 -2
  101. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +14 -2
  102. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +2 -2
  103. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +10 -10
  104. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +11 -8
  105. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +21 -18
  106. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +5 -3
  107. package/pp-core/workflows/discovery-sprint/templates/context-template.md +9 -3
  108. package/pp-core/workflows/discovery-sprint/vault-context.md +17 -12
  109. package/pp-core/workflows/discovery-sprint.md +11 -8
  110. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +1 -0
  111. package/pp-core/workflows/discuss-phase-assumptions.md +5 -3
  112. package/pp-core/workflows/discuss-phase.md +8 -3
  113. package/pp-core/workflows/docs-update.md +1 -0
  114. package/pp-core/workflows/eval-review.md +2 -1
  115. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +13 -14
  116. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -0
  117. package/pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md +5 -25
  118. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -0
  119. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +18 -6
  120. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +6 -109
  121. package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +1 -17
  122. package/pp-core/workflows/execute-phase.md +21 -12
  123. package/pp-core/workflows/execute-plan.md +24 -15
  124. package/pp-core/workflows/explore.md +1 -1
  125. package/pp-core/workflows/forensics.md +4 -2
  126. package/pp-core/workflows/health.md +1 -0
  127. package/pp-core/workflows/help/modes/full.md +1 -1
  128. package/pp-core/workflows/import.md +2 -1
  129. package/pp-core/workflows/ingest-docs.md +1 -0
  130. package/pp-core/workflows/init/steps/client-context-capture.md +1 -0
  131. package/pp-core/workflows/init/steps/github-provisioning.md +145 -0
  132. package/pp-core/workflows/init/steps/gitlab-provisioning.md +18 -4
  133. package/pp-core/workflows/init/steps/reachability-outcome.md +3 -1
  134. package/pp-core/workflows/init.md +56 -66
  135. package/pp-core/workflows/map-codebase.md +5 -1
  136. package/pp-core/workflows/milestone-summary.md +2 -1
  137. package/pp-core/workflows/mvp-phase.md +1 -0
  138. package/pp-core/workflows/new-milestone.md +9 -5
  139. package/pp-core/workflows/new-project.md +9 -8
  140. package/pp-core/workflows/new-workspace.md +1 -0
  141. package/pp-core/workflows/next.md +2 -1
  142. package/pp-core/workflows/pause-work.md +6 -4
  143. package/pp-core/workflows/plan-milestone-gaps.md +3 -1
  144. package/pp-core/workflows/plan-phase/steps/chunked-planning.md +1 -1
  145. package/pp-core/workflows/plan-phase.md +21 -6
  146. package/pp-core/workflows/plan-review-convergence.md +2 -0
  147. package/pp-core/workflows/plant-seed.md +4 -2
  148. package/pp-core/workflows/pr-branch.md +2 -0
  149. package/pp-core/workflows/progress.md +5 -0
  150. package/pp-core/workflows/quick.md +19 -12
  151. package/pp-core/workflows/reapply-patches.md +3 -0
  152. package/pp-core/workflows/remove-phase.md +1 -1
  153. package/pp-core/workflows/resume-project.md +3 -0
  154. package/pp-core/workflows/review.md +7 -1
  155. package/pp-core/workflows/secure-phase.md +2 -1
  156. package/pp-core/workflows/session-report.md +1 -0
  157. package/pp-core/workflows/ship.md +8 -1
  158. package/pp-core/workflows/sketch-wrap-up.md +1 -1
  159. package/pp-core/workflows/sketch.md +3 -2
  160. package/pp-core/workflows/spec-phase.md +2 -1
  161. package/pp-core/workflows/spike-wrap-up.md +1 -1
  162. package/pp-core/workflows/spike.md +4 -3
  163. package/pp-core/workflows/sync-skills.md +5 -2
  164. package/pp-core/workflows/thread.md +3 -2
  165. package/pp-core/workflows/transition.md +6 -0
  166. package/pp-core/workflows/ui-phase.md +3 -1
  167. package/pp-core/workflows/ui-review.md +2 -1
  168. package/pp-core/workflows/ultraplan-phase.md +1 -0
  169. package/pp-core/workflows/undo.md +14 -10
  170. package/pp-core/workflows/update.md +5 -0
  171. package/pp-core/workflows/validate-phase.md +9 -6
  172. package/pp-core/workflows/vault-review.md +65 -186
  173. package/pp-core/workflows/vault-write-back.md +78 -110
  174. package/pp-core/workflows/verify-phase.md +5 -0
  175. package/pp-core/workflows/verify-work.md +9 -2
  176. package/scripts/gen-capability-registry.cjs +12 -93
  177. package/scripts/lint-test-file-count.allowlist.json +1 -19
  178. package/scripts/lint-test-file-count.cjs +5 -6
  179. package/scripts/prompt-injection-scan.sh +1 -4
  180. package/scripts/sync-runtime-launcher.cjs +115 -5
  181. package/pp-core/bin/lib/practices-review-check.cjs +0 -944
  182. package/pp-core/bin/lib/vault-audit.cjs +0 -2594
  183. package/pp-core/bin/lib/vault-mode.cjs +0 -235
  184. package/pp-core/bin/lib/vault-practices.cjs +0 -2057
  185. package/pp-core/bin/lib/vault-review.cjs +0 -2102
  186. package/pp-core/bin/lib/vault-write-back.cjs +0 -905
  187. package/pp-core/references/planner-vault-practices.md +0 -95
  188. package/pp-core/references/practice-arithmetic-contract.md +0 -121
  189. package/pp-core/references/verifier-practice-verdicts.md +0 -263
  190. package/pp-core/templates/practices/README.md +0 -248
  191. package/pp-core/templates/practices/error-handling.md +0 -18
  192. package/pp-core/templates/practices/minimal-comments.md +0 -14
  193. package/pp-core/templates/practices/naming-convention.md +0 -18
  194. package/pp-core/templates/practices/practice-template.md +0 -23
  195. package/pp-core/workflows/execute-phase/steps/verifier-practice-payload.md +0 -165
  196. package/scripts/gen-practice-arithmetic-contract.cjs +0 -412
  197. package/scripts/practice-arithmetic-contract.allow.json +0 -35
  198. package/scripts/record-vault-mode-golden.cjs +0 -89
  199. package/scripts/vault-calibrate.cjs +0 -996
@@ -39,14 +39,14 @@ const node_path_1 = __importDefault(require("node:path"));
39
39
  // eslint-disable-next-line @typescript-eslint/no-require-imports
40
40
  const childProcess = require("node:child_process");
41
41
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
42
- // CR-04 (03-11): the trace render arm resolves its containment boundary the same
43
- // way `cmdVaultTrace` in src/check-command-router.cts does. project-root.cjs is a
42
+ // CR-04 (03-11): this router's path arms resolve their containment boundary the
43
+ // same way `cmdVaultTrace` in src/check-command-router.cts does. project-root.cjs is a
44
44
  // leaf (node builtins only), so this adds no cycle.
45
45
  // eslint-disable-next-line @typescript-eslint/no-require-imports
46
46
  const projectRootMod = require("./project-root.cjs");
47
47
  const { findProjectRoot } = projectRootMod;
48
48
  // 15-05: the plan-identifier form moved into the vault-standards leaf (one
49
- // definition shared by the trace verbs, the standards verbs and the gate).
49
+ // definition shared by the standards verbs and the gate).
50
50
  // That leaf requires only vault-index, which imports nothing, so no cycle.
51
51
  // eslint-disable-next-line @typescript-eslint/no-require-imports
52
52
  const vaultStandardsLeaf = require("./vault-standards.cjs");
@@ -68,18 +68,7 @@ const VAULT_SUBCOMMANDS = [
68
68
  'status',
69
69
  'probe',
70
70
  'summary',
71
- 'practices',
72
71
  'org-profile',
73
- 'trace',
74
- // 05-06 (CHECK-03): the mechanical practice audit. Added HERE and nowhere
75
- // else — the header comment above, the dispatch chain and the
76
- // unknown-subcommand message all derive from this list, which is exactly the
77
- // three-place drift 02-02 removed.
78
- 'audit',
79
- // 260915-ik2 (UAT-6-F1): the phase-manifest writer the execute-phase step
80
- // files call — `start` at execute:pre, `record` at verify:post. Added HERE
81
- // and nowhere else, for the same three-place reason as `audit` above.
82
- 'manifest',
83
72
  // PBI 74316 (fork-only): move the project to another client vault version.
84
73
  'update',
85
74
  // 15-07 (D-06, D-13): the standards record verbs, `block` and `consulted`.
@@ -93,28 +82,11 @@ const VAULT_SUBCOMMANDS = [
93
82
  'door-record',
94
83
  ];
95
84
  /**
96
- * The two trace stages, declared once for the same reason the subcommand
97
- * vocabulary is: the `--stage` validation and the operator-facing message that
98
- * names the permitted values are both DERIVED from this list, so they cannot
99
- * disagree. The values themselves are the frozen contract's
100
- * (`docs/vault-trace-format.md` § The freeze contract) — this list restates the
101
- * two stage NAMES the CLI accepts, and nothing else about the schema.
85
+ * The standards verbs (15-07), same discipline: the verb check and the message
86
+ * naming the permitted verbs both derive from this list. `promote` writes to
87
+ * the client vault rather than a record, and answers its own refusals.
102
88
  */
103
- const TRACE_STAGES = ['plan', 'execute'];
104
- /**
105
- * The trace verbs, same discipline (Phase 03-05).
106
- */
107
- const TRACE_VERBS = ['scaffold', 'render'];
108
- /**
109
- * The manifest verbs, same discipline (260915-ik2): the verb check and the
110
- * operator-facing message that names the permitted verbs both derive from it.
111
- */
112
- const MANIFEST_VERBS = ['start', 'record'];
113
- /**
114
- * The standards record verbs (15-07), same discipline: the verb check and the
115
- * message naming the permitted verbs both derive from this list.
116
- */
117
- const STANDARDS_VERBS = ['block', 'consulted'];
89
+ const STANDARDS_VERBS = ['block', 'consulted', 'promote'];
118
90
  /**
119
91
  * The anchor verbs (16-06), same discipline: the verb check and the message
120
92
  * naming the permitted verbs both derive from this list.
@@ -133,10 +105,10 @@ const DOOR_RECORD_FLAGS = ['--door', '--artifact', '--phase'];
133
105
  */
134
106
  const STANDARDS_PLAN_READ_BYTES = 1024 * 1024;
135
107
  /**
136
- * The plan-identifier shape `vault trace scaffold --plan` accepts (04-14,
108
+ * The plan-identifier shape `vault standards block --plan` accepts (04-14,
137
109
  * TGATE-01, CR-03). Defined in `src/vault-standards.cts` since 15-05 and
138
110
  * re-exported from here under the same name; the rationale stays here, beside
139
- * the verb that first needed it.
111
+ * the verb that uses it.
140
112
  *
141
113
  * ONE FORM, READ AND WRITTEN. `filenamePlanId` in `src/check-command-router.cts`
142
114
  * extracts `<this form>-PLAN.md` / `-SUMMARY.md` from an artifact's basename,
@@ -166,19 +138,12 @@ const STANDARDS_PLAN_READ_BYTES = 1024 * 1024;
166
138
  */
167
139
  const SCAFFOLD_PLAN_ID_RE = vaultStandardsLeaf.SCAFFOLD_PLAN_ID_RE;
168
140
  /**
169
- * The required form, in ONE wording, stated in exactly three places: this
170
- * rejection message, `pp-core/references/planner-vault-practices.md`, and the
171
- * planner-facing inline instruction in
172
- * `capabilities/vault-practices/capability.json` (04-14 T2).
173
- *
174
- * It is a constant rather than three sentences because a form stated three ways
175
- * is three forms (threat T-4-55). The test asserts the sentence extracted
176
- * independently from each document is EQUAL to the other's and to this value,
177
- * rather than asserting each document merely mentions a form.
141
+ * The required form, in ONE wording: the `vault standards block` rejection
142
+ * message (04-14 T2). A constant so the refusal and the test that pins it read
143
+ * one sentence (threat T-4-55).
178
144
  *
179
145
  * ASCII only, and no backticks: the same bytes have to survive a JSON string, a
180
- * markdown paragraph and a terminal, and a backtick would make the markdown
181
- * copy differ from the other two.
146
+ * markdown paragraph and a terminal.
182
147
  */
183
148
  const SCAFFOLD_PLAN_ID_FORM = "The plan identifier must be the plan's phase-and-number identifier, exactly the " +
184
149
  'prefix of its artifact filename (for example 04-02), not a bare plan number, a task ' +
@@ -187,23 +152,12 @@ const SCAFFOLD_PLAN_ID_FORM = "The plan identifier must be the plan's phase-and-
187
152
  * The quick-task half of the form (16-05, D-29), appended to both rejection
188
153
  * messages after `SCAFFOLD_PLAN_ID_FORM`.
189
154
  *
190
- * A SECOND constant rather than an edit to the first, and argued: the first is
191
- * stated verbatim in three places, and two of them are agent context the
192
- * golden set records byte for byte (the `vault-practices` plan:pre inline
193
- * instruction in three cells) or that sits under an exhausted size budget
194
- * (`agents/pp-executor.md`). Rewording it would re-record golden rows Phase 16
195
- * may not move. Its sentence is also already true of a quick task — "exactly
196
- * the prefix of its artifact filename" — so only the refusal, the one surface
197
- * a caller reads after being told no, needs to name the quick form.
155
+ * A second constant rather than an edit to the first: the first sentence is
156
+ * already true of a quick task — "exactly the prefix of its artifact filename"
157
+ * — so only the refusal, the one surface a caller reads after being told no,
158
+ * needs to name the quick form.
198
159
  */
199
160
  const SCAFFOLD_QUICK_ID_FORM = 'A quick task uses its quick-task id instead, the same filename prefix (for example 261011-8yt).';
200
- /**
201
- * Bound on the bounded prefix read the `trace render` arm performs. The trace
202
- * lives in the artifact's FRONTMATTER, so a head read is not a compromise — it
203
- * is the exact region the parser needs, and a SUMMARY.md body of any size can
204
- * never make this arm read more.
205
- */
206
- const TRACE_ARTIFACT_READ_BYTES = 262_144;
207
161
  /** Repo-relative artifact paths, long enough for a workstream phase tree. */
208
162
  const TRACE_REFUSAL_PATH_CAP = 200;
209
163
  /**
@@ -227,103 +181,20 @@ function boundedPath(value) {
227
181
  : collapsed;
228
182
  }
229
183
  /**
230
- * The gate an override recorded by the scaffold arm suppresses (04-06, SC4).
184
+ * The gate an override recorded by `vault standards block` suppresses (04-06, SC4).
231
185
  *
232
186
  * `vault.status` is not a name invented here: it is the `check.query` the
233
187
  * blocking vault gate is declared under at both write points
234
- * (`capabilities/vault/capability.json`), and it is the value the freeze
235
- * contract's own worked example carries (`docs/vault-trace-format.md`
236
- * § The override object). Declared once so the emitted record and the
237
- * documented example cannot drift apart. Its one owner is the standards leaf
188
+ * (`capabilities/vault/capability.json`), and it is the gate the override
189
+ * line names (`docs/vault-standards-mode.md` § Gates, "The override").
190
+ * Declared once so the emitted record and the documented contract cannot
191
+ * drift apart. Its one owner is the standards leaf
238
192
  * (15 D-35), because the standards judgement accepts an override line only
239
193
  * under this gate name, and a second literal could drift from the first.
240
194
  */
241
195
  const TRACE_OVERRIDE_GATE = vaultStandardsLeaf.STANDARDS_OVERRIDE_GATE;
242
- /**
243
- * A `gitCommit` digest: lowercase hex at sha1 or sha256 length. 16-06 (D-35)
244
- * re-binds it to the seal leaf's `ANCHOR_SHA_RE`: a per-file digest and a
245
- * record's start/end anchors are the same shape, so they have one owner. The
246
- * local name stays, as an alias, so this file's readers keep their wording.
247
- */
248
- const GIT_COMMIT_DIGEST_RE = vaultSeal.ANCHOR_SHA_RE;
249
196
  // Default CoreModule implementation. _core seam overrides this for test injection.
250
197
  const _defaultCore = { output: io.output };
251
- function tracePracticesActive(cwd) {
252
- // eslint-disable-next-line @typescript-eslint/no-require-imports
253
- const { isPracticesGateActive } = require('./vault.cjs');
254
- return isPracticesGateActive(cwd);
255
- }
256
- /**
257
- * 16-09 (SEAL-02, D-39) — the `vault audit` verb's answer when the practices
258
- * switch is off, or `null` for "print nothing" (the pre-16-09 behaviour).
259
- *
260
- * It asks what `check vault.audit` asks, in the gate's order (D-25), through
261
- * the same readers: bindings first (no `.platform-bindings.json` never holds
262
- * records, D-37/D-42); standards mode active answers nothing (the C-1 cell);
263
- * the phase argument must resolve (the same `resolveTracePhaseDir`, so an
264
- * argument error still prints nothing, T-03-43); then the leaf's
265
- * `collectRecordedArtifacts` and `disabledWithArtifactsVerdict`, whose message
266
- * is the gate's message byte for byte. Which switch is off is decided by the
267
- * gate readers, never a raw config read. It blocks nothing itself: an operator
268
- * surface, exit 0. There is no override.
269
- *
270
- * From the REAL leaf, not the `_vault` seam: a governance decision must not be
271
- * switchable by a test mock (the `tracePracticesActive` rule above).
272
- */
273
- function disabledWithArtifactsRefusal(cwd, args) {
274
- // eslint-disable-next-line @typescript-eslint/no-require-imports
275
- const v = require('./vault.cjs');
276
- let root;
277
- try {
278
- root = findProjectRoot(cwd) || cwd;
279
- }
280
- catch {
281
- root = cwd;
282
- }
283
- if (!v.hasPlatformBindings(root))
284
- return null;
285
- if (v.isStandardsModeActive(cwd))
286
- return null;
287
- // The audit arm's own positional rule (below): the first token that is not a
288
- // flag and not the `--range` value.
289
- const auditArgs = args.slice(2);
290
- const rangeAt = auditArgs.indexOf('--range');
291
- const phaseArg = auditArgs.find((a, i) => !a.startsWith('--') && !(rangeAt !== -1 && i === rangeAt + 1));
292
- if (phaseArg === undefined)
293
- return null;
294
- const phase = v.resolveTracePhaseDir(root, cwd, phaseArg);
295
- if (!phase)
296
- return null;
297
- const whichSwitch = v.isVaultGateActive(root) ? 'vault.practices_enabled' : 'vault.enabled';
298
- let mode;
299
- if (whichSwitch === 'vault.practices_enabled') {
300
- const m = v.vaultModeVerdict(cwd);
301
- if (m.active && m.ok === true && m.mode === 'practices')
302
- mode = 'practices';
303
- }
304
- const verdict = vaultSeal.disabledWithArtifactsVerdict(vaultSeal.collectRecordedArtifacts(phase.abs, { root }), {
305
- switch: whichSwitch,
306
- ...(mode ? { mode } : {}),
307
- phaseRel: phase.rel,
308
- });
309
- if (!verdict)
310
- return null;
311
- return { status: 'disabled_with_artifacts', block: true, present: true, ...verdict };
312
- }
313
- /**
314
- * The value token immediately following each of `flags`, by index, so a
315
- * flag's value is never mistaken for a positional argument. Mirrors the
316
- * `--expect` handling in the `practices promote` arm.
317
- */
318
- function flagValueIndices(argv, flags) {
319
- const indices = new Set();
320
- for (const flag of flags) {
321
- const i = argv.indexOf(flag);
322
- if (i !== -1)
323
- indices.add(i + 1);
324
- }
325
- return indices;
326
- }
327
198
  /**
328
199
  * Render a short plain-text `vault org-profile` report from its structured
329
200
  * shape — one line naming the pointer, one naming the resolution, a
@@ -387,73 +258,37 @@ function renderOrgProfileReport(report) {
387
258
  return lines.join('\n');
388
259
  }
389
260
  // ─── Implementation ───────────────────────────────────────────────────────────
390
- function routeVaultCommand({ args, cwd, raw, error, _vault, _core, _practices, _exec, _writeBack, _orgProfile, _coordinates, _provider, _trace, _audit, _update, }) {
261
+ function routeVaultCommand({ args, cwd, raw, error, _vault, _core, _exec, _orgProfile, _coordinates, _provider, _update, }) {
391
262
  // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
392
263
  const vault = _vault ?? require('./vault.cjs');
393
264
  const c = _core ?? _defaultCore;
394
265
  const subcommand = args[1];
395
- // 13-06 (D-08, D-22): an unrecognised `vault.mode` on a project whose vault
396
- // gate exists refuses EVERY known subcommand, here, before dispatch. One owner
397
- // for ten arms rather than ten copies that a new arm could forget; the test
398
- // `D-15 sweep` in tests/vault-mode.test.cjs fails if the chain below and
399
- // VAULT_SUBCOMMANDS ever disagree, so no arm can dodge this guard.
400
- // - Keyed on VAULT_SUBCOMMANDS: an unknown subcommand never reads the mode
401
- // and keeps its own message below.
402
- // - The read is a config read; its one possible spawn is loadConfig's memoised, fixed-argument
403
- // git check-ignore, so T-24-08b's typo never reaches the `--changed` git call. On an invalid-mode
404
- // project this refusal now precedes an arm's unknown-flag error.
405
- // - `vaultModeVerdict` answers `{ active: false }` without reading the mode
406
- // when the gate is off, so a vault-off project is byte-identical (golden).
407
- // - From the REAL leaf, not the `_vault` seam, like `isInsideDir` and
408
- // `resolveTracePhaseDir` below: a governance decision must not be
409
- // switchable by a test mock.
410
- // - The message is the leaf's fixed text: it names both allowed values and
411
- // never echoes the configured one (T-13-03).
412
- // - Quick 261015-ect (QD-11): only where `.platform-bindings.json` exists. A
413
- // project without it never reads the mode (T-16-13): admission's own step
414
- // 1, read the same way, and the order every `check` vault handler keeps
415
- // (bindings-inert first, D-22). The 1.16.0 release note stated the old
416
- // order as measured; this reverses it, and closes `/pp-fast`'s backstop,
417
- // which asks `vault admission` with no bindings probe of its own.
266
+ // From the REAL leaf, not the `_vault` seam: a governance answer must not be
267
+ // switchable by a test mock.
418
268
  // eslint-disable-next-line @typescript-eslint/no-require-imports
419
- const modeSeam = require('./vault.cjs');
420
- if (VAULT_SUBCOMMANDS.includes(subcommand) && modeSeam.hasPlatformBindings(cwd)) {
421
- const modeVerdict = modeSeam.vaultModeVerdict(cwd);
422
- if (modeVerdict.active && modeVerdict.ok === false) {
423
- // eslint-disable-next-line @typescript-eslint/no-require-imports
424
- const leaf = require('./vault-mode.cjs');
425
- // 13-REVIEW WR-01: a resolver failure is refused like an invalid value,
426
- // under its own typed reason.
427
- const reason = modeVerdict.reason ?? '';
428
- error(leaf.vaultModeRefusalMessage(reason), reason === leaf.VAULT_MODE_REASON.UNRESOLVED ? ERROR_REASON.VAULT_MODE_UNRESOLVED : ERROR_REASON.VAULT_MODE_INVALID);
429
- return;
430
- }
431
- }
269
+ const indexSeam = require('./vault.cjs');
432
270
  if (subcommand === 'fetch') {
433
271
  c.output(vault.vaultFetch(cwd), raw);
434
272
  }
435
273
  else if (subcommand === 'status') {
436
- // 13-06 (D-10): `mode: standards` LAST, in standards mode only; the same
437
- // object otherwise, so a practices-mode envelope gains no key (golden).
438
274
  // 14-05 (D-11/D-14): the same five flat fields `check vault.status` carries,
439
275
  // from the one state function, so the report and the gate cannot tell
440
- // different stories (UAT F6). From the REAL module (`modeSeam`), not the
276
+ // different stories (UAT F6). From the REAL module (`indexSeam`), not the
441
277
  // injectable `vault` seam: a governance answer must not be switchable by a
442
278
  // test mock (13-06 precedent). No `block` and no `reason`: this is a report.
443
- // The state is null outside standards mode, so practices gains no key.
279
+ // The state is null with the vault off, so the envelope gains no key.
444
280
  const base = vault.vaultStatus(cwd);
445
- const indexState = modeSeam.computeVaultIndexState(cwd);
281
+ const indexState = indexSeam.computeVaultIndexState(cwd);
446
282
  const withIndex = indexState !== null && base['present'] === true
447
- ? { ...base, ...modeSeam.vaultIndexEnvelopeFields(indexState) }
283
+ ? { ...base, ...indexSeam.vaultIndexEnvelopeFields(indexState) }
448
284
  : base;
449
- c.output(modeSeam.withVaultMode(withIndex, cwd), raw);
285
+ c.output(withIndex, raw);
450
286
  }
451
287
  else if (subcommand === 'probe') {
452
288
  // STRICT-02: pure reachability for the client vault, the shared fallback and
453
289
  // the platform repo — no clone, no cache mutation.
454
290
  //
455
- // T-02-07: reject any unrecognized `--` token BEFORE anything spawns, copying
456
- // the `practices match` posture below verbatim. A typo must never reach a
291
+ // T-02-07: reject any unrecognized `--` token BEFORE anything spawns. A typo must never reach a
457
292
  // subprocess. The host (pp-core/bin/pp-tools.cjs) already splices out its own
458
293
  // global flags before dispatch, which is why the recognized set is one wide.
459
294
  const RECOGNIZED_PROBE_FLAGS = new Set(['--raw']);
@@ -487,150 +322,10 @@ function routeVaultCommand({ args, cwd, raw, error, _vault, _core, _practices, _
487
322
  c.output(summary.index, false);
488
323
  }
489
324
  }
490
- else if (subcommand === 'practices') {
491
- const verb = args[2];
492
- if (verb === 'match') {
493
- // Quick 261015-at3 (QD-02, todo practices-record-verbs-print-without-
494
- // bindings): unless `vault admission` answers `on` this arm prints
495
- // nothing, FIRST — before the flag guard and before the mode read. It was
496
- // gated by no switch at all (measured 261014-na1: it printed a block on
497
- // `vault.enabled: false`), so a record no gate reads could be produced on
498
- // a project whose `check ship.practices` answers inert.
499
- // eslint-disable-next-line @typescript-eslint/no-require-imports
500
- const owner = require('./vault.cjs');
501
- if (owner.vaultProducerAdmissionVerdict(cwd).state !== 'on')
502
- return;
503
- // Quick 261015-ect (QD-14, at3 verifier W-1): in practices mode, silent
504
- // where `check ship.practices` answers inert — the practices switch off —
505
- // as the trace, audit and manifest arms already are: a block no gate
506
- // reads is not produced. Standards mode keeps D-22's answer below on
507
- // either switch, because the load-gate card relays that line as its
508
- // standards-first clause (15-11) and it records no practice.
509
- if (!modeSeam.isStandardsModeActive(cwd) && !tracePracticesActive(cwd))
510
- return;
511
- // T-24-08b: reject any unrecognized `--`-prefixed argument BEFORE the
512
- // `--changed` git call (so a typo never spawns the diff) instead
513
- // of silently filtering it out of the positional file list. Matched on
514
- // the WHOLE token so `--files=…` is caught by the same check as
515
- // `--files value`. DECISION (recorded, not deferred): `--files=` is
516
- // NOT implemented as a comma-splitting alias here, unlike
517
- // `/pp-code-review`'s `--files=a,b` (pp-core/workflows/code-review.md).
518
- // The auditor marked the alias "optional and secondary"; the required
519
- // behavior is loud failure, and the error message below names the
520
- // supported forms instead. The host (pp-core/bin/pp-tools.cjs) already
521
- // splices out its own global flags (--json-errors/--cwd/--ws/--raw/
522
- // --pick/--default) before dispatch, which is why the recognized set
523
- // here is only two flags wide.
524
- const RECOGNIZED_MATCH_FLAGS = new Set(['--changed', '--raw', '--block']);
525
- const unrecognizedFlag = args.slice(3).find((a) => a.startsWith('--') && !RECOGNIZED_MATCH_FLAGS.has(a));
526
- if (unrecognizedFlag !== undefined) {
527
- error(`Unrecognized flag "${unrecognizedFlag}" for vault practices match. Supported: positional file paths, --changed, --raw, or --block.`, ERROR_REASON.SDK_FAIL_FAST);
528
- return;
529
- }
530
- // 13-07 (D-12, PARK-01): in standards mode the matcher has no practice
531
- // layer to read, so this arm answers not-applicable — one line naming the
532
- // mode, or `--raw`'s envelope with NO match-set key, exit 0. AFTER the
533
- // flag guard (a typo still fails first, T-24-08b) and BEFORE the
534
- // `--changed` git call (a parked path spawns no diff, T-13-12). `--block`
535
- // is non-raw, so it prints the same line and no `<vault_practices>` block.
536
- // There is deliberately NO activation guard on this branch: the arm
537
- // answers not-applicable whenever the vault gate is active, even with
538
- // `vault.practices_enabled` false (D-22; QD-14 above gates practices mode
539
- // only). The text is the leaf's.
540
- if (modeSeam.isStandardsModeActive(cwd)) {
541
- // eslint-disable-next-line @typescript-eslint/no-require-imports
542
- const { notApplicableMatchEnvelope } = require('./vault-mode.cjs');
543
- const naMatch = notApplicableMatchEnvelope();
544
- c.output(naMatch, true, raw ? JSON.stringify(naMatch) : naMatch.message);
545
- return;
546
- }
547
- // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
548
- const practices = _practices ?? require('./vault-practices.cjs');
549
- const runGit = _exec ?? shell_command_projection_cjs_1.execGitLocal;
550
- let files;
551
- if (args.includes('--changed')) {
552
- const gitResult = runGit(['diff', '--name-only', 'HEAD'], { cwd });
553
- if (gitResult.exitCode !== 0) {
554
- error('Could not determine changed files: git diff failed (WR-02)', ERROR_REASON.SDK_FAIL_FAST);
555
- return;
556
- }
557
- files = gitResult.stdout.split('\n').map((f) => f.trim()).filter((f) => f.length > 0);
558
- }
559
- else {
560
- files = args.slice(3).filter((a) => !a.startsWith('--'));
561
- }
562
- const result = practices.matchPractices({ cwd, files });
563
- if (args.includes('--block')) {
564
- // T-25-08: emit the finished <vault_practices> block so the planner
565
- // embeds it VERBATIM. The planner never renames a field or applies a
566
- // fence itself — that transformation used to be prose-only, which is
567
- // what left the phase's prompt-injection mitigation unenforced.
568
- c.output(result, true, practices.renderPracticesBlock(result));
569
- }
570
- else if (raw) {
571
- c.output(result, true, JSON.stringify(result));
572
- }
573
- else {
574
- c.output(result, true, practices.renderPracticesReport(result));
575
- }
576
- }
577
- else if (verb === 'promote') {
578
- // T-29-16: reject any unrecognized `--`-prefixed argument BEFORE the
579
- // lazy require of the write-back module and before any subprocess can
580
- // spawn — mirrors the `match` branch's T-24-08b guard exactly. Matched
581
- // on the WHOLE token so `--name=value` is caught by the same check as
582
- // `--name value`. The recognized set holds exactly three flags — no
583
- // flag that disables, bypasses, or relaxes any guard exists.
584
- const RECOGNIZED_PROMOTE_FLAGS = new Set(['--confirmed', '--expect', '--raw']);
585
- const promoteArgs = args.slice(3);
586
- const unrecognizedPromoteFlag = promoteArgs.find((a) => a.startsWith('--') && !RECOGNIZED_PROMOTE_FLAGS.has(a));
587
- if (unrecognizedPromoteFlag !== undefined) {
588
- error(`Unrecognized flag "${unrecognizedPromoteFlag}" for vault practices promote. Supported: --confirmed, --expect <token>, --raw.`, ERROR_REASON.SDK_FAIL_FAST);
589
- return;
590
- }
591
- // Positional file path: the first token that is not `--`-prefixed and
592
- // is not the value immediately following `--expect`.
593
- const expectFlagIdx = promoteArgs.indexOf('--expect');
594
- const expectValueIdx = expectFlagIdx !== -1 ? expectFlagIdx + 1 : -1;
595
- const filePath = promoteArgs.find((a, i) => !a.startsWith('--') && i !== expectValueIdx);
596
- if (filePath === undefined) {
597
- error('No practice file was given. Usage: vault practices promote <file> [--confirmed --expect <token>] [--raw].', ERROR_REASON.SDK_MISSING_ARG);
598
- return;
599
- }
600
- const confirmed = promoteArgs.includes('--confirmed');
601
- const expect = expectValueIdx !== -1 ? promoteArgs[expectValueIdx] : undefined;
602
- if (confirmed && (expect === undefined || expect.startsWith('--'))) {
603
- error('This practice was not pushed: the confirmation is missing the preview token it needs. ' +
604
- 'Run the promotion without confirming first, then pass the token it prints.', ERROR_REASON.VAULT_PROMOTE_CONCURRENT_EDIT);
605
- return;
606
- }
607
- // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
608
- const writeBack = _writeBack ?? require('./vault-write-back.cjs');
609
- const verdict = writeBack.promotePractice({ cwd, filePath, confirmed, expect });
610
- // Emit the structured verdict BEFORE any error call — a refusal still
611
- // carries its verdict to stdout. This ordering is the whole point of
612
- // the uniform D-16 refusal contract and is what lets a wrapper
613
- // workflow render results without scraping prose.
614
- if (raw) {
615
- c.output(verdict, true, JSON.stringify(verdict));
616
- }
617
- else {
618
- c.output(verdict, true, writeBack.renderPromoteReport(verdict));
619
- }
620
- if (verdict.status === 'refused') {
621
- // An action command's refusal must be observable in the exit code —
622
- // do not adopt the ship gate's always-exit-zero convention here.
623
- error(verdict.message ?? 'This practice was not promoted.', verdict.reason ?? ERROR_REASON.UNKNOWN);
624
- }
625
- }
626
- else {
627
- error('Unknown practices subcommand. Available: match, promote', ERROR_REASON.SDK_UNKNOWN_COMMAND);
628
- }
629
- }
630
325
  else if (subcommand === 'org-profile') {
631
326
  // T-33-13: reject any unrecognized `--`-prefixed argument BEFORE any
632
- // lazy require or resolution work — mirrors the `practices match`
633
- // T-24-08b guard exactly, so a typo can never trigger filesystem work.
327
+ // lazy require or resolution work (T-24-08b), so a typo can never trigger
328
+ // filesystem work.
634
329
  const RECOGNIZED_ORG_PROFILE_FLAGS = new Set(['--raw']);
635
330
  const unrecognizedOrgProfileFlag = args
636
331
  .slice(2)
@@ -645,8 +340,6 @@ function routeVaultCommand({ args, cwd, raw, error, _vault, _core, _practices, _
645
340
  const coordinates = _coordinates ?? require('./vault-coordinates.cjs');
646
341
  // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
647
342
  const providerModule = _provider ?? require('./git-provider.cjs');
648
- // eslint-disable-next-line @typescript-eslint/no-require-imports
649
- const { VAULT_MODE_CONFIG_KEY: vaultModeKey } = require('./vault-mode.cjs');
650
343
  const resolution = orgProfile.resolveOrgProfile(cwd);
651
344
  // Resolve the EFFECTIVE values by calling the real resolvers, so the
652
345
  // report shows what actually won rather than what the profile offered.
@@ -677,14 +370,6 @@ function routeVaultCommand({ args, cwd, raw, error, _vault, _core, _practices, _
677
370
  // report cannot say the profile lost while the gate enforces it. It is
678
371
  // the policy only; with `vault.enabled` off no gate exists to apply it.
679
372
  [orgProfile.VAULT_REQUIRED_CONFIG_KEY]: String(orgProfile.resolveVaultRequired(cwd).required),
680
- // An invalid PROJECT mode reads as the typed reason
681
- // (`vault_mode_invalid`), never the value: the project's invalid value
682
- // did win over the profile, and the report says so without repeating
683
- // operator text (T-13-03).
684
- [vaultModeKey]: (() => {
685
- const r = orgProfile.resolveVaultMode(cwd);
686
- return r.ok ? r.mode : r.reason;
687
- })(),
688
373
  };
689
374
  // Build `effective`'s key list from ORG_PROFILE_ALLOWLIST rather than a
690
375
  // fresh literal array, so a future allowlist change surfaces here
@@ -741,615 +426,6 @@ function routeVaultCommand({ args, cwd, raw, error, _vault, _core, _practices, _
741
426
  c.output(report, true, renderOrgProfileReport(report));
742
427
  }
743
428
  }
744
- else if (subcommand === 'trace') {
745
- // ── The practice-trace producer (Phase 03-05, TRACE-01/02/03) ──
746
- //
747
- // D-01's split, mechanised: the CLI owns everything that must be PROVABLE
748
- // — the pinned `ref`, the `resolvedSha` and the considered practice-ID set
749
- // — and the agent writes only the judgement half (`status` + `reason`).
750
- // "Provable" cannot rest on an agent transcribing a forty-character SHA
751
- // correctly, and a well-formedness gate by definition cannot catch a
752
- // wrong-but-well-formed SHA.
753
- //
754
- // BOTH arms below are pass-throughs to `./vault-trace.cjs`. They collect
755
- // arguments, call through, and emit. They hold NO envelope logic, NO glob
756
- // pass and NO second read of the manifest: every provable field is the
757
- // leaf module's business, and duplicating any of it here would create a
758
- // second place where the trace can disagree with the gate that validates
759
- // it.
760
- //
761
- // NEITHER ARM WRITES A FILE, and that is a requirement rather than a
762
- // convenience not yet added. The producer emits text for an agent to embed
763
- // in the single write it is already performing. A CLI that patched a
764
- // plan's frontmatter would have to round-trip it through the shared
765
- // serializer, which this repository documents as "a deliberately lossy
766
- // serializer — it cannot faithfully re-emit nested object-list items" —
767
- // so the same write that added the trace would destroy the plan's own
768
- // must-haves block (T-03-23). If a direct write is ever wanted it must be a
769
- // raw text splice on the delimited region, and that is not this phase's
770
- // business. Do not add the convenience.
771
- //
772
- // ORDERING, and why it is this way round: the sub-verb check comes first,
773
- // so a mistyped sub-verb is refused with the usage error, bindings or not,
774
- // and produces no record (quick 261015-ect, QD-12 — measured and pinned).
775
- // Then the admission verdict and the capability gate (quick 261015-at3 put
776
- // the admission line ahead of the gate), ahead of the flag allowlist. The
777
- // two orders both satisfy T-03-25 (nothing is required and nothing spawns
778
- // before an unknown flag is rejected), because the gate is a bounded config
779
- // read, not a subprocess. Only this order also satisfies D-04's
780
- // non-negotiable clause — for every VALID sub-verb an inert project's
781
- // output is byte-identical, on every flag path including a mistyped one
782
- // (T-03-26).
783
- const verb = args[2];
784
- const traceArgs = args.slice(3);
785
- if (verb !== 'scaffold' && verb !== 'render') {
786
- error(`Unknown trace subcommand. Available: ${TRACE_VERBS.join(', ')}`, ERROR_REASON.SDK_UNKNOWN_COMMAND);
787
- return;
788
- }
789
- // Quick 261015-at3: the admission verdict precedes the capability gate, as
790
- // in `routeVaultAnchor` — no `.platform-bindings.json` means `check
791
- // vault.trace` answers inert, so no trace may be produced either.
792
- // eslint-disable-next-line @typescript-eslint/no-require-imports
793
- const owner = require('./vault.cjs');
794
- if (owner.vaultProducerAdmissionVerdict(cwd).state !== 'on')
795
- return;
796
- if (!tracePracticesActive(cwd))
797
- return;
798
- if (verb === 'scaffold') {
799
- const RECOGNIZED_SCAFFOLD_FLAGS = new Set(['--stage', '--plan', '--anchors-dir', '--raw']);
800
- const unrecognizedScaffoldFlag = traceArgs.find((a) => a.startsWith('--') && !RECOGNIZED_SCAFFOLD_FLAGS.has(a));
801
- if (unrecognizedScaffoldFlag !== undefined) {
802
- error(`Unrecognized flag "${unrecognizedScaffoldFlag}" for vault trace scaffold. ` +
803
- `Supported: positional file paths, --stage <${TRACE_STAGES.join('|')}>, --plan <id>, ` +
804
- '--anchors-dir <dir> (execute stage), --raw.', ERROR_REASON.SDK_FAIL_FAST);
805
- return;
806
- }
807
- // Quick 261015-ect (QD-15): parked in standards mode, beside `practices
808
- // match` (PARK-01) and `vault audit` (PARK-02). A standards-mode plan
809
- // carries no practices ledger: `check ship.practices`, `check
810
- // vault.audit` and `check vault.review` answer not_applicable here, and
811
- // the standards `check vault.trace` reads `<vault_standards>` blocks, not
812
- // this ledger. After admission, the practices gate and this verb's flag
813
- // allowlist (a typo still fails first, as in the match and audit arms),
814
- // before any read; one fixed line from the leaf, exit 0, nothing written.
815
- if (modeSeam.isStandardsModeActive(cwd)) {
816
- // eslint-disable-next-line @typescript-eslint/no-require-imports
817
- const { notApplicableTraceVerbEnvelope } = require('./vault-mode.cjs');
818
- const naTrace = notApplicableTraceVerbEnvelope();
819
- c.output(naTrace, true, raw ? JSON.stringify(naTrace) : naTrace.message);
820
- return;
821
- }
822
- const valueIndices = flagValueIndices(traceArgs, ['--stage', '--plan', '--anchors-dir']);
823
- const stageIdx = traceArgs.indexOf('--stage');
824
- const stage = stageIdx !== -1 ? traceArgs[stageIdx + 1] : undefined;
825
- // A missing stage and an invalid one share a reason deliberately: both
826
- // are "this invocation cannot be interpreted", and splitting them would
827
- // make the operator read two messages to learn the same two words.
828
- if (stage === undefined || !TRACE_STAGES.includes(stage)) {
829
- error(`Missing or unrecognized --stage value for vault trace scaffold. ` +
830
- `Supported: ${TRACE_STAGES.join(', ')}.`, ERROR_REASON.SDK_FAIL_FAST);
831
- return;
832
- }
833
- const planIdx = traceArgs.indexOf('--plan');
834
- const planId = planIdx !== -1 ? traceArgs[planIdx + 1] : undefined;
835
- if (planId === undefined || planId.startsWith('--')) {
836
- error('No plan identifier was given. Usage: vault trace scaffold ' +
837
- `--stage <${TRACE_STAGES.join('|')}> --plan <id> [files...] [--raw].`, ERROR_REASON.SDK_MISSING_ARG);
838
- return;
839
- }
840
- // 04-14 (TGATE-01, CR-03): the value is PRESENT but is not a plan
841
- // identifier. Its own rejection, below both guards and beside the other
842
- // two argument rejections, so a vault-disabled project still reaches
843
- // none of them.
844
- //
845
- // THE REASON IS THE ESTABLISHED ONE, not a third vocabulary entry. It is
846
- // `sdk_fail_fast`, the same reason the stage validation above uses,
847
- // because this is the same class of defect: a value was supplied and
848
- // cannot be interpreted. `sdk_missing_arg` belongs to the branch directly
849
- // above, where the value was consumed by a following flag and no
850
- // identifier reached the arm at all.
851
- //
852
- // IT TEACHES RATHER THAN REFUSING. The message names the value that was
853
- // given, the required form and one conforming example, and then says what
854
- // the gate will do with the value — because a caller who is told only
855
- // that a value is invalid has to guess, and the guess CR-03 documents is
856
- // the one this rejection exists to stop.
857
- //
858
- // FENCE 3 is untouched: this arm still writes no file, and a rejection
859
- // returns before the leaf is required, so nothing is produced and nothing
860
- // is partially produced.
861
- if (!SCAFFOLD_PLAN_ID_RE.test(planId)) {
862
- error(`Invalid --plan value "${planId}" for vault trace scaffold. ` +
863
- `${SCAFFOLD_PLAN_ID_FORM} ${SCAFFOLD_QUICK_ID_FORM} The trace gate compares the identifier a plan artifact ` +
864
- 'records against the one its own filename states, and blocks on a disagreement.', ERROR_REASON.SDK_FAIL_FAST);
865
- return;
866
- }
867
- const files = traceArgs.filter((a, i) => !a.startsWith('--') && !valueIndices.has(i));
868
- // The execute stage resolves a `gitCommit` digest per governed file, so
869
- // the statement asserts something falsifiable: these practices governed
870
- // THESE artifacts at this vault SHA (D-08). The digest is the commit
871
- // that last produced the artifact, not merely HEAD, so a subject entry
872
- // points at the work it actually describes. A file with no commit yields
873
- // no digest — deliberately, because the leaf module records that gap as
874
- // a LOUD note inside the statement, and a HEAD fallback would paper over
875
- // a real coverage hole with a plausible-looking value.
876
- const runGit = _exec ?? shell_command_projection_cjs_1.execGitLocal;
877
- // 16-06 (D-14/D-31/D-35): the execute-stage record's anchors, read by the
878
- // CLI and never typed. The END anchor is HEAD now; the START anchor is
879
- // the one `vault anchor start` recorded in the plan's directory at
880
- // execute:pre (phase) or where quick captured its base. `--anchors-dir`
881
- // names that directory explicitly; otherwise it is derived from `--plan`
882
- // (`resolveScaffoldAnchorsDir`). The plan stage reads neither and its
883
- // output is byte-identical (golden-pinned).
884
- const anchorsDirIdx = traceArgs.indexOf('--anchors-dir');
885
- let anchors = null;
886
- if (anchorsDirIdx !== -1) {
887
- const anchorsDirArg = traceArgs[anchorsDirIdx + 1];
888
- if (stage !== 'execute') {
889
- error('--anchors-dir applies to --stage execute only; a plan-stage ledger records no anchors.', ERROR_REASON.SDK_FAIL_FAST);
890
- return;
891
- }
892
- if (anchorsDirArg === undefined || anchorsDirArg.startsWith('--')) {
893
- error('--anchors-dir needs a directory under .planning/. Usage: vault trace scaffold --stage execute --plan <id> --anchors-dir <dir> [files...].', ERROR_REASON.SDK_MISSING_ARG);
894
- return;
895
- }
896
- }
897
- if (stage === 'execute') {
898
- const head = runGit(['rev-parse', 'HEAD'], { cwd });
899
- const end = head.exitCode === 0 ? head.stdout.trim() : '';
900
- anchors = {};
901
- if (vaultSeal.ANCHOR_SHA_RE.test(end))
902
- anchors.endCommit = end;
903
- const located = anchorsDirIdx !== -1
904
- ? resolvePlanningPath(cwd, traceArgs[anchorsDirIdx + 1], 'dir')
905
- : resolveScaffoldAnchorsDir(cwd, planId);
906
- if (anchorsDirIdx !== -1 && !located.ok) {
907
- error(`--anchors-dir ${located.message}`, ERROR_REASON.SDK_FAIL_FAST);
908
- return;
909
- }
910
- const start = located.ok ? vaultSeal.readDirAnchors(located.abs)?.start : undefined;
911
- if (start !== undefined) {
912
- anchors.startCommit = start;
913
- }
914
- else {
915
- // A note, not an error: the record is still written, with its END
916
- // anchor, and the strict points type the missing start (16-07). One
917
- // line on stderr, so the embedded block on stdout is unchanged.
918
- process.stderr.write(`vault trace scaffold: no start anchor recorded for plan ${boundedPath(planId)} ` +
919
- `(${located.ok ? `${located.rel} holds no ${vaultSeal.VAULT_ANCHORS_FILE}` : located.message}); ` +
920
- 'the ledger carries endCommit only. `pp_run vault anchor start <dir>` records it at execute:pre.\n');
921
- }
922
- }
923
- const fileInputs = files.map((name) => {
924
- if (stage !== 'execute')
925
- return name;
926
- const res = runGit(['log', '-1', '--format=%H', '--', name], { cwd });
927
- const digest = res.exitCode === 0 ? res.stdout.trim() : '';
928
- return GIT_COMMIT_DIGEST_RE.test(digest) ? { name, gitCommit: digest } : name;
929
- });
930
- // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
931
- const traceMod = _trace ?? require('./vault-trace.cjs');
932
- // No files is a REAL path, not an error: a documentation-only plan is
933
- // still governed by the global practices, and emitting nothing there
934
- // would make the ruled-out set unauditable for exactly the plans where a
935
- // reviewer most wants to know what was considered.
936
- //
937
- // THE OVERRIDE CONTEXT (04-06, SC4 / D-13 / D-14 / D-15).
938
- //
939
- // The verdict is obtained HERE rather than inside the leaf, and the
940
- // reason is the same one that put the digest mapping above in the arm:
941
- // this is the only place that knows a trace is being written NOW, and an
942
- // override record is a statement about now. `buildTraceStatement`
943
- // reaching for a live verdict from inside itself would make a pure
944
- // producer's output depend on the machine's vault state through a path
945
- // its signature does not declare — the producer/judge split D-01 drew,
946
- // undone.
947
- //
948
- // The AGENT is never the courier for these three fields. Were the gate,
949
- // the block and the suppressed reason passed as flags, a bypass could be
950
- // concealed by simply not passing one — and the record that a bypass
951
- // happened is the single field a bypass most wants hidden. The CLI
952
- // computes all three, so there is nothing for a caller to omit.
953
- //
954
- // D-13: the arm runs once per stage per plan, and each invocation
955
- // re-reads the environment and re-computes the verdict at its own
956
- // timestamp. A bypass taken at execution but not at planning is
957
- // therefore a distinct visible event, not an inference.
958
- //
959
- // The common path is unchanged. `buildOverrideRecord` returns null unless
960
- // the verdict GENUINELY blocked *and* an accepted override reason is
961
- // present in the environment; a non-blocking verdict with the reason set
962
- // still records nothing, because recording one would manufacture evidence
963
- // of a bypass that never happened (D-15).
964
- //
965
- // Obtaining a verdict is a READ. `computeVaultVerdict` never fetches or
966
- // throws (its one possible spawn is a read-only git check-ignore), and this arm still writes no file and
967
- // splices no frontmatter (FENCE 3).
968
- const verdict = vault.computeVaultVerdict(cwd);
969
- const statement = traceMod.buildTraceStatement({
970
- cwd,
971
- stage,
972
- plan: planId,
973
- files: fileInputs,
974
- override: {
975
- gate: TRACE_OVERRIDE_GATE,
976
- block: verdict.block,
977
- suppressedReason: verdict.reason,
978
- },
979
- // Spread, never `anchors: null` on the plan stage: a seam that records
980
- // its arguments (the router tests' trace mock) then sees the call it
981
- // saw before 16-06.
982
- ...(anchors !== null ? { anchors } : {}),
983
- });
984
- // The three-way emit the `practices match --block` arm established: the
985
- // structured object is ALWAYS the output value, and the text a consumer
986
- // embeds is the third argument. The planner embeds the block VERBATIM —
987
- // it never renames a field and never re-derives a value, which is the
988
- // whole point of D-01's split.
989
- if (raw) {
990
- c.output(statement, true, JSON.stringify(statement));
991
- }
992
- else {
993
- c.output(statement, true, traceMod.serializeTraceBlock(statement));
994
- }
995
- }
996
- else {
997
- const RECOGNIZED_RENDER_FLAGS = new Set(['--raw']);
998
- const unrecognizedRenderFlag = traceArgs.find((a) => a.startsWith('--') && !RECOGNIZED_RENDER_FLAGS.has(a));
999
- if (unrecognizedRenderFlag !== undefined) {
1000
- error(`Unrecognized flag "${unrecognizedRenderFlag}" for vault trace render. ` +
1001
- 'Supported: a positional artifact path, --raw.', ERROR_REASON.SDK_FAIL_FAST);
1002
- return;
1003
- }
1004
- // Quick 261015-ect (QD-15): parked in standards mode, as the scaffold
1005
- // branch above is and for the same reason (no practices ledger is read).
1006
- if (modeSeam.isStandardsModeActive(cwd)) {
1007
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1008
- const { notApplicableTraceVerbEnvelope } = require('./vault-mode.cjs');
1009
- const naRender = notApplicableTraceVerbEnvelope();
1010
- c.output(naRender, true, raw ? JSON.stringify(naRender) : naRender.message);
1011
- return;
1012
- }
1013
- const artifactPath = traceArgs.find((a) => !a.startsWith('--'));
1014
- if (artifactPath === undefined) {
1015
- error('No artifact path was given. Usage: vault trace render <artifact> [--raw].', ERROR_REASON.SDK_MISSING_ARG);
1016
- return;
1017
- }
1018
- // CONTAINMENT (CR-04, T-03-50/51/52). `artifactPath` is an unvalidated
1019
- // positional and the read below is unconditional, so before this branch
1020
- // existed `vault trace render ../outside/secret.md` emitted an out-of-tree
1021
- // file's content as the `## Practices Applied` section that
1022
- // `agents/pp-executor.md` instructs the executor to include VERBATIM in
1023
- // SUMMARY.md — an arbitrary file's bytes becoming governance-artifact
1024
- // content a later reviewer agent reads.
1025
- //
1026
- // It sits HERE, inside the render arm, BELOW the `tracePracticesActive`
1027
- // early return above — and must stay there. That return is the single
1028
- // guard making the whole trace verb inert on a capability-off project, so
1029
- // no containment decision, no project-root resolution and no read may be
1030
- // reachable above it or an inert project stops being byte-identically
1031
- // silent (D-04, T-03-26). Do not hoist any of this for a cheaper argument
1032
- // check, and do not share a resolved root with the scaffold arm.
1033
- //
1034
- // It also sits BEFORE the read rather than being inferred from it:
1035
- // `readHeadBounded` returns the empty string on any problem and never
1036
- // throws, so a decision taken on its RESULT would be no decision at all.
1037
- //
1038
- // The predicate comes from the REAL leaf, not the `_vault` seam: a
1039
- // security decision must not be switchable by a test mock. Same
1040
- // `require('./vault.cjs')` the seam itself defaults to — one module, not
1041
- // a second import path.
1042
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1043
- const { isInsideDir } = require('./vault.cjs');
1044
- if (artifactPath.indexOf('\u0000') !== -1) {
1045
- error(`The artifact path is not a usable path: ${boundedPath(artifactPath)}`, ERROR_REASON.SDK_FAIL_FAST);
1046
- return;
1047
- }
1048
- let projectRoot;
1049
- try {
1050
- projectRoot = findProjectRoot(cwd) || cwd;
1051
- }
1052
- catch {
1053
- projectRoot = cwd;
1054
- }
1055
- let artifactAbs;
1056
- try {
1057
- artifactAbs = node_path_1.default.resolve(cwd, artifactPath);
1058
- }
1059
- catch {
1060
- error(`The artifact path could not be resolved: ${boundedPath(artifactPath)}`, ERROR_REASON.SDK_FAIL_FAST);
1061
- return;
1062
- }
1063
- if (!isInsideDir(projectRoot, artifactAbs)) {
1064
- error(`The artifact path resolves outside the project and was not read: ${boundedPath(artifactPath)}`, ERROR_REASON.SDK_FAIL_FAST);
1065
- return;
1066
- }
1067
- // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
1068
- const traceMod = _trace ?? require('./vault-trace.cjs');
1069
- const content = vault.readHeadBounded(artifactAbs, TRACE_ARTIFACT_READ_BYTES);
1070
- const parsed = traceMod.parseVaultTraceBlock(content, artifactPath);
1071
- // THE LEDGER, AND ONLY THE LEDGER. This arm must never call the matcher.
1072
- // Re-deriving the practice set at render time would produce a section
1073
- // that disagrees with the frontmatter it is supposedly generated from,
1074
- // which is the precise failure D-03 exists to prevent: the whole point of
1075
- // GENERATING the surface is that the two can never disagree (T-03-24).
1076
- // Adding a matcher call here looks like a robustness improvement and is
1077
- // the exact opposite — it reintroduces the drift the generation removed.
1078
- //
1079
- // An absent ledger yields the parser's documented absent result and NO
1080
- // section — not an error, and not an empty heading. An empty ledger is a
1081
- // different thing entirely and the leaf module's renderer handles it: it
1082
- // emits the heading plus one explanatory line, because a degraded run's
1083
- // record must stay visible and a reader who finds no section at all
1084
- // cannot tell a degraded run from a plan that was never vault-enabled.
1085
- const rendered = parsed.present ? traceMod.renderPracticesApplied(parsed.statement) : '';
1086
- if (raw) {
1087
- c.output(parsed, true, JSON.stringify(parsed));
1088
- }
1089
- else {
1090
- c.output(parsed, true, rendered);
1091
- }
1092
- }
1093
- }
1094
- else if (subcommand === 'audit') {
1095
- // ── The mechanical practice audit (Phase 05-06, CHECK-03/SC1) ──
1096
- //
1097
- // A PASS-THROUGH, on exactly the rule the two trace arms above state: it
1098
- // collects arguments, calls through and emits. It holds NO verdict, NO
1099
- // message, NO range and NO cap logic. `check vault.audit` serializes the
1100
- // SAME envelope from the SAME engine, and duplicating any of it here would
1101
- // create a second place where the operator's report can disagree with the
1102
- // gate that blocks the build on it (D-05-02). Even the human text is the
1103
- // leaf's — `renderAuditReport` is called over the object the leaf returned,
1104
- // never over a copy this arm rebuilt.
1105
- //
1106
- // ORDERING, copied from the trace arm above because the reason carries over
1107
- // verbatim: the capability gate is the FIRST branch, ahead of the flag
1108
- // allowlist (the admission verdict follows it here, per QD-03 below). Both orders satisfy T-02-07/T-03-25 — nothing is required and
1109
- // nothing spawns before an unknown flag is rejected, because the gate is a
1110
- // bounded config read and not a subprocess. Only THIS order also satisfies
1111
- // D-04's non-negotiable clause: an inert project's output is byte-identical
1112
- // on every path INCLUDING a mistyped one (T-03-26).
1113
- //
1114
- // 16-09 (SEAL-02, D-39) — ONE exception, and it is the gate's own: a
1115
- // switch that is off over a phase holding engine-written vault records.
1116
- // `check vault.audit` answers that `disabled_with_artifacts`, blocking, and
1117
- // this verb prints the SAME line from the SAME leaf (an agreeing pair). A
1118
- // project that never recorded anything still prints nothing.
1119
- if (!tracePracticesActive(cwd)) {
1120
- const refusal = disabledWithArtifactsRefusal(cwd, args);
1121
- if (refusal)
1122
- c.output(refusal, true, raw ? JSON.stringify(refusal) : refusal.message);
1123
- return;
1124
- }
1125
- // Quick 261015-at3 (QD-03, D-39): the admission verdict comes AFTER the
1126
- // switch block above, not before it. That block's refusal already asks
1127
- // `hasPlatformBindings` first and is the verb's half of an agreeing pair with
1128
- // `check vault.audit`; an admission-first line would silence the
1129
- // `vault.enabled: false`-over-records refusal the gate still answers. This
1130
- // line closes the remaining shape — both switches on, no bindings — where
1131
- // `check vault.audit` answers `inert`.
1132
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1133
- const owner = require('./vault.cjs');
1134
- if (owner.vaultProducerAdmissionVerdict(cwd).state !== 'on')
1135
- return;
1136
- // T-5-19 / T-02-07: whole-token rejection, so `--raw=json` is caught by the
1137
- // same check as a bare unknown flag rather than slipping past a prefix test
1138
- // and reaching a subprocess. The host (pp-core/bin/pp-tools.cjs) splices out
1139
- // its own global flags before dispatch, which is why the recognized set is
1140
- // one wide.
1141
- // The flag's NAME is spelled here and in `check-command-router.cts`'s
1142
- // `AUDIT_RANGE_FLAG`. What must not be duplicated is the POLICY — what
1143
- // counts as a well-formed range — and it is not: both surfaces call the
1144
- // leaf's `isValidOperatorRange`. A shared constant for the token itself
1145
- // would mean importing the audit leaf at this file's top level, which the
1146
- // lazy-require convention here exists to avoid, to buy agreement on seven
1147
- // characters that a test on each surface already pins.
1148
- const AUDIT_RANGE_FLAG = '--range';
1149
- const RECOGNIZED_AUDIT_FLAGS = new Set(['--raw', AUDIT_RANGE_FLAG]);
1150
- const unrecognizedAuditFlag = args.slice(2).find((a) => a.startsWith('--') && !RECOGNIZED_AUDIT_FLAGS.has(a));
1151
- if (unrecognizedAuditFlag !== undefined) {
1152
- error(`Unrecognized flag "${unrecognizedAuditFlag}" for vault audit. ` +
1153
- `Supported: a positional phase directory, --raw, ${AUDIT_RANGE_FLAG} <from>..<to>.`, ERROR_REASON.SDK_FAIL_FAST);
1154
- return;
1155
- }
1156
- // 13-07 (D-13, PARK-02): standards mode has no practice layer to audit.
1157
- // AFTER the inert gate above (D-22) and the flag guard, BEFORE the range,
1158
- // the positional and the leaf — the same place `check vault.audit` answers
1159
- // it, so the two surfaces agree on every argument shape. Still a
1160
- // pass-through: it emits the leaf's builder output, never its own wording
1161
- // (D-05-02).
1162
- if (modeSeam.isStandardsModeActive(cwd)) {
1163
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1164
- const { notApplicableGateEnvelope } = require('./vault-mode.cjs');
1165
- const naAudit = notApplicableGateEnvelope('vault.audit');
1166
- c.output(naAudit, true, raw ? JSON.stringify(naAudit) : naAudit.message);
1167
- return;
1168
- }
1169
- // 08-06 (D-08-19): the operator's range, and the ONE token in this arm that
1170
- // is a flag's VALUE rather than a positional. It is located before the
1171
- // positional is picked because the pick below is "the first token that does
1172
- // not start with `--`" — which, for `vault audit --range a..b <dir>`, is
1173
- // `a..b`. That would have scoped the audit to a phase directory nobody
1174
- // named, silently and with a plausible-looking envelope: the exact class of
1175
- // the WR-08 defect the gate surface's positional scanner was rewritten to
1176
- // remove. Here the fix is local, because this arm has one value-bearing
1177
- // flag and no enforcer appending others.
1178
- const auditArgs = args.slice(2);
1179
- const rangeAt = auditArgs.indexOf(AUDIT_RANGE_FLAG);
1180
- const auditRangeValue = rangeAt === -1 ? undefined : auditArgs[rangeAt + 1];
1181
- // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
1182
- const auditMod = _audit ?? require('./vault-audit.cjs');
1183
- if (rangeAt !== -1) {
1184
- // HARVESTED, NEVER RESTATED: the predicate is the leaf's own, so this
1185
- // operator surface and the gate surface cannot disagree about what a
1186
- // well-formed range is. A seam that does not provide it (the test mock)
1187
- // falls back to the real module rather than to "anything goes" — a
1188
- // missing predicate must never mean a passing one.
1189
- //
1190
- // Each candidate is CALLED ON ITS OWN OBJECT rather than detached into a
1191
- // variable: an unbound method is a `this` hazard the linter is right
1192
- // about, and nothing here needs the function as a value.
1193
- const rangeIsWellFormed = (v) => {
1194
- if (typeof auditMod.isValidOperatorRange === 'function') {
1195
- return auditMod.isValidOperatorRange(v);
1196
- }
1197
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1198
- const leaf = require('./vault-audit.cjs');
1199
- return typeof leaf.isValidOperatorRange === 'function' ? leaf.isValidOperatorRange(v) : false;
1200
- };
1201
- if (typeof auditRangeValue !== 'string' || !rangeIsWellFormed(auditRangeValue)) {
1202
- // The refused value is NOT echoed: it failed validation, so it is not
1203
- // trusted text, and this arm's message is read by a person whose next
1204
- // action is to retype it (T-08-32).
1205
- error(`${AUDIT_RANGE_FLAG} needs a well-formed <from>..<to> git range (letters, digits, `
1206
- + '. _ / ^ ~ -, two or three dots, at most 200 characters). The supplied value is not '
1207
- + 'reproduced here because it failed validation. Re-run with a well-formed range, or '
1208
- + 'drop the flag to let the phase-start ladder resolve it.', ERROR_REASON.SDK_FAIL_FAST);
1209
- return;
1210
- }
1211
- }
1212
- // The phase argument. A DIRECTORY path carries the workstream partition,
1213
- // which is why neither dispatch point has to pass `--ws`
1214
- // (check-query-gate.md:27, and the four-times-shipped omitted-`--ws`
1215
- // lesson behind it). The range's VALUE is excluded by index, so the flag
1216
- // may precede the positional.
1217
- const auditPhaseArg = auditArgs.find((a, i) => !a.startsWith('--') && !(rangeAt !== -1 && i === rangeAt + 1));
1218
- if (auditPhaseArg === undefined) {
1219
- error(`No phase directory was given. Usage: vault audit <phase-dir> [${AUDIT_RANGE_FLAG} <from>..<to>] [--raw].`, ERROR_REASON.SDK_MISSING_ARG);
1220
- return;
1221
- }
1222
- let auditRoot;
1223
- try {
1224
- auditRoot = findProjectRoot(cwd) || cwd;
1225
- }
1226
- catch {
1227
- auditRoot = cwd;
1228
- }
1229
- // 08-10 (F4). THE ARGUMENT IS PART OF THE PASS-THROUGH. This arm read the
1230
- // positional as a raw path while `check vault.audit` resolved it through
1231
- // `resolveTracePhaseDir` — either an existing directory OR a phase token the
1232
- // locator resolves. Measured on the live fixture, `01` therefore produced
1233
- // `{"status":"ok","block":false,"rangeSource":"phase-start-commit"}` from the
1234
- // gate and `{"status":"warn","block":true,"rangeSource":"base-branch",
1235
- // "...":"manifest-not-found"}` from the operator's own command: a blocking,
1236
- // meaningless verdict where the gate saw a clean phase-scoped pass. That is
1237
- // exactly the disagreement this arm's own doc comment above forbids
1238
- // (D-05-02), so the RESOLVER is harvested — never restated — from the leaf
1239
- // both routers already import.
1240
- //
1241
- // From the REAL leaf, not the `_vault` seam — same rule and same reason as
1242
- // the `isInsideDir` harvest in the render arm above: a containment decision
1243
- // must not be switchable by a test mock.
1244
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1245
- const { resolveTracePhaseDir } = require('./vault.cjs');
1246
- const resolvedAuditPhase = resolveTracePhaseDir(auditRoot, cwd, auditPhaseArg);
1247
- if (!resolvedAuditPhase) {
1248
- // The SAME `unresolved` shape the gate emits (check-command-router.cts),
1249
- // not a base-branch fallback that reads as a real audit of nothing. Stated
1250
- // only from what was measured, and naming a re-run rather than a deletion
1251
- // (T-4-75).
1252
- const unresolved = {
1253
- status: 'unresolved',
1254
- block: true,
1255
- message: `Vault audit: "${boundedPath(auditPhaseArg)}" is neither an existing `
1256
- + 'directory inside the project nor a phase the locator resolves, so no practice was '
1257
- + 'compared against anything. Repair: re-run this command with the phase directory.',
1258
- };
1259
- c.output(unresolved, true, raw ? JSON.stringify(unresolved) : unresolved.message);
1260
- return;
1261
- }
1262
- const auditResult = auditMod.runVaultAudit({
1263
- root: auditRoot,
1264
- phaseDir: resolvedAuditPhase.abs,
1265
- exec: _exec ?? shell_command_projection_cjs_1.execGitLocal,
1266
- // Spread, never `range: undefined` — the leaf keys on the key's TYPE to
1267
- // decide whether its ladder runs.
1268
- ...(typeof auditRangeValue === 'string' ? { range: auditRangeValue } : {}),
1269
- });
1270
- if (raw) {
1271
- c.output(auditResult, true, JSON.stringify(auditResult));
1272
- }
1273
- else {
1274
- c.output(auditResult, true, auditMod.renderAuditReport(auditResult));
1275
- }
1276
- }
1277
- else if (subcommand === 'manifest') {
1278
- // ── The phase-manifest writer (260915-ik2, UAT-6-F1) ──
1279
- //
1280
- // `start` anchors `phases[NN].startCommit = HEAD` (write-once); `record`
1281
- // writes `phases[NN].commits` from that anchor. Both are the leaf's —
1282
- // this arm collects arguments, calls through and emits, exactly as the
1283
- // `audit` arm above does, and in the same ORDER for the same reason: the
1284
- // capability gate is FIRST and silent, so an inert project's output is
1285
- // byte-identical on every path including a mistyped one (T-03-26,
1286
- // T-ik2-08). It exits 0 on every path: the step files print the result and
1287
- // continue, and a refusal travels as `ok: false` with a reason.
1288
- //
1289
- // Quick 261015-at3: the admission verdict precedes the capability gate — a
1290
- // project with no `.platform-bindings.json` gets no manifest file written.
1291
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1292
- const owner = require('./vault.cjs');
1293
- if (owner.vaultProducerAdmissionVerdict(cwd).state !== 'on')
1294
- return;
1295
- if (!tracePracticesActive(cwd))
1296
- return;
1297
- const RECOGNIZED_MANIFEST_FLAGS = new Set(['--raw']);
1298
- const unrecognizedManifestFlag = args.slice(2).find((a) => a.startsWith('--') && !RECOGNIZED_MANIFEST_FLAGS.has(a));
1299
- if (unrecognizedManifestFlag !== undefined) {
1300
- error(`Unrecognized flag "${unrecognizedManifestFlag}" for vault manifest. ` +
1301
- `Supported: a verb (${MANIFEST_VERBS.join(' | ')}), a positional phase directory, --raw.`, ERROR_REASON.SDK_FAIL_FAST);
1302
- return;
1303
- }
1304
- const manifestPositionals = args.slice(2).filter((a) => !a.startsWith('--'));
1305
- const manifestVerb = manifestPositionals[0];
1306
- if (manifestVerb !== 'start' && manifestVerb !== 'record') {
1307
- error(`Unknown vault manifest verb. Available: ${MANIFEST_VERBS.join(', ')}`, ERROR_REASON.SDK_UNKNOWN_COMMAND);
1308
- return;
1309
- }
1310
- // Quick 261015-ect (QD-15): parked in standards mode. The range this verb
1311
- // anchors serves two gates, `vault audit` and `check vault.review`, and
1312
- // both answer not_applicable in this mode (PARK-02, PARK-04). Its third
1313
- // reader, `/pp-undo --phase` (pp-core/workflows/undo.md), falls back to its
1314
- // git-log search when the file or the phase entry is missing — the path a
1315
- // project with no bindings or with the practices switch off already takes.
1316
- // After the verb check and before any positional or phase read; the
1317
- // envelope carries `ok: false` and a typed reason, which is how the step
1318
- // files read a manifest refusal (print the line and continue).
1319
- if (modeSeam.isStandardsModeActive(cwd)) {
1320
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1321
- const { notApplicableManifestVerbEnvelope } = require('./vault-mode.cjs');
1322
- const naManifest = notApplicableManifestVerbEnvelope();
1323
- c.output(naManifest, true, raw ? JSON.stringify(naManifest) : naManifest.message);
1324
- return;
1325
- }
1326
- // The phase DIRECTORY, as for `audit`: the path carries the workstream
1327
- // partition, and the manifest is written two levels above it.
1328
- const manifestPhaseDir = manifestPositionals[1];
1329
- if (manifestPhaseDir === undefined) {
1330
- error('No phase directory was given. Usage: vault manifest <start|record> <phase-dir> [--raw].', ERROR_REASON.SDK_MISSING_ARG);
1331
- return;
1332
- }
1333
- let manifestRoot;
1334
- try {
1335
- manifestRoot = findProjectRoot(cwd) || cwd;
1336
- }
1337
- catch {
1338
- manifestRoot = cwd;
1339
- }
1340
- // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
1341
- const manifestMod = _audit ?? require('./vault-audit.cjs');
1342
- const writeOpts = { root: manifestRoot, phaseDir: manifestPhaseDir, cwd, exec: _exec ?? shell_command_projection_cjs_1.execGitLocal };
1343
- const manifestResult = manifestVerb === 'start'
1344
- ? manifestMod.recordPhaseStart(writeOpts)
1345
- : manifestMod.recordPhaseCommits(writeOpts);
1346
- if (raw) {
1347
- c.output(manifestResult, true, JSON.stringify(manifestResult));
1348
- }
1349
- else {
1350
- c.output(manifestResult, true, manifestMod.renderManifestResult(manifestResult));
1351
- }
1352
- }
1353
429
  else if (subcommand === 'update') {
1354
430
  // PBI 74316: `vault update [<tag>] [--yes]`. Without --yes it reports the
1355
431
  // plan and writes nothing; see src/vault-update.cts for the contract.
@@ -1383,8 +459,7 @@ function routeVaultCommand({ args, cwd, raw, error, _vault, _core, _practices, _
1383
459
  }
1384
460
  else if (subcommand === 'standards') {
1385
461
  // 15-07 (D-06, D-08, D-13): the standards record verbs. The arm body lives
1386
- // in `routeVaultStandards` below; the invalid-mode refusal above already
1387
- // ran, because 'standards' is a member of VAULT_SUBCOMMANDS.
462
+ // in `routeVaultStandards` below.
1388
463
  routeVaultStandards({ cwd, args, raw, error, vault, c, exec: _exec ?? shell_command_projection_cjs_1.execGitLocal });
1389
464
  }
1390
465
  else if (subcommand === 'admission') {
@@ -1411,7 +486,7 @@ function routeVaultCommand({ args, cwd, raw, error, _vault, _core, _practices, _
1411
486
  *
1412
487
  * The answer comes from `vaultAdmissionVerdict` in the REAL src/vault.cts, never
1413
488
  * from the injectable `_vault` seam: a governance decision must not be
1414
- * switchable by a test mock (the mode-guard precedent in `routeVaultCommand`).
489
+ * switchable by a test mock.
1415
490
  * It stays on the cwd's own verdict, not the producer verdict the seven record
1416
491
  * producers ask (quick 261015-ect, QD-10): this verb is a REPORT of the cwd,
1417
492
  * which is what every fence asks, so a bindings-less linked worktree answers
@@ -1420,9 +495,7 @@ function routeVaultCommand({ args, cwd, raw, error, _vault, _core, _practices, _
1420
495
  * Exit 0 on every answer, because a door fence reads the TOKEN: any stdout other
1421
496
  * than `on`, `off` or `inert` halts it, and its `|| echo unrecognised` fallback
1422
497
  * turns a non-zero exit into a halt as well. The only non-zero paths are an
1423
- * argument error here and the 13-06 invalid-mode refusal that precedes every
1424
- * vault subcommand where `.platform-bindings.json` exists (QD-11); both
1425
- * therefore halt the fence, never skip it.
498
+ * argument error here, which therefore halts the fence, never skips it.
1426
499
  */
1427
500
  function admissionVerb({ cwd, rest, raw, error, c }) {
1428
501
  let explain = false;
@@ -1462,7 +535,7 @@ function admissionVerb({ cwd, rest, raw, error, c }) {
1462
535
  *
1463
536
  * ORDER: unless `vault admission` answers `on` (bindings present, every config
1464
537
  * readable, the vault switch on) both verbs print nothing and write nothing,
1465
- * before any argument or the mode is read — so a project without the vault is
538
+ * before any argument is read — so a project without the vault is
1466
539
  * byte-identical on every path including a mistyped one, and a standards record
1467
540
  * cannot be produced on a project whose trace gate is inert (17-REVIEW CR-01,
1468
541
  * D-13; the same gate `routeVaultAnchor` and `doorRecordVerb` ask). The verdict
@@ -1470,26 +543,23 @@ function admissionVerb({ cwd, rest, raw, error, c }) {
1470
543
  * QD-09): in a linked worktree reading its owning checkout's cache — where an
1471
544
  * executor runs `consulted` and has no untracked bindings of its own — that is
1472
545
  * the owning checkout, which is also where the gate reading the record runs. Then
1473
- * practices mode answers its one not-applicable line, then the verb and the
1474
- * flags are checked. An invalid mode never reaches here: the pre-dispatch
1475
- * guard in `routeVaultCommand` refused it.
546
+ * the verb and the flags are checked.
1476
547
  *
1477
- * Every governance read is from the REAL module, never the `_vault` seam (the
1478
- * mode-guard precedent); only the bounded PLAN read goes through the seam, so
548
+ * Every governance read is from the REAL module, never the `_vault` seam; only the bounded PLAN read goes through the seam, so
1479
549
  * a test can count it. Neither verb writes a file.
1480
550
  */
1481
551
  function routeVaultStandards({ cwd, args, raw, error, vault, c, exec }) {
552
+ // `promote` is an operator action, not a record producer: it is dispatched
553
+ // BEFORE the producer admission below, so a vault that is off answers with
554
+ // the promote module's own activation refusal instead of printing nothing.
555
+ if (args[2] === 'promote') {
556
+ standardsPromoteVerb({ cwd, rest: args.slice(3), raw, error, c, exec });
557
+ return;
558
+ }
1482
559
  // eslint-disable-next-line @typescript-eslint/no-require-imports
1483
560
  const owner = require('./vault.cjs');
1484
561
  if (owner.vaultProducerAdmissionVerdict(cwd).state !== 'on')
1485
562
  return;
1486
- if (!owner.isStandardsModeActive(cwd)) {
1487
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1488
- const { notApplicableStandardsVerbEnvelope } = require('./vault-mode.cjs');
1489
- const na = notApplicableStandardsVerbEnvelope();
1490
- c.output(na, true, raw ? JSON.stringify(na) : na.message);
1491
- return;
1492
- }
1493
563
  const verb = args[2];
1494
564
  const rest = args.slice(3);
1495
565
  if (verb === 'block') {
@@ -1502,6 +572,79 @@ function routeVaultStandards({ cwd, args, raw, error, vault, c, exec }) {
1502
572
  error(`Unknown standards subcommand. Available: ${STANDARDS_VERBS.join(', ')}`, ERROR_REASON.SDK_UNKNOWN_COMMAND);
1503
573
  }
1504
574
  }
575
+ /** The flags `vault standards promote` takes that carry a value. */
576
+ const PROMOTE_VALUE_FLAGS = ['--path', '--entry', '--section', '--expect'];
577
+ /**
578
+ * `vault standards promote <file> [--path <vault-path>] [--entry "<what it is>;
579
+ * used when <…>"] [--section <INDEX section>] [--confirmed --expect <token>]
580
+ * [--raw]` (FORK-ONLY): add a standards document from this project to the
581
+ * client vault with its INDEX.md entry. Without `--confirmed` it only
582
+ * previews; see src/vault-standards-promote.cts for the guards and the two
583
+ * runs. An unrecognised flag is refused BEFORE the module loads or anything
584
+ * spawns (T-29-16); no flag disables or relaxes a guard. The verdict is
585
+ * written to stdout first, then a refusal also exits non-zero: an action
586
+ * command's refusal must be observable in the exit code.
587
+ */
588
+ function standardsPromoteVerb({ cwd, rest, raw, error, c, exec }) {
589
+ const usage = 'Usage: vault standards promote <file> [--path <vault-path>] [--entry "<what it is>; used when <...>"] '
590
+ + '[--section <INDEX section>] [--confirmed --expect <token>] [--raw].';
591
+ const values = {};
592
+ let confirmed = false;
593
+ let filePath;
594
+ for (let i = 0; i < rest.length; i += 1) {
595
+ const a = rest[i];
596
+ if (a === '--raw')
597
+ continue;
598
+ if (a === '--confirmed') {
599
+ confirmed = true;
600
+ continue;
601
+ }
602
+ if (PROMOTE_VALUE_FLAGS.includes(a)) {
603
+ const v = rest[i + 1];
604
+ if (v === undefined || v.startsWith('--')) {
605
+ error(`${a} needs a value. ${usage}`, ERROR_REASON.SDK_MISSING_ARG);
606
+ return;
607
+ }
608
+ values[a] = v;
609
+ i += 1;
610
+ continue;
611
+ }
612
+ if (a.startsWith('--')) {
613
+ error(`Unrecognized flag "${boundedPath(a)}" for vault standards promote. ${usage}`, ERROR_REASON.SDK_FAIL_FAST);
614
+ return;
615
+ }
616
+ if (filePath !== undefined) {
617
+ error(`vault standards promote takes exactly one document. ${usage}`, ERROR_REASON.SDK_FAIL_FAST);
618
+ return;
619
+ }
620
+ filePath = a;
621
+ }
622
+ if (filePath === undefined) {
623
+ error(`No standards document was given. ${usage}`, ERROR_REASON.SDK_MISSING_ARG);
624
+ return;
625
+ }
626
+ if (confirmed && values['--expect'] === undefined) {
627
+ error('This standards document was not pushed: the confirmation is missing the preview token. '
628
+ + 'Run the promotion without --confirmed first, then pass the token it prints.', ERROR_REASON.VAULT_PROMOTE_CONCURRENT_EDIT);
629
+ return;
630
+ }
631
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
632
+ const promote = require('./vault-standards-promote.cjs');
633
+ const verdict = promote.promoteStandard({
634
+ cwd,
635
+ filePath,
636
+ vaultPath: values['--path'] ?? null,
637
+ entry: values['--entry'] ?? null,
638
+ section: values['--section'] ?? null,
639
+ confirmed,
640
+ expect: values['--expect'] ?? null,
641
+ _exec: exec,
642
+ });
643
+ c.output(verdict, true, raw ? JSON.stringify(verdict) : promote.renderPromoteReport(verdict));
644
+ if (verdict.status === 'refused') {
645
+ error(String(verdict.message), String(verdict.reason));
646
+ }
647
+ }
1505
648
  function standardsBlockVerb({ cwd, rest, raw, error, c, owner }) {
1506
649
  // ── arguments ──
1507
650
  let planId;
@@ -1602,7 +745,7 @@ function standardsBlockVerb({ cwd, rest, raw, error, c, owner }) {
1602
745
  // trace leaf accepts the operator's override reason from this process's
1603
746
  // environment; a non-blocking verdict records nothing (D-15 of Phase 4). The
1604
747
  // router names no part of the override stack (the leaf owns it all). The
1605
- // gate name is the scaffold arm's own constant. The verdict is computed
748
+ // gate name is `TRACE_OVERRIDE_GATE`. The verdict is computed
1606
749
  // against the SAME cache the stamp above measured (D-36, 15-REVIEW iter-2
1607
750
  // WR-02), exactly as the trace gate recomputes it, so the suppressed reason
1608
751
  // recorded here is the one the gate will compare it with, in any checkout.
@@ -1627,7 +770,7 @@ function standardsBlockVerb({ cwd, rest, raw, error, c, owner }) {
1627
770
  error(`Vault standards block could not write the record: ${boundedPath(e instanceof Error ? e.message : String(e))}`, ERROR_REASON.UNKNOWN);
1628
771
  return;
1629
772
  }
1630
- const envelope = owner.withVaultMode({
773
+ const envelope = {
1631
774
  status: 'ok',
1632
775
  planId,
1633
776
  stamp: { repo: record.repo, ref: record.ref, sha: record.sha, indexStatus },
@@ -1635,7 +778,7 @@ function standardsBlockVerb({ cwd, rest, raw, error, c, owner }) {
1635
778
  degraded: record.degraded,
1636
779
  override: record.override,
1637
780
  text,
1638
- }, cwd);
781
+ };
1639
782
  c.output(envelope, true, raw ? JSON.stringify(envelope) : text);
1640
783
  }
1641
784
  function standardsConsultedVerb({ cwd, rest, raw, error, c, owner, vault, exec, }) {
@@ -1652,9 +795,9 @@ function standardsConsultedVerb({ cwd, rest, raw, error, c, owner, vault, exec,
1652
795
  }
1653
796
  const artifactPath = positionals[0];
1654
797
  const shown = boundedPath(artifactPath);
1655
- // CONTAINMENT FIRST, then the size, then the read (the `trace render`
1656
- // precedent, T-03-50): the plan path is an unvalidated positional, and a
1657
- // decision taken on the bounded read's RESULT would be no decision.
798
+ // CONTAINMENT FIRST, then the size, then the read (T-03-50): the plan path
799
+ // is an unvalidated positional, and a decision taken on the bounded read's
800
+ // RESULT would be no decision.
1658
801
  if (artifactPath.indexOf('\u0000') !== -1) {
1659
802
  error(`The plan path is not a usable path: ${shown}`, ERROR_REASON.SDK_FAIL_FAST);
1660
803
  return;
@@ -1758,7 +901,7 @@ function standardsConsultedVerb({ cwd, rest, raw, error, c, owner, vault, exec,
1758
901
  const start = vaultSeal.readDirAnchors(node_path_1.default.dirname(artifactAbs))?.start ?? null;
1759
902
  const anchors = vaultSeal.ANCHOR_SHA_RE.test(end) ? { start, end } : null;
1760
903
  const section = vaultStandardsLeaf.serializeConsultedSection(record, { anchors });
1761
- const envelope = owner.withVaultMode({
904
+ const envelope = {
1762
905
  status: 'ok',
1763
906
  planId: record.planId,
1764
907
  stamp: { repo: record.repo, ref: record.ref, sha: record.sha },
@@ -1767,13 +910,13 @@ function standardsConsultedVerb({ cwd, rest, raw, error, c, owner, vault, exec,
1767
910
  degraded: record.degraded === null ? null : vaultStandardsLeaf.standardsEchoDegraded(record.degraded),
1768
911
  cacheSource: cache.source,
1769
912
  section,
1770
- }, cwd);
913
+ };
1771
914
  c.output(envelope, true, raw ? JSON.stringify(envelope) : section);
1772
915
  }
1773
916
  /**
1774
917
  * The real vault module, for the governance reads of the 16-06 verbs: the
1775
918
  * admission verdict, containment and phase resolution must not be switchable
1776
- * by a test mock (the mode-guard precedent in `routeVaultCommand`).
919
+ * by a test mock.
1777
920
  */
1778
921
  function anchorOwner() {
1779
922
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -1856,52 +999,6 @@ function isDanglingLink(abs) {
1856
999
  return false;
1857
1000
  }
1858
1001
  }
1859
- /**
1860
- * Where the execute-stage scaffold reads a plan's START anchor from, derived
1861
- * from `--plan` (16-06, D-31). Measured order:
1862
- *
1863
- * 1. the QUICK form first: the one directory `<root>/.planning/quick/<id>-*`
1864
- * (quick task directories always live under the ROOT planning tree,
1865
- * `src/init.cts`). Exactly one match resolves; two or more is ambiguous
1866
- * and resolves nothing. Tried for every id, so an all-digit quick id
1867
- * (`261011-123`, which the phase alternative of `SCAFFOLD_PLAN_ID_RE`
1868
- * also accepts) still finds its quick directory, and no second copy of
1869
- * the quick-id form is needed here;
1870
- * 2. otherwise the PHASE form: the segment before the first `-` (`02.1` of
1871
- * `02.1-03`) resolved by `resolveTracePhaseDir`, the resolver the trace
1872
- * gate uses for a phase-number positional, which honours the active
1873
- * workstream (`--ws`, `GSD_WORKSTREAM`, the session pointer) through the
1874
- * phase locator.
1875
- *
1876
- * Unresolvable → `ok: false`, and the scaffold writes `endCommit` only.
1877
- */
1878
- function resolveScaffoldAnchorsDir(cwd, planId) {
1879
- const root = projectRootOf(cwd);
1880
- const quickRoot = node_path_1.default.join(root, '.planning', 'quick');
1881
- let quick = [];
1882
- try {
1883
- quick = node_fs_1.default.readdirSync(quickRoot, { withFileTypes: true })
1884
- .filter((d) => d.isDirectory() && d.name.startsWith(`${planId}-`))
1885
- .map((d) => d.name);
1886
- }
1887
- catch {
1888
- quick = [];
1889
- }
1890
- if (quick.length === 1) {
1891
- const abs = node_path_1.default.join(quickRoot, quick[0]);
1892
- return { ok: true, root, abs, rel: node_path_1.default.relative(root, abs).split(node_path_1.default.sep).join('/') };
1893
- }
1894
- if (quick.length > 1) {
1895
- return { ok: false, reason: 'anchors-dir-ambiguous', message: `${quick.length} quick directories match .planning/quick/${boundedPath(planId)}-*` };
1896
- }
1897
- const dash = planId.indexOf('-');
1898
- if (dash > 0) {
1899
- const resolved = anchorOwner().resolveTracePhaseDir(root, cwd, planId.slice(0, dash));
1900
- if (resolved && resolved.onlyFile === undefined)
1901
- return { ok: true, root, abs: resolved.abs, rel: resolved.rel };
1902
- }
1903
- return { ok: false, reason: 'anchors-dir-unresolved', message: `no phase or quick directory resolves for plan ${boundedPath(planId)}` };
1904
- }
1905
1002
  /**
1906
1003
  * `vault anchor <verb>` (16-06). INERT FIRST (D-13): unless the producer
1907
1004
  * admission verdict is `on` (bindings present, every config readable, the
@@ -1938,8 +1035,6 @@ function routeVaultAnchor({ cwd, args, raw, error, c, exec }) {
1938
1035
  * `vault anchor start <dir>` (16-06, D-31): record HEAD as the START anchor of
1939
1036
  * a phase or quick directory, in `<dir>/.vault-anchors.json`
1940
1037
  * (`{ start, recordedAt }`, written by the seal leaf's `writeDirStartAnchor`).
1941
- * Mode-neutral: it is the start the practices-only `vault manifest start`
1942
- * cannot give a standards-mode phase.
1943
1038
  *
1944
1039
  * Re-running at the SAME HEAD keeps the recorded start (`kept: true`, nothing
1945
1040
  * written). At any other HEAD it records HEAD as the new start and says which
@@ -1957,8 +1052,8 @@ function routeVaultAnchor({ cwd, args, raw, error, c, exec }) {
1957
1052
  * widening.
1958
1053
  *
1959
1054
  * Exit 0 on every refusal, which travels as `{ ok: false, reason, message }`
1960
- * (the `vault manifest` posture: the step prints it and continues; the strict
1961
- * points type a missing start). An argument error is the only exit 1.
1055
+ * (the step prints it and continues; the strict points type a missing
1056
+ * start). An argument error is the only exit 1.
1962
1057
  */
1963
1058
  function anchorStartVerb({ cwd, rest, raw, error, c, exec }) {
1964
1059
  const positionals = [];
@@ -2247,75 +1342,6 @@ function insertLinesAfter(text, spans, after, lines) {
2247
1342
  const at = spans[after].end;
2248
1343
  return `${text.slice(0, at)}${lines.map((l) => `${eol}${l}`).join('')}${text.slice(at)}`;
2249
1344
  }
2250
- /**
2251
- * The index of the practices ledger's `generatedAt` line, the line the
2252
- * serializer writes the anchors after, located the way the parser locates the
2253
- * block: the frontmatter from line 0 to the first later line opening `---`, the
2254
- * top-level `vault:` key, its `predicate:` child, and that child's own
2255
- * `generatedAt:` key. Also returns the indent of the predicate's keys and one
2256
- * indentation step, so the spliced lines sit at the ledger's own indentation.
2257
- * `null` when any of these is missing.
2258
- */
2259
- function ledgerGeneratedAtLine(text, spans) {
2260
- const line = (i) => text.slice(spans[i].start, spans[i].end);
2261
- const indentOf = (l) => l.length - l.trimStart().length;
2262
- if (spans.length < 3 || line(0) !== '---')
2263
- return null;
2264
- let close = -1;
2265
- for (let i = 1; i < spans.length; i += 1) {
2266
- if (line(i).startsWith('---')) {
2267
- close = i;
2268
- break;
2269
- }
2270
- }
2271
- if (close === -1)
2272
- return null;
2273
- const vaultAt = Array.from({ length: close - 1 }, (_, k) => k + 1).find((i) => /^vault:\s*$/.test(line(i)));
2274
- if (vaultAt === undefined)
2275
- return null;
2276
- let predicateAt = -1;
2277
- let childIndent = -1;
2278
- for (let i = vaultAt + 1; i < close; i += 1) {
2279
- const l = line(i);
2280
- if (l.trim() === '')
2281
- continue;
2282
- const ind = indentOf(l);
2283
- if (ind === 0)
2284
- break;
2285
- if (childIndent === -1)
2286
- childIndent = ind;
2287
- if (ind === childIndent && l.trim() === 'predicate:') {
2288
- predicateAt = i;
2289
- break;
2290
- }
2291
- }
2292
- if (predicateAt === -1)
2293
- return null;
2294
- let keyIndent = -1;
2295
- for (let i = predicateAt + 1; i < close; i += 1) {
2296
- const l = line(i);
2297
- if (l.trim() === '')
2298
- continue;
2299
- const ind = indentOf(l);
2300
- if (ind <= childIndent)
2301
- break;
2302
- if (keyIndent === -1)
2303
- keyIndent = ind;
2304
- if (ind === keyIndent && l.trimStart().startsWith('generatedAt:')) {
2305
- return { index: i, keyIndent, step: keyIndent - childIndent };
2306
- }
2307
- }
2308
- return null;
2309
- }
2310
- /** The parsed statement minus its anchor keys, as text, for the before/after equality the back-fill asserts. */
2311
- function statementWithoutAnchors(statement) {
2312
- const st = JSON.parse(JSON.stringify(statement ?? null));
2313
- if (st && st.predicate) {
2314
- for (const k of ['startCommit', 'endCommit', 'anchorBackfill', 'anchorsDropped'])
2315
- delete st.predicate[k];
2316
- }
2317
- return JSON.stringify(st);
2318
- }
2319
1345
  /**
2320
1346
  * `vault anchor backfill <summary> --at <sha>` (16-06, D-32):
2321
1347
  * give a record written before anchors existed its anchors, ATTRIBUTED. There
@@ -2325,13 +1351,9 @@ function statementWithoutAnchors(statement) {
2325
1351
  * user.name`, bounded) and when. 16-07 reports a back-filled anchor as a note
2326
1352
  * on every later check. `--at HEAD` is allowed, and is still attributed.
2327
1353
  *
2328
- * - practices (a SUMMARY's execute-stage `vault:` ledger, tried first):
2329
- * `predicate.endCommit` = `--at` and `predicate.anchorBackfill: { by, at }`
2330
- * (no start: the record takes its directory's, D-59), spliced after the
2331
- * ledger's `generatedAt` line with the serializer's own lines
2332
- * (`traceAnchorLines`), so a later `trace render` round trip agrees;
2333
- * - standards (a `## Standards Consulted` section): the anchor line with its
2334
- * `backfilled by <who> at <iso>` tail, inserted as the section's last line.
1354
+ * The record is a SUMMARY's `## Standards Consulted` section: the anchor line
1355
+ * with its `backfilled by <who> at <iso>` tail is inserted as the section's
1356
+ * last line (no start: the record takes its directory's, D-59).
2335
1357
  *
2336
1358
  * Every other byte of the file is kept: the new text is the old one with lines
2337
1359
  * inserted, and it is parsed back before it is written — the anchors must read
@@ -2340,8 +1362,8 @@ function statementWithoutAnchors(statement) {
2340
1362
  * renamed into place.
2341
1363
  *
2342
1364
  * Refused, writing nothing: a record that already carries an anchor (a valid,
2343
- * a dropped or a malformed one: `already-anchored`), a file with no record of
2344
- * either family (`no-record`), an `--at` that is not an ancestor of HEAD. A
1365
+ * a dropped or a malformed one: `already-anchored`), a file with no
1366
+ * Standards Consulted section (`no-record`), an `--at` that is not an ancestor of HEAD. A
2345
1367
  * path outside `.planning/`, a malformed sha and `--from`/`--attest` beside
2346
1368
  * `--at` are argument errors (exit 1), refused before any read or git call;
2347
1369
  * the state refusals are `ok: false` envelopes (exit 0).
@@ -2429,20 +1451,13 @@ function anchorBackfillVerb({ cwd, rest, raw, error, c, exec }) {
2429
1451
  const text = node_fs_1.default.readFileSync(located.abs, 'utf8');
2430
1452
  // eslint-disable-next-line @typescript-eslint/no-require-imports
2431
1453
  const traceLeaf = require('./vault-trace.cjs');
2432
- const ledger = traceLeaf.parseVaultTraceBlock(text, located.rel);
2433
- const st = (ledger.present ? ledger.statement : null);
2434
- const isLedger = st !== null && st._type === traceLeaf.TRACE_STATEMENT_TYPE && st.predicateType === traceLeaf.TRACE_PREDICATE_TYPE
2435
- && st.predicate !== undefined && st.predicate.stage === 'execute';
2436
- const section = isLedger ? null : vaultStandardsLeaf.parseConsultedSection(text);
2437
- const isSection = section !== null && section.present && section.planId !== null;
2438
- if (!isLedger && !isSection) {
2439
- refuse('no-record', `${located.rel} carries no execute-stage vault: ledger and no ${vaultStandardsLeaf.STANDARDS_CONSULTED_HEADING} section to back-fill`);
2440
- return;
2441
- }
2442
- const anchored = isLedger
2443
- ? ['startCommit', 'endCommit', 'anchorBackfill', 'anchorsDropped'].some((k) => k in st.predicate)
2444
- : section.anchors !== undefined
2445
- || section.problems.some((p) => p.kind === vaultStandardsLeaf.STANDARDS_PROBLEM_KIND.ANCHOR_MALFORMED);
1454
+ const section = vaultStandardsLeaf.parseConsultedSection(text);
1455
+ if (!section.present || section.planId === null) {
1456
+ refuse('no-record', `${located.rel} carries no ${vaultStandardsLeaf.STANDARDS_CONSULTED_HEADING} section to back-fill`);
1457
+ return;
1458
+ }
1459
+ const anchored = section.anchors !== undefined
1460
+ || section.problems.some((p) => p.kind === vaultStandardsLeaf.STANDARDS_PROBLEM_KIND.ANCHOR_MALFORMED);
2446
1461
  if (at === undefined) {
2447
1462
  // `--from` + `--attest`: the attestation (16 review iteration 3 CR-01, D-102).
2448
1463
  if (!anchored) {
@@ -2450,7 +1465,7 @@ function anchorBackfillVerb({ cwd, rest, raw, error, c, exec }) {
2450
1465
  return;
2451
1466
  }
2452
1467
  attestVerb({
2453
- cwd, exec, text, located, from, attest, isLedger, refuse, emit, traceLeaf,
1468
+ cwd, exec, text, located, from, attest, refuse, emit, traceLeaf,
2454
1469
  });
2455
1470
  return;
2456
1471
  }
@@ -2465,49 +1480,20 @@ function anchorBackfillVerb({ cwd, rest, raw, error, c, exec }) {
2465
1480
  }
2466
1481
  const backfill = { by: traceLeaf.resolveBackfillIdentity(cwd), at: new Date().toISOString() };
2467
1482
  const spans = lineSpans(text);
2468
- let next;
2469
- if (isLedger) {
2470
- const where = ledgerGeneratedAtLine(text, spans);
2471
- if (where === null) {
2472
- refuse('rewrite-unverifiable', `the ledger in ${located.rel} has no generatedAt line where the anchors belong`);
2473
- return;
2474
- }
2475
- // The serializer's lines, re-indented to this ledger's own indentation.
2476
- const lines = traceLeaf.traceAnchorLines({ ...(from !== undefined ? { startCommit: from } : {}), endCommit: at, anchorBackfill: backfill })
2477
- .map((l) => {
2478
- const depth = (l.length - l.trimStart().length) / 2 - 2;
2479
- return `${' '.repeat(where.keyIndent + depth * where.step)}${l.trimStart()}`;
2480
- });
2481
- next = insertLinesAfter(text, spans, where.index, lines);
2482
- const back = traceLeaf.parseVaultTraceBlock(next, located.rel);
2483
- const bp = back.present ? back.statement.predicate : {};
2484
- const agrees = back.present
2485
- && bp.endCommit === at
2486
- && bp.startCommit === from
2487
- && JSON.stringify(bp.anchorBackfill) === JSON.stringify(backfill)
2488
- && !('anchorsDropped' in bp)
2489
- && statementWithoutAnchors(back.statement) === statementWithoutAnchors(st);
2490
- if (!agrees) {
2491
- refuse('rewrite-unverifiable', `the back-filled ledger in ${located.rel} would not read back as written, so nothing was written`);
2492
- return;
2493
- }
2494
- }
2495
- else {
2496
- const live = vaultStandardsLeaf.findLiveSection(text, vaultStandardsLeaf.STANDARDS_CONSULTED_HEADING);
2497
- const lastBody = live.body[live.body.length - 1];
2498
- const line = vaultStandardsLeaf.formatStandardsAnchorLine({ start: from ?? null, end: at, backfill });
2499
- next = insertLinesAfter(text, spans, lastBody, [line]);
2500
- const back = vaultStandardsLeaf.parseConsultedSection(next);
2501
- const before = section;
2502
- const expected = { ...(from !== undefined ? { start: from } : {}), end: at, backfill };
2503
- const strip = (p) => JSON.stringify({ ...p, anchors: undefined, headingLine: undefined });
2504
- const agrees = back.present
2505
- && JSON.stringify(back.anchors) === JSON.stringify({ end: expected.end, ...(from !== undefined ? { start: from } : {}), backfill })
2506
- && strip(back) === strip(before);
2507
- if (!agrees) {
2508
- refuse('rewrite-unverifiable', `the back-filled section in ${located.rel} would not read back as written, so nothing was written`);
2509
- return;
2510
- }
1483
+ const live = vaultStandardsLeaf.findLiveSection(text, vaultStandardsLeaf.STANDARDS_CONSULTED_HEADING);
1484
+ const lastBody = live.body[live.body.length - 1];
1485
+ const line = vaultStandardsLeaf.formatStandardsAnchorLine({ start: from ?? null, end: at, backfill });
1486
+ const next = insertLinesAfter(text, spans, lastBody, [line]);
1487
+ const back = vaultStandardsLeaf.parseConsultedSection(next);
1488
+ const before = section;
1489
+ const expected = { ...(from !== undefined ? { start: from } : {}), end: at, backfill };
1490
+ const strip = (p) => JSON.stringify({ ...p, anchors: undefined, headingLine: undefined });
1491
+ const agrees = back.present
1492
+ && JSON.stringify(back.anchors) === JSON.stringify({ end: expected.end, ...(from !== undefined ? { start: from } : {}), backfill })
1493
+ && strip(back) === strip(before);
1494
+ if (!agrees) {
1495
+ refuse('rewrite-unverifiable', `the back-filled section in ${located.rel} would not read back as written, so nothing was written`);
1496
+ return;
2511
1497
  }
2512
1498
  const tmp = `${located.abs}.${process.pid}.tmp`;
2513
1499
  try {
@@ -2526,7 +1512,7 @@ function anchorBackfillVerb({ cwd, rest, raw, error, c, exec }) {
2526
1512
  ok: true,
2527
1513
  action: 'backfill',
2528
1514
  file: located.rel,
2529
- family: isLedger ? 'practices' : 'standards',
1515
+ family: 'standards',
2530
1516
  endCommit: at,
2531
1517
  ...(from !== undefined ? { startCommit: from } : {}),
2532
1518
  backfilledBy: backfill.by,
@@ -2566,10 +1552,10 @@ function anchorBackfillVerb({ cwd, rest, raw, error, c, exec }) {
2566
1552
  * whoever ran the command — what the CLI can read, not a verified identity —
2567
1553
  * and the envelope says so (`byVerified: false`).
2568
1554
  */
2569
- function attestVerb({ cwd, exec, text, located, from, attest, isLedger, refuse, emit, traceLeaf }) {
1555
+ function attestVerb({ cwd, exec, text, located, from, attest, refuse, emit, traceLeaf }) {
2570
1556
  const dirAbs = node_path_1.default.dirname(located.abs);
2571
1557
  const recordName = node_path_1.default.basename(located.abs);
2572
- const kind = isLedger ? vaultSeal.RECORDED_ARTIFACT_KIND.SUMMARY_LEDGER : vaultSeal.RECORDED_ARTIFACT_KIND.STANDARDS_CONSULTED;
1558
+ const kind = vaultSeal.RECORDED_ARTIFACT_KIND.STANDARDS_CONSULTED;
2573
1559
  const isAncestor = (a, d) => vaultSeal.isAnchorSha(a) && vaultSeal.isAnchorSha(d)
2574
1560
  && exec(['merge-base', '--is-ancestor', a, d], { cwd }).exitCode === 0;
2575
1561
  const anchors = vaultSeal.resolveRecordAnchors(text, kind, dirAbs, { isAncestor });
@@ -2676,7 +1662,7 @@ function attestVerb({ cwd, exec, text, located, from, attest, isLedger, refuse,
2676
1662
  ok: true,
2677
1663
  action: 'attest',
2678
1664
  file: located.rel,
2679
- family: isLedger ? 'practices' : 'standards',
1665
+ family: 'standards',
2680
1666
  from,
2681
1667
  endCommit: end,
2682
1668
  attested: listed,
@@ -2690,7 +1676,7 @@ function attestVerb({ cwd, exec, text, located, from, attest, isLedger, refuse,
2690
1676
  const DOOR_TEXT_MAX = 190;
2691
1677
  /**
2692
1678
  * A free-text value made safe for a door record line: the vault leaf's display
2693
- * sanitiser (controls, bidi, zero-width, fence markers), backticks turned into
1679
+ * sanitiser (controls, bidi, zero-width), backticks turned into
2694
1680
  * apostrophes, bounded. Empty or the literal `none` becomes `unknown`, because
2695
1681
  * `none` is how a door record spells an absent value.
2696
1682
  */
@@ -2704,31 +1690,15 @@ function doorText(value, opts = {}) {
2704
1690
  }
2705
1691
  /**
2706
1692
  * The vault coordinates a door worked under, or the degraded one-liner saying
2707
- * why there are none (D-12 "or") — read by the SAME pin reader the mode's own
2708
- * record verb uses, never a third reader of the manifest:
1693
+ * why there are none (D-12 "or") — read by the standards block verb's own pin
1694
+ * reader, `standardsLoadedPin` over the manifest of the one resolved cache; no
1695
+ * loaded pin records the degraded line `vault standards block` would write.
2709
1696
  *
2710
- * - practices: the trace scaffold's `resolvePin` over the one resolved cache.
2711
- * Only a `loaded` pin is recorded as coordinates; a degraded run (stale,
2712
- * fallback tier, ref drift, miss, no cache) records its reason instead,
2713
- * the same distinction the practices ledger draws;
2714
- * - standards: `standardsLoadedPin` over the manifest of the one resolved
2715
- * cache, the block verb's reader; no loaded pin records the degraded line
2716
- * `vault standards block` would write for it.
2717
- *
2718
- * The repo has already been through `redactCloneTarget` (both readers go
2719
- * through `readManifest`), so no credential reaches the record.
1697
+ * The repo has already been through `redactCloneTarget` (`readManifest`), so
1698
+ * no credential reaches the record.
2720
1699
  */
2721
- function doorVaultPin(cwd, mode, owner) {
1700
+ function doorVaultPin(cwd, owner) {
2722
1701
  const cacheDir = owner.resolveVaultCacheDir(cwd).dir;
2723
- if (mode === 'practices') {
2724
- // eslint-disable-next-line @typescript-eslint/no-require-imports
2725
- const traceLeaf = require('./vault-trace.cjs');
2726
- const pin = traceLeaf.resolvePin(cwd, cacheDir);
2727
- if (pin.state === 'loaded' && typeof pin.ref === 'string' && vaultSeal.ANCHOR_SHA_RE.test(pin.resolvedSha ?? '')) {
2728
- return { vault: { repo: doorText(pin.repo, { token: true }), ref: doorText(pin.ref, { token: true }), sha: pin.resolvedSha }, degraded: null };
2729
- }
2730
- return { vault: null, degraded: doorText(pin.reason ?? 'the vault was not loaded') };
2731
- }
2732
1702
  const pin = vaultStandardsLeaf.standardsLoadedPin(owner.readManifest(cacheDir));
2733
1703
  if (pin !== null && vaultSeal.ANCHOR_SHA_RE.test(pin.sha)) {
2734
1704
  return { vault: { repo: doorText(pin.repo, { token: true }), ref: doorText(pin.ref, { token: true }), sha: pin.sha }, degraded: null };
@@ -2937,15 +1907,13 @@ function doorRecordVerb({ cwd, rest, raw, error, c, exec }) {
2937
1907
  }
2938
1908
  phase = doorText(resolved.rel, { token: true });
2939
1909
  }
2940
- const modeVerdict = owner.vaultModeVerdict(cwd);
2941
- const mode = modeVerdict.active && modeVerdict.ok === true && (modeVerdict.mode === 'practices' || modeVerdict.mode === 'standards')
2942
- ? modeVerdict.mode
2943
- : null;
2944
- if (mode === null) {
2945
- error('The vault mode could not be resolved, so the record has no mode and was not written.', ERROR_REASON.SDK_FAIL_FAST);
1910
+ if (!owner.isVaultGateActive(cwd)) {
1911
+ error('The vault is off for this project, so there is no vault record to write.', ERROR_REASON.SDK_FAIL_FAST);
2946
1912
  return;
2947
1913
  }
2948
- const pin = doorVaultPin(cwd, mode, owner);
1914
+ // The record grammar keeps its `mode` field; standards is the only mode.
1915
+ const mode = 'standards';
1916
+ const pin = doorVaultPin(cwd, owner);
2949
1917
  let existing = '';
2950
1918
  if (node_fs_1.default.existsSync(located.abs)) {
2951
1919
  const size = node_fs_1.default.statSync(located.abs).size;