@pareto4/pp-core 1.13.0 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/.claude-plugin/plugin.json +3 -3
  2. package/README.md +26 -9
  3. package/agents/pp-code-reviewer.md +2 -1
  4. package/agents/pp-debug-session-manager.md +9 -6
  5. package/agents/pp-executor.md +10 -13
  6. package/agents/pp-plan-checker.md +4 -4
  7. package/agents/pp-planner.md +23 -9
  8. package/agents/pp-vault-reviewer.md +244 -21
  9. package/agents/pp-verifier.md +7 -3
  10. package/bin/install.js +10 -3
  11. package/commands/pp-execute-phase.md +1 -3
  12. package/commands/pp-ns-review.md +1 -1
  13. package/commands/pp-plan-phase.md +1 -2
  14. package/commands/pp-vault-review.md +5 -3
  15. package/commands/pp-verify-work.md +1 -2
  16. package/gemini-extension.json +1 -1
  17. package/hooks/dist/pp-check-update.js +0 -0
  18. package/hooks/dist/pp-cursor-session-start.js +1 -1
  19. package/hooks/dist/pp-statusline.js +1 -1
  20. package/hooks/dist/pp-update-banner.js +0 -0
  21. package/hooks/pp-check-update.js +0 -0
  22. package/hooks/pp-cursor-session-start.js +1 -1
  23. package/hooks/pp-statusline.js +1 -1
  24. package/hooks/pp-update-banner.js +0 -0
  25. package/package.json +14 -19
  26. package/pareto-skills/_shared/create-project-board.sh +0 -0
  27. package/pareto-skills/pp-project-setup/SKILL.md +1 -1
  28. package/pp-core/bin/check-latest-version.cjs +0 -0
  29. package/pp-core/bin/detect-build-commands.sh +2 -2
  30. package/pp-core/bin/lib/capability-activation.cjs +83 -9
  31. package/pp-core/bin/lib/capability-registry.cjs +396 -82
  32. package/pp-core/bin/lib/capability-state.cjs +160 -4
  33. package/pp-core/bin/lib/capability-writer.cjs +19 -5
  34. package/pp-core/bin/lib/check-command-router.cjs +3334 -946
  35. package/pp-core/bin/lib/clusters.cjs +3 -0
  36. package/pp-core/bin/lib/command-aliases.cjs +0 -35
  37. package/pp-core/bin/lib/commands.cjs +9 -4
  38. package/pp-core/bin/lib/compose-bindings.cjs +251 -88
  39. package/pp-core/bin/lib/config-loader.cjs +7 -103
  40. package/pp-core/bin/lib/config-schema.cjs +1 -1
  41. package/pp-core/bin/lib/config.cjs +154 -62
  42. package/pp-core/bin/lib/decisions.cjs +4 -4
  43. package/pp-core/bin/lib/discovery-vault-check.cjs +246 -0
  44. package/pp-core/bin/lib/frontmatter.cjs +1 -38
  45. package/pp-core/bin/lib/git-base-branch.cjs +13 -11
  46. package/pp-core/bin/lib/git-provider.cjs +4 -4
  47. package/pp-core/bin/lib/init-command-router.cjs +5 -8
  48. package/pp-core/bin/lib/init-contract.cjs +648 -738
  49. package/pp-core/bin/lib/init-interactive.cjs +76 -4
  50. package/pp-core/bin/lib/init.cjs +24 -51
  51. package/pp-core/bin/lib/install-profiles.cjs +1 -1
  52. package/pp-core/bin/lib/io.cjs +8 -0
  53. package/pp-core/bin/lib/loop-resolver.cjs +189 -129
  54. package/pp-core/bin/lib/org-profile.cjs +469 -152
  55. package/pp-core/bin/lib/package-identity.cjs +3 -3
  56. package/pp-core/bin/lib/phase-command-router.cjs +1 -29
  57. package/pp-core/bin/lib/phase-scope.cjs +1 -1
  58. package/pp-core/bin/lib/phase.cjs +11 -253
  59. package/pp-core/bin/lib/platform-bindings.cjs +40 -19
  60. package/pp-core/bin/lib/practices-review-check.cjs +15 -9
  61. package/pp-core/bin/lib/profile-output.cjs +1 -1
  62. package/pp-core/bin/lib/shell-command-projection.cjs +104 -1
  63. package/pp-core/bin/lib/state.cjs +11 -85
  64. package/pp-core/bin/lib/template.cjs +1 -1
  65. package/pp-core/bin/lib/vault-audit.cjs +650 -126
  66. package/pp-core/bin/lib/vault-command-router.cjs +2604 -367
  67. package/pp-core/bin/lib/vault-coordinates.cjs +13 -4
  68. package/pp-core/bin/lib/vault-index.cjs +699 -0
  69. package/pp-core/bin/lib/vault-mode.cjs +235 -0
  70. package/pp-core/bin/lib/vault-practices.cjs +139 -72
  71. package/pp-core/bin/lib/vault-review.cjs +194 -451
  72. package/pp-core/bin/lib/vault-seal.cjs +1936 -0
  73. package/pp-core/bin/lib/vault-standards.cjs +1505 -0
  74. package/pp-core/bin/lib/vault-trace.cjs +607 -254
  75. package/pp-core/bin/lib/vault-update.cjs +9 -33
  76. package/pp-core/bin/lib/vault-write-back.cjs +70 -42
  77. package/pp-core/bin/lib/vault.cjs +2734 -920
  78. package/pp-core/bin/lib/verify.cjs +0 -11
  79. package/pp-core/bin/portable-timeout.sh +1 -1
  80. package/pp-core/bin/pp-tools.cjs +184 -54
  81. package/pp-core/bin/shared/config-defaults.manifest.json +0 -7
  82. package/pp-core/bin/shared/config-schema.manifest.json +0 -3
  83. package/pp-core/bin/shared/model-catalog.json +2 -2
  84. package/pp-core/bin/verify-reapply-patches.cjs +0 -0
  85. package/pp-core/org-profile.json +26 -0
  86. package/pp-core/references/autonomous-best-effort.md +8 -7
  87. package/pp-core/references/loop-hook-dispatch.md +0 -25
  88. package/pp-core/references/planner-gap-closure.md +0 -1
  89. package/pp-core/references/planner-tdd.md +0 -1
  90. package/pp-core/references/planner-vault-practices.md +17 -34
  91. package/pp-core/references/planner-vault-standards.md +320 -0
  92. package/pp-core/references/planning-config.md +0 -8
  93. package/pp-core/references/practice-arithmetic-contract.md +121 -0
  94. package/pp-core/references/tdd.md +0 -1
  95. package/pp-core/references/verifier-practice-verdicts.md +263 -0
  96. package/pp-core/templates/README.md +1 -0
  97. package/pp-core/templates/VAULT-REVIEW.md +202 -0
  98. package/pp-core/templates/phase-prompt.md +2 -6
  99. package/pp-core/templates/practices/README.md +20 -23
  100. package/pp-core/workflows/_runtime-launcher.snippet.sh +0 -0
  101. package/pp-core/workflows/add-tests.md +55 -4
  102. package/pp-core/workflows/audit-fix.md +63 -2
  103. package/pp-core/workflows/audit-milestone.md +1 -1
  104. package/pp-core/workflows/autonomous.md +85 -11
  105. package/pp-core/workflows/check-todos.md +13 -2
  106. package/pp-core/workflows/code-review-fix.md +59 -3
  107. package/pp-core/workflows/code-review.md +10 -1
  108. package/pp-core/workflows/complete-milestone.md +1 -1
  109. package/pp-core/workflows/debug.md +54 -4
  110. package/pp-core/workflows/discovery-sprint/prefill.md +19 -7
  111. package/pp-core/workflows/discovery-sprint/prototype.md +46 -49
  112. package/pp-core/workflows/discovery-sprint/session-13-gates.md +2 -0
  113. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +1 -3
  114. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +1 -3
  115. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +1 -3
  116. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +20 -24
  117. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +1 -3
  118. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +1 -3
  119. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +1 -3
  120. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +1 -3
  121. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +1 -3
  122. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +1 -6
  123. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +1 -3
  124. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +1 -3
  125. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -3
  126. package/pp-core/workflows/discovery-sprint/templates/context-template.md +1 -1
  127. package/pp-core/workflows/discovery-sprint/vault-context.md +213 -0
  128. package/pp-core/workflows/discovery-sprint.md +7 -3
  129. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +4 -4
  130. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +43 -1
  131. package/pp-core/workflows/discovery-to-build.md +10 -2
  132. package/pp-core/workflows/discuss-phase.md +3 -10
  133. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +329 -0
  134. package/pp-core/workflows/execute-phase/steps/client-context-gate.md +3 -2
  135. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -1
  136. package/pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md +98 -0
  137. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
  138. package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +6 -8
  139. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +115 -14
  140. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +322 -20
  141. package/pp-core/workflows/execute-phase/steps/verifier-practice-payload.md +165 -0
  142. package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +163 -0
  143. package/pp-core/workflows/execute-phase.md +155 -128
  144. package/pp-core/workflows/execute-plan.md +56 -6
  145. package/pp-core/workflows/fast.md +53 -4
  146. package/pp-core/workflows/help/modes/full.md +3 -3
  147. package/pp-core/workflows/init/steps/client-context-capture.md +101 -0
  148. package/pp-core/workflows/init/steps/gitlab-provisioning.md +130 -0
  149. package/pp-core/workflows/init/steps/project-md-deferral-narration.md +66 -0
  150. package/pp-core/workflows/init/steps/reachability-outcome.md +70 -0
  151. package/pp-core/workflows/init.md +232 -272
  152. package/pp-core/workflows/new-project.md +31 -23
  153. package/pp-core/workflows/plan-phase.md +98 -45
  154. package/pp-core/workflows/quick.md +117 -3
  155. package/pp-core/workflows/secure-phase.md +1 -1
  156. package/pp-core/workflows/ship.md +57 -28
  157. package/pp-core/workflows/undo.md +1 -1
  158. package/pp-core/workflows/update.md +3 -3
  159. package/pp-core/workflows/validate-phase.md +58 -3
  160. package/pp-core/workflows/vault-review.md +282 -22
  161. package/pp-core/workflows/verify-work.md +12 -14
  162. package/scripts/changeset/cli.cjs +6 -5
  163. package/scripts/changeset/lint.cjs +0 -0
  164. package/scripts/changeset/new.cjs +33 -50
  165. package/scripts/dev-env.cjs +25 -85
  166. package/scripts/gen-capability-registry.cjs +199 -11
  167. package/scripts/gen-practice-arithmetic-contract.cjs +412 -0
  168. package/scripts/lib/allowlist-ratchet.cjs +1 -103
  169. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -2
  170. package/scripts/lint-docs-required.cjs +0 -0
  171. package/scripts/lint-pr-check-project-dir.cjs +1 -3
  172. package/scripts/lint-test-file-count.allowlist.json +12 -10
  173. package/scripts/practice-arithmetic-contract.allow.json +35 -0
  174. package/scripts/prompt-injection-scan.sh +22 -17
  175. package/scripts/record-vault-mode-golden.cjs +89 -0
  176. package/scripts/release-channel.cjs +0 -0
  177. package/scripts/sync-manifest-versions.cjs +5 -4
  178. package/scripts/sync-rulesets.sh +1 -1
  179. package/scripts/update-size-baseline.cjs +30 -274
  180. package/scripts/vault-calibrate.cjs +996 -0
  181. package/scripts/verify-opengsd-sweep.cjs +3 -5
  182. package/scripts/workflow-size.cjs +9 -61
  183. package/pp-core/bin/lib/dispatch-plan.cjs +0 -483
  184. package/pp-core/bin/lib/install-run.cjs +0 -536
  185. package/pp-core/bin/lib/loop-gates.cjs +0 -213
  186. package/pp-core/bin/lib/phase-manifest.cjs +0 -527
  187. package/pp-core/bin/lib/phase-tier.cjs +0 -480
  188. package/pp-core/bin/lib/plan-graph.cjs +0 -368
  189. package/pp-core/references/executor-vault-trace.md +0 -47
  190. package/pp-core/references/planner-plan-graph.md +0 -66
  191. package/pp-core/workflows/discovery-sprint/vault-fetch.md +0 -37
  192. package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +0 -45
  193. package/pp-core/workflows/execute-phase/steps/install-run.md +0 -29
  194. package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +0 -23
  195. package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +0 -73
  196. package/pp-core/workflows/execute-phase/steps/vault-door-gate.md +0 -19
  197. package/pp-core/workflows/plan-phase/steps/phase-tier.md +0 -43
  198. package/scripts/lib/size-budget.cjs +0 -311
  199. package/scripts/lint-neutrality-names.cjs +0 -694
  200. package/scripts/lint-org-coordinates.allowlist.json +0 -35
  201. package/scripts/lint-org-coordinates.cjs +0 -426
@@ -11,9 +11,11 @@
11
11
  * profile" — that carries a small set of vault/platform coordinates for a
12
12
  * single operator's projects. The package name lives entirely in the
13
13
  * consuming project's own `.planning/config.json`; pp-core's own source
14
- * never names one (ORG-01, SC1). An install that configures no such
15
- * pointer resolves nothing, warns about nothing, and probes no filesystem
16
- * beyond the one config read every project already pays for (ORG-02).
14
+ * never names one (ORG-01, SC1). Upstream, an install that configures no
15
+ * such pointer resolves nothing (ORG-02). This fork reverses ORG-02 on
16
+ * purpose: with no usable pointer it resolves the embedded profile
17
+ * (`EMBEDDED_ORG_PROFILE_NAME`, PBI 74316), and `org.profile: "none"` is the
18
+ * explicit opt-out.
17
19
  *
18
20
  * It imports ONLY node builtins (`node:fs`, `node:path`) at the top level —
19
21
  * no `./vault.cjs`, `./vault-coordinates.cjs`, `./vault-write-back.cjs`,
@@ -39,6 +41,13 @@
39
41
  * than writing a second regex; the lazy seam is how a zero-import leaf can
40
42
  * still do that.
41
43
  *
44
+ * The second exception (Vault Assurance 13-03, D-23) is a top-level import
45
+ * of `./vault-mode.cjs`, the mode vocabulary leaf. That module imports
46
+ * nothing at all, so it can never require this module back and cannot close
47
+ * a cycle; it is imported so `resolveVaultMode` and the `vault.mode`
48
+ * disposition quote the key and the two literals from their one owner rather
49
+ * than spelling them here.
50
+ *
42
51
  * This file's own prose and code deliberately never name a concrete org
43
52
  * profile package or any client — the pointer is described only as "the
44
53
  * npm package name an operator configures". `tests/org-profile.test.cjs`
@@ -48,16 +57,23 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
48
57
  return (mod && mod.__esModule) ? mod : { "default": mod };
49
58
  };
50
59
  Object.defineProperty(exports, "__esModule", { value: true });
51
- exports.HOST_FORBIDDEN_CHARS = exports.PROFILE_REJECTED_CLONE_TARGET_PATTERNS = exports.ORG_PROFILE_SEED_KEYS = exports.ORG_PROFILE_SEED_SECTION = exports.ORG_PROFILE_ALLOWLIST = exports.TRACE_ACCEPTED_PREDICATE_MAX = exports.TRACE_ACCEPTED_PREDICATES_CAP = exports.VAULT_TRACE_ACCEPTED_PREDICATES_KEY = exports.VAULT_REQUIRED_CONFIG_KEY = exports.ORG_PROFILE_DATA_FILE = exports.ORG_PROFILE_CONFIG_KEY = void 0;
60
+ exports.HOST_FORBIDDEN_CHARS = exports.PROFILE_REJECTED_CLONE_TARGET_PATTERNS = exports.ORG_PROFILE_SEED_KEYS = exports.ORG_PROFILE_INIT_SECTION = exports.ORG_PROFILE_SEED_SECTION = exports.ORG_PROFILE_ALLOWLIST = exports.VAULT_REQUIRED_CONFIG_KEY = exports.EMBEDDED_ORG_PROFILE_ENV = exports.EMBEDDED_ORG_PROFILE_NAME = exports.ORG_PROFILE_NONE = exports.ORG_PROFILE_DATA_FILE = exports.ORG_PROFILE_CONFIG_KEY = void 0;
61
+ exports.embeddedOrgProfilePath = embeddedOrgProfilePath;
62
+ exports.effectiveEmbeddedOrgProfilePath = effectiveEmbeddedOrgProfilePath;
52
63
  exports.resolveGlobalPackageRootCandidates = resolveGlobalPackageRootCandidates;
53
64
  exports.globalRootCandidatesFor = globalRootCandidatesFor;
65
+ exports.probeOrgProfilePackage = probeOrgProfilePackage;
54
66
  exports.resolveOrgProfileDocument = resolveOrgProfileDocument;
55
67
  exports.resolveOrgProfile = resolveOrgProfile;
56
68
  exports.readProfileValue = readProfileValue;
69
+ exports.booleanPolicyValue = booleanPolicyValue;
57
70
  exports.resolveVaultRequired = resolveVaultRequired;
58
- exports.resolveAcceptedTracePredicates = resolveAcceptedTracePredicates;
71
+ exports.resolveVaultMode = resolveVaultMode;
59
72
  const node_fs_1 = __importDefault(require("node:fs"));
60
73
  const node_path_1 = __importDefault(require("node:path"));
74
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
75
+ const vaultMode = require("./vault-mode.cjs");
76
+ const { VAULT_MODE_CONFIG_KEY, DEFAULT_VAULT_MODE, VAULT_MODE_REASON, isVaultMode } = vaultMode;
61
77
  // ─── Constants ───────────────────────────────────────────────────────────────
62
78
  /** The dotted `.planning/config.json` key naming the profile package. Deliberately
63
79
  * top-level, NOT nested under `vault.`, because the profile also supplies
@@ -66,41 +82,113 @@ exports.ORG_PROFILE_CONFIG_KEY = 'org.profile';
66
82
  /** The fixed filename read from the resolved package's own directory — never the
67
83
  * package's `main`/`exports` entry, and never loaded via `require` (D-08). */
68
84
  exports.ORG_PROFILE_DATA_FILE = 'org-profile.json';
69
- /** The dotted key naming the vault load policy, declared boolean (default
70
- * false) by `capabilities/vault/capability.json`. Named once here so neither
71
- * the allowlist, the value guard nor `resolveVaultRequired` restates it. */
72
- exports.VAULT_REQUIRED_CONFIG_KEY = 'vault.required';
73
85
  /**
74
- * The dotted key naming the practice-trace predicate types a READER accepts
75
- * besides pp-core's own (`TRACE_PREDICATE_TYPE` in src/vault-trace.cts). It
76
- * exists so a project that switches engine mid-engagement can keep reading
77
- * the traces its earlier engine wrote: the org profile of that engagement
78
- * lists the earlier engine's predicate URI here. Writers never use it — pp-core
79
- * always writes its own URI — and pp-core itself names no such URI.
86
+ * The `org.profile` value that opts a project out of every org profile, the
87
+ * embedded one included. Compared case-insensitively after trimming — the
88
+ * same spelling `pp-tools init` already accepts as a skip (UAT-13-F3).
80
89
  */
81
- exports.VAULT_TRACE_ACCEPTED_PREDICATES_KEY = 'vault.trace.accepted_predicates';
82
- /** At most this many accepted predicate URIs, each at most this long. */
83
- exports.TRACE_ACCEPTED_PREDICATES_CAP = 8;
84
- exports.TRACE_ACCEPTED_PREDICATE_MAX = 200;
90
+ exports.ORG_PROFILE_NONE = 'none';
91
+ /**
92
+ * The name `resolveOrgProfileDocument` reports as `packageName` for this
93
+ * fork's embedded profile. Not a valid npm package name shape for a scoped
94
+ * org profile, so it can never collide with a pointer that resolves.
95
+ *
96
+ * FORK-ONLY (PBI 74316). This fork serves a single organisation, so the
97
+ * profile ships inside pp-core as `pp-core/org-profile.json` instead of as a
98
+ * separate package. It carries coordinates only — never a vault ref: the
99
+ * vault version a project builds on is committed per project in its
100
+ * `.platform-bindings.json` (`client.vaultRef`), so no pp-core release can
101
+ * move it. Never offered upstream — see docs/PORT-BACK-DELTA.md.
102
+ */
103
+ exports.EMBEDDED_ORG_PROFILE_NAME = 'embedded';
104
+ /**
105
+ * Absolute path of the embedded profile document. Resolved against this
106
+ * module's own directory: the compiled file sits at `pp-core/bin/lib/`, and
107
+ * both this repo and every installed tree keep `pp-core/org-profile.json`
108
+ * two levels up.
109
+ */
110
+ function embeddedOrgProfilePath() {
111
+ return node_path_1.default.join(__dirname, '..', '..', exports.ORG_PROFILE_DATA_FILE);
112
+ }
113
+ /**
114
+ * Environment override for the embedded profile, for diagnostics and for test
115
+ * suites that exercise the upstream contract: `none` (or `off`, any case)
116
+ * disables it, any other non-blank value is a path to the document to use
117
+ * instead. Unset or blank leaves the shipped document in place. A project
118
+ * opts out through `org.profile: "none"`, never through this variable.
119
+ */
120
+ exports.EMBEDDED_ORG_PROFILE_ENV = 'PP_EMBEDDED_ORG_PROFILE';
121
+ /**
122
+ * The embedded document path in force for this process: `opts._embeddedPath`
123
+ * (test seam) when given, else the `EMBEDDED_ORG_PROFILE_ENV` override, else
124
+ * `embeddedOrgProfilePath()`. `null` means no embedded profile applies.
125
+ */
126
+ function effectiveEmbeddedOrgProfilePath(opts) {
127
+ if (opts && opts._embeddedPath !== undefined)
128
+ return opts._embeddedPath;
129
+ const override = process.env[exports.EMBEDDED_ORG_PROFILE_ENV];
130
+ if (typeof override === 'string' && override.trim() !== '') {
131
+ const trimmed = override.trim();
132
+ return ['none', 'off'].includes(trimmed.toLowerCase()) ? null : trimmed;
133
+ }
134
+ return embeddedOrgProfilePath();
135
+ }
136
+ /**
137
+ * The version reported for the embedded profile: the pp-core release that
138
+ * ships it. Probes the `VERSION` beside the document (the installer writes
139
+ * `pp-core/VERSION` into every installed tree), then the repo root's. Returns
140
+ * `null` when neither is readable and semver-shaped.
141
+ */
142
+ function readEmbeddedProfileVersion(dataPath) {
143
+ const dir = node_path_1.default.dirname(dataPath);
144
+ for (const candidate of [node_path_1.default.join(dir, 'VERSION'), node_path_1.default.join(dir, '..', 'VERSION')]) {
145
+ try {
146
+ const raw = node_fs_1.default.readFileSync(candidate, 'utf-8').trim();
147
+ if (/^\d+\.\d+\.\d+/.test(raw))
148
+ return raw;
149
+ }
150
+ catch {
151
+ // try the next candidate
152
+ }
153
+ }
154
+ return null;
155
+ }
156
+ /** The dotted key naming the vault load policy (02-04, GATE-05 / STRICT-01).
157
+ * Declared in `capabilities/vault/capability.json`'s federated config block,
158
+ * boolean, default `false`. Named here so neither the allowlist, the value
159
+ * guard nor `resolveVaultRequired` hand-restates the string. */
160
+ exports.VAULT_REQUIRED_CONFIG_KEY = 'vault.required';
85
161
  /**
86
162
  * D-06's fixed allowlist: a profile may contribute exactly these seven dotted
87
- * config keys. Entry order is not contractual; membership is. Anything else
163
+ * config keys (five coordinates and two policy keys; the seventh, vault.mode,
164
+ * is documented at its entry). Entry order is not contractual; membership is. Anything else
88
165
  * present in a profile document is dropped with a bounded warning.
89
166
  *
90
- * FIVE COORDINATES AND ONE POLICY. The first five answer where to fetch from.
91
- * `vault.required` answers whether the vault load is mandatory, so one
92
- * org-profile package can turn the load gate on across a fleet without a
93
- * per-repository edit. That is this list's sharpest widening: an `npm update`
94
- * of the profile package can move a build gate without a review in the
95
- * consuming repository. It is bounded three ways: project config outranks the
96
- * profile, so any project can opt out explicitly; the profile can only raise
97
- * the gate from the built-in default `false` (a gate turning on fails
98
- * closed); and the value guard accepts only the two boolean literals.
99
- * `vault.enabled` (arming the capability) stays off the list.
100
- *
101
- * `vault.trace.accepted_predicates` is READ-ONLY widening: it lets the trace
102
- * readers accept another engine's predicate URI (a list of https URIs). It
103
- * changes no writer and no gate polarity.
167
+ * FIVE COORDINATES AND ONE POLICY (02-04, D-VH-4 / STRICT-01). The first five
168
+ * members answer *where to fetch from* — a provider, three shared-vault
169
+ * coordinates and a platform host. `vault.required` answers something
170
+ * categorically different: *whether the fetch is mandatory*. It is the first
171
+ * POLICY member of this list, and it is here deliberately, so one org-profile
172
+ * package can turn the load gate on across a whole fleet without a per-repo
173
+ * edit.
174
+ *
175
+ * That is also this list's sharpest widening, and it is recorded rather than
176
+ * glossed (threat T-02-17): an `npm update` of the profile package can now
177
+ * move the value of a build gate without a code review in the consuming repo.
178
+ * Three things bound it. First, precedence — project config outranks the
179
+ * profile for every member, so any project can opt out explicitly, and
180
+ * `tests/org-profile.test.cjs` pins that direction with a non-vacuity partner
181
+ * rather than assuming it. Second, direction — the profile can only ever make
182
+ * the gate MORE strict from the built-in default of `false`, and a gate
183
+ * turning on is fail-closed. Third, the value guard below accepts only the two
184
+ * boolean literals for this key, so a profile cannot smuggle an arbitrary
185
+ * string into the policy.
186
+ *
187
+ * The warning in the block beneath this one is NOT retracted: it is still
188
+ * exactly right about the seed section, which stays off this list. A key that
189
+ * layers at runtime is a key an `npm update` can silently move — the
190
+ * difference is that for `vault.required` that consequence was weighed and
191
+ * accepted under a locked decision, and for `bindings.*` it was not.
104
192
  */
105
193
  exports.ORG_PROFILE_ALLOWLIST = Object.freeze([
106
194
  'vault.provider',
@@ -108,8 +196,15 @@ exports.ORG_PROFILE_ALLOWLIST = Object.freeze([
108
196
  'vault.fallback_ref',
109
197
  'vault.fallback_host',
110
198
  'platform.host',
111
- exports.VAULT_REQUIRED_CONFIG_KEY,
112
- exports.VAULT_TRACE_ACCEPTED_PREDICATES_KEY,
199
+ 'vault.required',
200
+ // Vault Assurance 13-03 (D-06): the second POLICY key, after vault.required
201
+ // (02-04). Unlike vault.required, a profile value here can make a gate LESS
202
+ // strict — `standards` turns the blocking vault.audit gate into
203
+ // not_applicable — so the widening is accepted and bounded in 13-CONTEXT
204
+ // D-24 (threat T-13-01): project config outranks the profile, only the two
205
+ // literals pass the disposition below, and the embedded fork profile does
206
+ // not seed it. vault.enabled and init.* stay absent from this list.
207
+ VAULT_MODE_CONFIG_KEY,
113
208
  ]);
114
209
  /**
115
210
  * The single top-level key naming the Phase 34 init-only seed section.
@@ -127,6 +222,15 @@ exports.ORG_PROFILE_ALLOWLIST = Object.freeze([
127
222
  * directly from the package.
128
223
  */
129
224
  exports.ORG_PROFILE_SEED_SECTION = 'bindings';
225
+ /**
226
+ * PBI 74299: the top-level key naming the init-answers section — fixed answers
227
+ * for `pp-tools init` questions (`client_name`, `provider`, `visibility`) and
228
+ * `derive_slugs`. Read ONLY by `src/init-contract.cts` through
229
+ * `resolveOrgProfileDocument`; like the seed section it is deliberately absent
230
+ * from `ORG_PROFILE_ALLOWLIST`, because it answers questions once at init and
231
+ * never layers a runtime config key.
232
+ */
233
+ exports.ORG_PROFILE_INIT_SECTION = 'init';
130
234
  /**
131
235
  * The five dotted leaf keys the seed section may carry, each prefixed with
132
236
  * `ORG_PROFILE_SEED_SECTION`. Membership is contractual; entry ORDER is not.
@@ -257,39 +361,66 @@ function describeUnknown(value) {
257
361
  return Object.prototype.toString.call(value);
258
362
  }
259
363
  }
260
- // ─── Global package roots ────────────────────────────────────────────────────
364
+ // ─── resolveGlobalPackageRoot ────────────────────────────────────────────────
261
365
  /**
262
- * D-03's "global" half of the two-path resolution ladder, as a pure heuristic
263
- * with NO subprocess spawn. Returns an ORDERED list of existing candidate
264
- * roots (a single root silently missed Homebrew layouts):
366
+ * D-03's "global" half of the two-path resolution ladder, as a pure
367
+ * heuristic with NO subprocess spawn (this repo avoids spawning a
368
+ * subprocess for a computation that does not strictly require one).
369
+ * Returns an ORDERED list of existing candidate roots (init-friction F1 —
370
+ * the single-root version silently missed Homebrew layouts):
265
371
  *
266
372
  * 1. `NPM_CONFIG_PREFIX` / `npm_config_prefix`, when set to a non-blank
267
373
  * string: `<prefix>/node_modules` on `win32`, `<prefix>/lib/node_modules`
268
374
  * otherwise.
269
- * 2. Every PATH directory containing a `node` entry, mapped through the same
270
- * `<bin>` → `<global tree>` rule — WITHOUT resolving symlinks. This is what
271
- * finds Homebrew: `process.execPath` resolves `/opt/homebrew/bin/node` into
272
- * the Cellar, whose `lib/node_modules` exists but holds only npm, while the
273
- * real global tree lives at `/opt/homebrew/lib/node_modules` beside the
274
- * PATH symlink.
275
- * 3. Derived from `process.execPath` — still correct for nvm/Volta layouts.
375
+ * 2. Every PATH directory containing a `node` entry, mapped through the
376
+ * same `<bin>` → `<global tree>` rule — WITHOUT resolving symlinks.
377
+ * This is what fixes Homebrew: `process.execPath` resolves
378
+ * `/opt/homebrew/bin/node` into the Cellar, whose `lib/node_modules`
379
+ * exists but holds only npm itself, while the real global tree lives at
380
+ * `/opt/homebrew/lib/node_modules` beside the PATH symlink.
381
+ * 3. Derived from `process.execPath` (the pre-existing heuristic) — still
382
+ * correct for nvm/Volta layouts.
276
383
  *
277
384
  * Candidates are deduped and filtered to `fs.existsSync`; resolution tries
278
- * each in order (first hit wins), so an existing-but-empty earlier root cannot
279
- * mask a later root that holds the package.
385
+ * each in order (first hit wins), so a wrong-but-existing root earlier in
386
+ * the list cannot mask a later root that actually holds the package.
387
+ *
388
+ * Accepted residual: a layout that honours none of the three sources will
389
+ * still not resolve a globally-installed profile — this degrades per D-09
390
+ * (falls through to the no-profile behaviour with a warning) rather than
391
+ * failing.
280
392
  *
281
- * Detection (the `/pp-init` org-profile hint and its invariant check) passes
282
- * `explicitPrefixOnly: true`: with `NPM_CONFIG_PREFIX` set, only the prefix
283
- * root is scanned, because a hint must never come from a tree the operator did
284
- * not point at. Detection roots are therefore a subset of resolution roots.
393
+ * pp-core's own install directory is deliberately NOT a candidate here:
394
+ * the AI-runtime install is a bare directory copy with no `package.json`
395
+ * and no `node_modules` of its own, so resolving relative to it would
396
+ * never find a sibling package.
285
397
  *
286
- * pp-core's own install directory is deliberately NOT a candidate: the
287
- * runtime install is a bare directory copy with no `node_modules` of its own.
398
+ * TWO READERS, ONE BUILDER, ONE FLAG (UAT-13-F2). RESOLUTION — the
399
+ * answer-time probe (`probeOrgProfilePackage`) and profile load
400
+ * (`resolveOrgProfileDocument`) — walks the full list above. DETECTION —
401
+ * `buildInitQuestions`' org-profile hint and `verifyInitInvariants`'
402
+ * INIT_QUESTIONS_ORG_PROFILE_UNASKED check in `src/init-contract.cts` —
403
+ * passes `explicitPrefixOnly: true`: when `NPM_CONFIG_PREFIX` /
404
+ * `npm_config_prefix` is set, only the prefix-derived root is scanned.
405
+ * An explicit prefix is the operator's — or a test harness's — statement
406
+ * of where the global tree is, and a hint or a violation must never be
407
+ * produced from a tree the operator did not point at. Detection roots are
408
+ * therefore a SUBSET of resolution roots: anything detected resolves by
409
+ * name, and a package that sits only on a PATH-derived root while an
410
+ * explicit prefix is set is still selectable by typing its name. Without a
411
+ * prefix both readers see the same list, which is what lets detection find
412
+ * a Homebrew layout at all.
288
413
  */
414
+ /** Bounds the PATH scan below — a DoS guard in the same convention as
415
+ * `PROFILE_FLATTEN_LEAF_CAP` (a pathological PATH cannot make resolution
416
+ * scan unboundedly many directories). */
289
417
  const GLOBAL_ROOT_PATH_SCAN_CAP = 64;
290
- /** npm's own `<bin dir>` → `<global node_modules>` mapping. */
418
+ /** The `<bin dir>` → `<global node_modules>` mapping npm itself uses:
419
+ * `<bin>/node_modules` on win32, `<bin>/../lib/node_modules` elsewhere. */
291
420
  function globalRootFromBinDir(binDir) {
292
- return process.platform === 'win32' ? node_path_1.default.join(binDir, 'node_modules') : node_path_1.default.resolve(binDir, '..', 'lib', 'node_modules');
421
+ return process.platform === 'win32'
422
+ ? node_path_1.default.join(binDir, 'node_modules')
423
+ : node_path_1.default.resolve(binDir, '..', 'lib', 'node_modules');
293
424
  }
294
425
  function resolveGlobalPackageRootCandidates(opts = {}) {
295
426
  const candidates = [];
@@ -306,16 +437,25 @@ function resolveGlobalPackageRootCandidates(opts = {}) {
306
437
  }
307
438
  });
308
439
  try {
440
+ // 1. Explicit prefix always wins — same rule as before.
309
441
  const prefixEnv = process.env.NPM_CONFIG_PREFIX ?? process.env.npm_config_prefix;
310
442
  if (typeof prefixEnv === 'string' && prefixEnv.trim() !== '') {
311
443
  const prefix = prefixEnv.trim();
312
444
  push(process.platform === 'win32' ? node_path_1.default.join(prefix, 'node_modules') : node_path_1.default.join(prefix, 'lib', 'node_modules'));
313
- // An explicit prefix is authoritative for detection; one that points
314
- // nowhere yields [] — never a fall-through to PATH.
445
+ // Detection mode (see the header): an explicit prefix is authoritative.
446
+ // A prefix that points nowhere yields [] — still the authoritative
447
+ // answer ("no global tree"), never a fall-through to PATH.
315
448
  if (opts.explicitPrefixOnly === true)
316
449
  return existing();
317
450
  }
318
- const pathDirs = (process.env.PATH ?? '')
451
+ // 2. PATH scan for the directory holding the `node` entry AS INVOKED
452
+ // (symlink not resolved). `process.execPath` resolves symlinks, so a
453
+ // Homebrew layout (`/opt/homebrew/bin/node` → Cellar) lands in the
454
+ // Cellar's private `lib/node_modules` (npm only) and misses the real
455
+ // global tree at `/opt/homebrew/lib/node_modules`. The un-resolved
456
+ // PATH entry is what npm's own prefix derivation is anchored to.
457
+ const pathEnv = process.env.PATH ?? '';
458
+ const pathDirs = pathEnv
319
459
  .split(node_path_1.default.delimiter)
320
460
  .filter((d) => d.trim() !== '')
321
461
  .slice(0, GLOBAL_ROOT_PATH_SCAN_CAP);
@@ -335,6 +475,9 @@ function resolveGlobalPackageRootCandidates(opts = {}) {
335
475
  }
336
476
  }
337
477
  }
478
+ // 3. execPath-derived (the pre-existing heuristic) — still the correct
479
+ // root for nvm/Volta-style layouts where the global tree sits beside
480
+ // the real binary.
338
481
  push(globalRootFromBinDir(node_path_1.default.dirname(process.execPath)));
339
482
  }
340
483
  catch {
@@ -342,23 +485,12 @@ function resolveGlobalPackageRootCandidates(opts = {}) {
342
485
  }
343
486
  return existing();
344
487
  }
345
- /**
346
- * Map the `_globalRoot` test seam (a single root, or `null` for "no global
347
- * root found") onto the candidate-list shape; when the seam is absent, run the
348
- * production heuristic. The seam wins over `explicitPrefixOnly`.
349
- */
350
- function globalRootCandidatesFor(opts) {
351
- if (opts && opts._globalRoot !== undefined) {
352
- return opts._globalRoot === null ? [] : [opts._globalRoot];
353
- }
354
- return resolveGlobalPackageRootCandidates({ explicitPrefixOnly: opts?.explicitPrefixOnly === true });
355
- }
356
488
  // ─── resolveProfilePackage ───────────────────────────────────────────────────
357
489
  /**
358
490
  * D-03's two-path resolution ladder. Attempt `require.resolve(pkgName +
359
491
  * '/package.json', { paths: [cwd] })` first and report `source: 'project'`;
360
- * on failure attempt the same with `paths: [root]` for each global candidate
361
- * root in order and report `source: 'global'`; otherwise `null`. The
492
+ * on failure attempt the same with `paths: [globalRoot]` when a global root
493
+ * was found and report `source: 'global'`; otherwise `null`. The
362
494
  * project-local hit always wins when both would resolve — the global
363
495
  * attempt is only ever reached when the project attempt threw.
364
496
  *
@@ -383,6 +515,37 @@ function resolveProfilePackage(pkgName, cwd, globalRoots) {
383
515
  }
384
516
  return null;
385
517
  }
518
+ /** Map the `_globalRoot` test seam (a single root, or `null` for "no global
519
+ * root found") onto the candidate-list shape resolution now uses; when the
520
+ * seam is absent, run the production heuristic. The seam wins over
521
+ * `explicitPrefixOnly`, which only shapes the production heuristic —
522
+ * detection passes it, resolution does not (see the builder's header). */
523
+ function globalRootCandidatesFor(opts) {
524
+ if (opts && opts._globalRoot !== undefined) {
525
+ return opts._globalRoot === null ? [] : [opts._globalRoot];
526
+ }
527
+ return resolveGlobalPackageRootCandidates({ explicitPrefixOnly: opts?.explicitPrefixOnly === true });
528
+ }
529
+ /**
530
+ * Answer-time probe for `/pp-init`'s `org_profile` question (init-friction
531
+ * F2): does `pkgName` resolve from the project or any global root right
532
+ * now? Returns the probed global roots so a rejection message can name
533
+ * them. Never throws (D-09) — any fault reports `resolved: false` with the
534
+ * roots collected so far.
535
+ */
536
+ function probeOrgProfilePackage(pkgName, cwd, opts) {
537
+ let probedGlobalRoots = [];
538
+ try {
539
+ probedGlobalRoots = globalRootCandidatesFor(opts);
540
+ const trimmed = typeof pkgName === 'string' ? pkgName.trim() : '';
541
+ if (trimmed === '')
542
+ return { resolved: false, probedGlobalRoots };
543
+ return { resolved: resolveProfilePackage(trimmed, cwd, probedGlobalRoots) !== null, probedGlobalRoots };
544
+ }
545
+ catch {
546
+ return { resolved: false, probedGlobalRoots };
547
+ }
548
+ }
386
549
  /**
387
550
  * Flatten a parsed profile document to dotted leaf paths, recursing only
388
551
  * through plain objects. Bounded on two axes (T-33-06, denial-of-service):
@@ -428,7 +591,17 @@ function flattenProfileDocument(doc) {
428
591
  * a DISPOSITION over the shared shape predicates, never as a parallel
429
592
  * regex (D-10's "one source of shape truth" requirement).
430
593
  *
431
- * Scalar guard, applied to every key: the value must be a string; its
594
+ * `vault.required` (02-04) is handled BEFORE the scalar guard, because it is
595
+ * the list's only boolean-typed key and the natural JSON an operator writes
596
+ * for it is `true`, not `"true"`. It accepts exactly four inputs — the JSON
597
+ * booleans `true`/`false` and the trimmed strings `'true'`/`'false'` — and
598
+ * carries forward the canonical `'true'`/`'false'` string, so `contributes`
599
+ * stays a `Record<string, string>`. Everything else is rejected, which means
600
+ * a profile cannot smuggle an arbitrary value into a build-gate policy: a
601
+ * typo like `"yes"` is dropped with a bounded warning and the gate stays at
602
+ * its `false` default rather than silently becoming truthy.
603
+ *
604
+ * Scalar guard, applied to every other key: the value must be a string; its
432
605
  * trimmed form must be non-empty; the UNTRIMMED value must carry no ASCII
433
606
  * control character; the TRIMMED value must carry no whitespace (so
434
607
  * leading/trailing padding — e.g. `' azure '` — is tolerated, matching
@@ -471,9 +644,8 @@ function flattenProfileDocument(doc) {
471
644
  */
472
645
  function profileValueDisposition(key, value) {
473
646
  try {
474
- // The one boolean-typed member, disposed of before the string-only scalar
475
- // guard below would reject its natural JSON form. Carried as the canonical
476
- // string "true" / "false".
647
+ // 02-04: the one boolean-typed allowlist member, disposed of before the
648
+ // string-only scalar guard below would reject its natural JSON form.
477
649
  if (key === exports.VAULT_REQUIRED_CONFIG_KEY) {
478
650
  if (value === true || value === false)
479
651
  return { ok: true, value: String(value) };
@@ -484,35 +656,25 @@ function profileValueDisposition(key, value) {
484
656
  }
485
657
  return { ok: false, reason: 'value must be the boolean true or false' };
486
658
  }
487
- // The one list-typed member. Carried forward as a canonical JSON array
488
- // string, so `contributes` stays a string map.
489
- if (key === exports.VAULT_TRACE_ACCEPTED_PREDICATES_KEY) {
490
- const list = Array.isArray(value) ? value : null;
491
- if (list === null || list.length === 0) {
492
- return { ok: false, reason: 'value must be a non-empty list of https URIs' };
493
- }
494
- if (list.length > exports.TRACE_ACCEPTED_PREDICATES_CAP) {
495
- return { ok: false, reason: `value lists more than ${exports.TRACE_ACCEPTED_PREDICATES_CAP} URIs` };
496
- }
497
- const kept = [];
498
- for (const item of list) {
499
- if (typeof item !== 'string')
500
- return { ok: false, reason: 'every entry must be a string' };
501
- const t = item.trim();
502
- if (t.length > exports.TRACE_ACCEPTED_PREDICATE_MAX) {
503
- return { ok: false, reason: `an entry is longer than ${exports.TRACE_ACCEPTED_PREDICATE_MAX} characters` };
504
- }
505
- if (!/^https:\/\/[^\s\x00-\x1f\x7f"'<>`\\]+$/.test(t)) {
506
- return { ok: false, reason: 'every entry must be an https URI with no whitespace or quotes' };
507
- }
508
- if (!kept.includes(t))
509
- kept.push(t);
510
- }
511
- return { ok: true, value: JSON.stringify(kept) };
659
+ // 13-03 (D-06, D-09): the mode accepts only its two literals, trimmed.
660
+ // Anything else is dropped through the existing warning / rejected[]
661
+ // path, and the mode then resolves to the default. The reason names the
662
+ // allowed values and never echoes the rejected one.
663
+ if (key === VAULT_MODE_CONFIG_KEY) {
664
+ if (isVaultMode(value))
665
+ return { ok: true, value: value.trim() };
666
+ return { ok: false, reason: 'value must be practices or standards' };
512
667
  }
513
668
  if (typeof value !== 'string') {
514
669
  return { ok: false, reason: 'value must be a string' };
515
670
  }
671
+ // PBI 74316: exactly "" for the shared repo is an explicit opt-out of the
672
+ // shared tier — the same decision an empty `vault.fallback_repo` in project
673
+ // config already records (see `resolveSharedCoordinates`). Whitespace is
674
+ // not "", so it still falls through to the blank rejection below.
675
+ if (key === 'vault.fallback_repo' && value === '') {
676
+ return { ok: true, value: '' };
677
+ }
516
678
  const trimmed = value.trim();
517
679
  if (trimmed === '') {
518
680
  return { ok: false, reason: 'value must not be blank' };
@@ -580,14 +742,23 @@ function profileValueDisposition(key, value) {
580
742
  * The `_globalRoot` option is the repo's `_`-prefixed test seam convention
581
743
  * (as used by `_vault` / `_exec` / `_writeBack` in
582
744
  * `src/vault-command-router.cts`); production callers omit it and the
583
- * module walks `resolveGlobalPackageRootCandidates()`. Passing `_globalRoot`
745
+ * module calls `resolveGlobalPackageRoot()`. Passing `_globalRoot`
584
746
  * explicitly (including `null`, to simulate "no global root found")
585
747
  * overrides the production heuristic for that one call.
586
748
  *
749
+ * FORK-ONLY (PBI 74316) — the embedded profile. Where upstream resolves
750
+ * nothing, this fork resolves `embeddedOrgProfilePath()`: an absent key, a
751
+ * blank or non-string pointer, and a configured package that cannot be found
752
+ * all yield the embedded document (source `embedded`). Only the explicit
753
+ * `ORG_PROFILE_NONE` pointer yields the fully-absent shape, so a project can
754
+ * still opt out. A configured package that DOES resolve always wins. The
755
+ * `_embeddedPath` option is the test seam for that document: a path replaces
756
+ * it, `null` simulates a tree that ships none; without it the
757
+ * `EMBEDDED_ORG_PROFILE_ENV` override applies (`effectiveEmbeddedOrgProfilePath`).
758
+ *
587
759
  * Never throws (D-09): any fault anywhere in this function degrades to the
588
- * fully-absent shape plus one bounded warning, exactly as if no profile
589
- * were configured. The document is read only via `fs.readFileSync` +
590
- * `JSON.parse`, never `require` (D-08).
760
+ * fully-absent shape plus one bounded warning. The document is read only via
761
+ * `fs.readFileSync` + `JSON.parse`, never `require` (D-08).
591
762
  */
592
763
  function resolveOrgProfileDocument(cwd, opts) {
593
764
  const absentDocShape = (warnings = []) => ({
@@ -600,25 +771,62 @@ function resolveOrgProfileDocument(cwd, opts) {
600
771
  document: {},
601
772
  warnings,
602
773
  });
774
+ const embeddedDocShape = (configured) => {
775
+ const dataPath = effectiveEmbeddedOrgProfilePath(opts);
776
+ if (dataPath === null) {
777
+ return { ...absentDocShape(), configured };
778
+ }
779
+ let parsed;
780
+ try {
781
+ parsed = JSON.parse(node_fs_1.default.readFileSync(dataPath, 'utf-8'));
782
+ }
783
+ catch {
784
+ return {
785
+ ...absentDocShape([bound('the embedded org profile could not be read — no coordinates contributed.')]),
786
+ configured,
787
+ };
788
+ }
789
+ if (!isObject(parsed)) {
790
+ return {
791
+ ...absentDocShape([bound('the embedded org profile is not a JSON object — no coordinates contributed.')]),
792
+ configured,
793
+ };
794
+ }
795
+ return {
796
+ configured,
797
+ resolved: true,
798
+ packageName: exports.EMBEDDED_ORG_PROFILE_NAME,
799
+ version: readEmbeddedProfileVersion(dataPath),
800
+ source: 'embedded',
801
+ dataPath,
802
+ document: parsed,
803
+ warnings: [],
804
+ };
805
+ };
603
806
  try {
604
807
  // Step 1: read the pointer. An absent key (undefined) or a blank string
605
- // is the fully-absent shape with ZERO warnings and no further
606
- // filesystem access — this is the ORG-02 contract. A present
607
- // non-string value is the one exception: same absent shape, plus
608
- // exactly one bounded warning.
808
+ // resolves the embedded profile with ZERO warnings. A present non-string
809
+ // value does the same, plus exactly one bounded warning. The explicit
810
+ // opt-out is the one pointer that resolves nothing.
609
811
  const rawPointer = cfgGet(cwd, exports.ORG_PROFILE_CONFIG_KEY, undefined);
610
812
  if (rawPointer === undefined) {
611
- return absentDocShape();
813
+ return embeddedDocShape(null);
612
814
  }
613
815
  if (typeof rawPointer !== 'string') {
614
816
  const echoed = sanitizeForWarning(describeUnknown(rawPointer));
615
- return absentDocShape([
616
- bound(`${exports.ORG_PROFILE_CONFIG_KEY} value "${echoed}" is not a string — ignoring, no org profile applied.`),
617
- ]);
817
+ const embedded = embeddedDocShape(null);
818
+ const consequence = embedded.resolved ? 'ignoring it, the embedded org profile applies.' : 'ignoring, no org profile applied.';
819
+ return {
820
+ ...embedded,
821
+ warnings: [bound(`${exports.ORG_PROFILE_CONFIG_KEY} value "${echoed}" is not a string — ${consequence}`), ...embedded.warnings],
822
+ };
618
823
  }
619
824
  const pkgName = rawPointer.trim();
620
825
  if (pkgName === '') {
621
- return absentDocShape();
826
+ return embeddedDocShape(null);
827
+ }
828
+ if (pkgName.toLowerCase() === exports.ORG_PROFILE_NONE) {
829
+ return { ...absentDocShape(), configured: pkgName };
622
830
  }
623
831
  // Step 2: resolve the package (project, then every global candidate).
624
832
  const globalRoots = globalRootCandidatesFor(opts);
@@ -628,11 +836,13 @@ function resolveOrgProfileDocument(cwd, opts) {
628
836
  const probed = globalRoots.length > 0
629
837
  ? `the global npm root(s) ${globalRoots.map((r) => sanitizeForWarning(r)).join(', ')}`
630
838
  : 'no locatable global npm root';
839
+ const embedded = embeddedDocShape(pkgName);
840
+ const fallback = embedded.resolved ? 'Falling back to the embedded org profile.' : 'Falling back to built-in defaults.';
631
841
  return {
632
- ...absentDocShape(),
633
- configured: pkgName,
842
+ ...embedded,
634
843
  warnings: [
635
- bound(`org profile "${echoed}" was not found — resolution was attempted from the project and ${probed} and found nothing. Falling back to built-in defaults.`),
844
+ bound(`org profile "${echoed}" was not found — resolution was attempted from the project and ${probed} and found nothing. ${fallback}`),
845
+ ...embedded.warnings,
636
846
  ],
637
847
  };
638
848
  }
@@ -737,6 +947,9 @@ function resolveOrgProfile(cwd, opts) {
737
947
  if (leaf.key === exports.ORG_PROFILE_SEED_SECTION || leaf.key.startsWith(`${exports.ORG_PROFILE_SEED_SECTION}.`)) {
738
948
  continue;
739
949
  }
950
+ if (leaf.key === exports.ORG_PROFILE_INIT_SECTION || leaf.key.startsWith(`${exports.ORG_PROFILE_INIT_SECTION}.`)) {
951
+ continue;
952
+ }
740
953
  if (!exports.ORG_PROFILE_ALLOWLIST.includes(leaf.key)) {
741
954
  ignored.push(leaf.key);
742
955
  if (ignoredWarningCount < PROFILE_IGNORED_WARNING_CAP) {
@@ -819,52 +1032,156 @@ function readProfileValue(cwd, key, opts) {
819
1032
  source: resolution.source,
820
1033
  };
821
1034
  }
1035
+ // ─── resolveVaultRequired (02-04, D-VH-4 / STRICT-01) ────────────────────────
1036
+ /**
1037
+ * The four inputs a boolean policy value may take, and nothing else — the SAME
1038
+ * disposition `profileValueDisposition` above already applies to this key.
1039
+ * Returns `undefined` for anything else, which is not the same as `false`: it
1040
+ * means "this value expresses no policy".
1041
+ *
1042
+ * 02-SECURITY UF-01. The project-config read used to be
1043
+ * `value === true || value === 'true'`, which collapsed every other spelling
1044
+ * to `false` — while `config-set vault.required TRUE|True|1|yes|on|enabled`
1045
+ * all answered `{"updated": true}` and wrote exactly those, so the supported
1046
+ * CLI could leave a build gate OFF while reporting success. UF-01 closed the
1047
+ * READ half (here); 07-14 closed the WRITE half, and `config-set` now refuses
1048
+ * every one of those spellings with a usage error and writes nothing, so this
1049
+ * function's `undefined` arm is reachable only from a hand-edited config.json
1050
+ * or an org profile. The asymmetry made it sharper: the LOWER-trust org
1051
+ * profile validated to four literals and warned, while project config — which
1052
+ * outranks it — did not, so a project `"TRUE"` both failed to arm the gate AND
1053
+ * suppressed a profile value that would have, because `found` rather than
1054
+ * validity decided precedence.
1055
+ *
1056
+ * Moved here from `src/vault.cts` together with the gate's ladder
1057
+ * (13-REVIEW CR-02), so the gate and the report share one reading.
1058
+ */
1059
+ function booleanPolicyValue(value) {
1060
+ if (value === true || value === false)
1061
+ return value;
1062
+ if (typeof value === 'string') {
1063
+ const trimmed = value.trim();
1064
+ if (trimmed === 'true')
1065
+ return true;
1066
+ if (trimmed === 'false')
1067
+ return false;
1068
+ }
1069
+ return undefined;
1070
+ }
822
1071
  /**
823
- * The ONE ladder for `vault.required`, which `computeVaultVerdict` (the
824
- * verdict owner) and `vault org-profile` both read:
1072
+ * Resolve the `vault.required` POLICY for `cwd`: project config first, the
1073
+ * org profile second, the registered default (`false`) last. This is the ONE
1074
+ * reader of the policy ladder (13-REVIEW CR-02): the load gate
1075
+ * (`resolveVaultRequiredFlag` in `src/vault.cts`) asks it after its own
1076
+ * `vault.enabled` check, and the `vault org-profile` report asks it for the
1077
+ * `effective` row — so the report and the gate cannot disagree about which
1078
+ * level won for the same inputs. Before CR-02 this function read only the
1079
+ * root `.planning/config.json` and counted only `=== true || === 'true'`,
1080
+ * while the gate read the workstream config first and trimmed, so a project
1081
+ * `" true"` or `"yes"` was reported as overriding a profile `true` that the
1082
+ * gate in fact enforced.
1083
+ *
1084
+ * PROJECT CONFIG OUTRANKS THE PROFILE (T-02-17), so the ordering below is
1085
+ * load-bearing. Level 1 reads RAW config through `resolveConfigKey` with
1086
+ * `config: {}` and `registry: {}`: the workstream config.json, then the root
1087
+ * config.json, each in either dotted form (nested first), with no schema
1088
+ * default. A project value of `false` is a real value and wins. A project
1089
+ * value that is PRESENT BUT UNPARSEABLE (`booleanPolicyValue` answers
1090
+ * `undefined`) does not consume the decision: the profile is consulted as if
1091
+ * the project had said nothing, because a value that expresses no policy must
1092
+ * not outrank one that does. It is returned as `ignoredConfigValue` so the
1093
+ * gate can say so on stderr; this function prints nothing.
825
1094
  *
826
- * 1. The project's `.planning/config.json`. A present value wins, whatever
827
- * it is: a JSON boolean is used as is; anything else is treated as false
828
- * and reported as invalid (the gate card names it), never passed on to the
829
- * profile — an explicit project value is the project's decision.
830
- * 2. The org profile, already narrowed to "true" / "false" by the value guard.
831
- * 3. The registered default, false.
1095
+ * This answers the POLICY, not whether a gate exists: on a project with
1096
+ * `vault.enabled` off no gate renders and `resolveVaultRequiredFlag` answers
1097
+ * `false` whatever this returns.
832
1098
  *
833
- * Never throws.
1099
+ * Never throws — a failed config read falls through to the profile, and the
1100
+ * profile seam already degrades to a miss on any fault.
834
1101
  */
835
1102
  function resolveVaultRequired(cwd, opts) {
1103
+ // Level 1 — project config, raw (ws then root, either dotted form).
1104
+ let ignoredConfigValue;
1105
+ let ignored = false;
836
1106
  try {
837
- const raw = cfgGet(cwd, exports.VAULT_REQUIRED_CONFIG_KEY, undefined);
838
- if (raw !== undefined) {
839
- return { required: raw === true, source: 'config', valid: typeof raw === 'boolean', raw, warnings: [] };
840
- }
841
- const profile = readProfileValue(cwd, exports.VAULT_REQUIRED_CONFIG_KEY, opts);
842
- if (profile.value === 'true' || profile.value === 'false') {
843
- return { required: profile.value === 'true', source: 'profile', valid: true, raw: undefined, warnings: profile.warnings };
1107
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1108
+ const activation = require('./capability-activation.cjs');
1109
+ const fromConfig = activation.resolveConfigKey(exports.VAULT_REQUIRED_CONFIG_KEY, { config: {}, cwd, registry: {} });
1110
+ if (fromConfig.found) {
1111
+ const decided = booleanPolicyValue(fromConfig.value);
1112
+ if (decided !== undefined)
1113
+ return { required: decided, source: 'config', warnings: [] };
1114
+ ignored = true;
1115
+ ignoredConfigValue = fromConfig.value;
844
1116
  }
845
- return { required: false, source: 'default', valid: true, raw: undefined, warnings: profile.warnings };
846
1117
  }
847
- catch {
848
- return { required: false, source: 'default', valid: true, raw: undefined, warnings: [] };
1118
+ catch { /* fall through to the profile */ }
1119
+ const withIgnored = (r) => (ignored ? { ...r, ignoredConfigValue } : r);
1120
+ // Level 2 — the org profile. Already narrowed to 'true'/'false' by
1121
+ // profileValueDisposition; booleanPolicyValue agrees with it by construction.
1122
+ const profile = readProfileValue(cwd, exports.VAULT_REQUIRED_CONFIG_KEY, opts);
1123
+ if (profile.value !== undefined) {
1124
+ const decided = booleanPolicyValue(profile.value);
1125
+ if (decided !== undefined)
1126
+ return withIgnored({ required: decided, source: 'profile', warnings: profile.warnings });
849
1127
  }
1128
+ // Level 3 — the registered default from capabilities/vault/capability.json.
1129
+ return withIgnored({ required: false, source: 'default', warnings: profile.warnings });
850
1130
  }
851
- // ─── resolveAcceptedTracePredicates ──────────────────────────────────────────
852
1131
  /**
853
- * The extra practice-trace predicate types the org profile lists under
854
- * `vault.trace.accepted_predicates` — `[]` when there is no profile, the key is
855
- * absent, or its value was refused by the value guard. Org profile only: a
856
- * project config value is not read (the key is not a project setting). Never
857
- * throws.
1132
+ * Resolve `vault.mode` for `cwd`: project config, then the org profile, then
1133
+ * the registered default `practices` — the same three levels as
1134
+ * `resolveVaultRequired` above.
1135
+ *
1136
+ * Like `resolveVaultRequired` (the one ladder both the load gate and the
1137
+ * report ask, since 13-REVIEW CR-02), this resolver is the ONE reader of the
1138
+ * mode for every mode-aware surface: the `vault
1139
+ * org-profile` report (13-05), the gate, refusals and status field (13-06)
1140
+ * and the parked practices surfaces (13-07). No surface re-reads the key
1141
+ * from config itself; the negative-grep anti-drift test in
1142
+ * `tests/vault-mode.test.cjs` is what keeps a second reader from appearing.
1143
+ *
1144
+ * Level 1 reads RAW config (D-21): `resolveConfigKey` with `config: {}` and
1145
+ * `registry: {}` reads the workstream config.json and then the root
1146
+ * config.json, each in either dotted form (nested first), with NO schema
1147
+ * default and no federated fallback. The merged config from
1148
+ * `config-loader.cts` is deliberately not used:
1149
+ * `federated-config`'s `_typeMatches` substitutes the schema default for an
1150
+ * out-of-enum value, and its warning is suppressed at load
1151
+ * (`config-loader.cts`), so `"whatever"` would silently read as `practices`.
1152
+ * A project value that is present but not one of the two literals returns
1153
+ * `ok: false` with `vault_mode_invalid` and NEVER falls back (D-08): an
1154
+ * unknown mode must be refused, not treated as practices.
1155
+ *
1156
+ * Level 2 is `readProfileValue`; `profileValueDisposition` has already
1157
+ * narrowed the profile value to one of the two literals or dropped it with a
1158
+ * warning (D-09), so an invalid profile value lands at level 3.
1159
+ *
1160
+ * KNOWN GAP (D-25), recorded rather than hidden: `config-get` reads only the
1161
+ * planning dir's config.json, while level 1 here reads the workstream config
1162
+ * and then the root config. In flat mode the two agree (pinned by the parity
1163
+ * test in `tests/vault-mode.test.cjs`); on a partitioned project a root-only
1164
+ * `vault.mode` can read differently. Filed as a todo; the same gap pre-exists
1165
+ * for `vault.required`.
1166
+ *
1167
+ * Never throws: both seams already degrade to a miss on any fault.
858
1168
  */
859
- function resolveAcceptedTracePredicates(cwd, opts) {
860
- try {
861
- const profile = readProfileValue(cwd, exports.VAULT_TRACE_ACCEPTED_PREDICATES_KEY, opts);
862
- if (typeof profile.value !== 'string')
863
- return [];
864
- const parsed = JSON.parse(profile.value);
865
- return Array.isArray(parsed) ? parsed.filter((v) => typeof v === 'string') : [];
1169
+ function resolveVaultMode(cwd, opts) {
1170
+ // Level 1 — project config, raw (ws then root, either dotted form).
1171
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1172
+ const activation = require('./capability-activation.cjs');
1173
+ const fromConfig = activation.resolveConfigKey(VAULT_MODE_CONFIG_KEY, { config: {}, cwd, registry: {} });
1174
+ if (fromConfig.found) {
1175
+ if (isVaultMode(fromConfig.value)) {
1176
+ return { ok: true, mode: fromConfig.value.trim(), source: 'config', warnings: [] };
1177
+ }
1178
+ return { ok: false, source: 'config', reason: VAULT_MODE_REASON.INVALID, warnings: [] };
866
1179
  }
867
- catch {
868
- return [];
1180
+ // Level 2 — the org profile, already narrowed by profileValueDisposition.
1181
+ const profile = readProfileValue(cwd, VAULT_MODE_CONFIG_KEY, opts);
1182
+ if (isVaultMode(profile.value)) {
1183
+ return { ok: true, mode: profile.value.trim(), source: 'profile', warnings: profile.warnings };
869
1184
  }
1185
+ // Level 3 — the registered default from capabilities/vault/capability.json.
1186
+ return { ok: true, mode: DEFAULT_VAULT_MODE, source: 'default', warnings: profile.warnings };
870
1187
  }