@pareto4/pp-core 1.12.1 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/.claude-plugin/plugin.json +3 -3
  2. package/README.md +26 -9
  3. package/agents/pp-code-reviewer.md +3 -2
  4. package/agents/pp-debug-session-manager.md +9 -6
  5. package/agents/pp-executor.md +7 -21
  6. package/agents/pp-plan-checker.md +4 -4
  7. package/agents/pp-planner.md +23 -9
  8. package/agents/pp-vault-reviewer.md +274 -0
  9. package/agents/pp-verifier.md +7 -3
  10. package/bin/install.js +10 -3
  11. package/commands/pp-execute-phase.md +1 -3
  12. package/commands/pp-ns-review.md +2 -1
  13. package/commands/pp-plan-phase.md +1 -2
  14. package/commands/pp-vault-review.md +35 -0
  15. package/commands/pp-verify-work.md +1 -2
  16. package/gemini-extension.json +1 -1
  17. package/hooks/dist/pp-check-update.js +0 -0
  18. package/hooks/dist/pp-cursor-session-start.js +1 -1
  19. package/hooks/dist/pp-statusline.js +1 -1
  20. package/hooks/dist/pp-update-banner.js +0 -0
  21. package/hooks/pp-check-update.js +0 -0
  22. package/hooks/pp-cursor-session-start.js +1 -1
  23. package/hooks/pp-statusline.js +1 -1
  24. package/hooks/pp-update-banner.js +0 -0
  25. package/package.json +14 -19
  26. package/pareto-skills/_shared/create-project-board.sh +0 -0
  27. package/pareto-skills/pp-project-setup/SKILL.md +1 -1
  28. package/pp-core/bin/check-latest-version.cjs +0 -0
  29. package/pp-core/bin/detect-build-commands.sh +2 -2
  30. package/pp-core/bin/lib/capability-activation.cjs +83 -9
  31. package/pp-core/bin/lib/capability-registry.cjs +526 -22
  32. package/pp-core/bin/lib/capability-state.cjs +160 -4
  33. package/pp-core/bin/lib/capability-writer.cjs +19 -5
  34. package/pp-core/bin/lib/check-command-router.cjs +3867 -274
  35. package/pp-core/bin/lib/clusters.cjs +4 -0
  36. package/pp-core/bin/lib/command-aliases.cjs +0 -28
  37. package/pp-core/bin/lib/commands.cjs +9 -4
  38. package/pp-core/bin/lib/compose-bindings.cjs +253 -76
  39. package/pp-core/bin/lib/config-loader.cjs +7 -103
  40. package/pp-core/bin/lib/config-schema.cjs +1 -1
  41. package/pp-core/bin/lib/config.cjs +154 -62
  42. package/pp-core/bin/lib/decisions.cjs +104 -27
  43. package/pp-core/bin/lib/discovery-vault-check.cjs +246 -0
  44. package/pp-core/bin/lib/frontmatter.cjs +1 -1
  45. package/pp-core/bin/lib/git-base-branch.cjs +138 -10
  46. package/pp-core/bin/lib/git-provider.cjs +4 -4
  47. package/pp-core/bin/lib/init-command-router.cjs +5 -8
  48. package/pp-core/bin/lib/init-contract.cjs +653 -629
  49. package/pp-core/bin/lib/init-interactive.cjs +77 -21
  50. package/pp-core/bin/lib/init.cjs +24 -51
  51. package/pp-core/bin/lib/install-profiles.cjs +1 -1
  52. package/pp-core/bin/lib/io.cjs +8 -0
  53. package/pp-core/bin/lib/loop-resolver.cjs +169 -47
  54. package/pp-core/bin/lib/org-profile.cjs +544 -53
  55. package/pp-core/bin/lib/package-identity.cjs +3 -3
  56. package/pp-core/bin/lib/phase-command-router.cjs +1 -23
  57. package/pp-core/bin/lib/phase-scope.cjs +1 -1
  58. package/pp-core/bin/lib/phase.cjs +11 -203
  59. package/pp-core/bin/lib/platform-bindings.cjs +40 -19
  60. package/pp-core/bin/lib/practices-review-check.cjs +15 -9
  61. package/pp-core/bin/lib/profile-output.cjs +1 -1
  62. package/pp-core/bin/lib/shell-command-projection.cjs +104 -1
  63. package/pp-core/bin/lib/state.cjs +11 -85
  64. package/pp-core/bin/lib/template.cjs +1 -1
  65. package/pp-core/bin/lib/vault-audit.cjs +2594 -0
  66. package/pp-core/bin/lib/vault-command-router.cjs +2756 -60
  67. package/pp-core/bin/lib/vault-coordinates.cjs +13 -4
  68. package/pp-core/bin/lib/vault-index.cjs +699 -0
  69. package/pp-core/bin/lib/vault-mode.cjs +235 -0
  70. package/pp-core/bin/lib/vault-practices.cjs +1194 -89
  71. package/pp-core/bin/lib/vault-review.cjs +2102 -0
  72. package/pp-core/bin/lib/vault-seal.cjs +1936 -0
  73. package/pp-core/bin/lib/vault-standards.cjs +1505 -0
  74. package/pp-core/bin/lib/vault-trace.cjs +3184 -0
  75. package/pp-core/bin/lib/vault-update.cjs +242 -0
  76. package/pp-core/bin/lib/vault-write-back.cjs +78 -31
  77. package/pp-core/bin/lib/vault.cjs +2713 -634
  78. package/pp-core/bin/lib/verify.cjs +0 -11
  79. package/pp-core/bin/portable-timeout.sh +1 -1
  80. package/pp-core/bin/pp-tools.cjs +184 -37
  81. package/pp-core/bin/shared/config-defaults.manifest.json +0 -7
  82. package/pp-core/bin/shared/config-schema.manifest.json +0 -3
  83. package/pp-core/bin/shared/model-catalog.json +1 -0
  84. package/pp-core/bin/verify-reapply-patches.cjs +0 -0
  85. package/pp-core/org-profile.json +26 -0
  86. package/pp-core/references/autonomous-best-effort.md +8 -7
  87. package/pp-core/references/loop-hook-dispatch.md +0 -14
  88. package/pp-core/references/planner-gap-closure.md +0 -1
  89. package/pp-core/references/planner-tdd.md +0 -1
  90. package/pp-core/references/planner-vault-practices.md +31 -3
  91. package/pp-core/references/planner-vault-standards.md +320 -0
  92. package/pp-core/references/planning-config.md +0 -8
  93. package/pp-core/references/practice-arithmetic-contract.md +121 -0
  94. package/pp-core/references/tdd.md +0 -1
  95. package/pp-core/references/verifier-practice-verdicts.md +263 -0
  96. package/pp-core/templates/README.md +1 -0
  97. package/pp-core/templates/VAULT-REVIEW.md +202 -0
  98. package/pp-core/templates/phase-prompt.md +2 -6
  99. package/pp-core/templates/practices/README.md +137 -11
  100. package/pp-core/templates/project-charter.md +226 -0
  101. package/pp-core/workflows/_runtime-launcher.snippet.sh +0 -0
  102. package/pp-core/workflows/add-tests.md +55 -0
  103. package/pp-core/workflows/audit-fix.md +63 -0
  104. package/pp-core/workflows/audit-milestone.md +1 -1
  105. package/pp-core/workflows/autonomous.md +85 -11
  106. package/pp-core/workflows/check-todos.md +13 -2
  107. package/pp-core/workflows/code-review-fix.md +59 -1
  108. package/pp-core/workflows/code-review.md +10 -1
  109. package/pp-core/workflows/complete-milestone.md +1 -1
  110. package/pp-core/workflows/debug.md +56 -2
  111. package/pp-core/workflows/discovery-sprint/prefill.md +21 -0
  112. package/pp-core/workflows/discovery-sprint/prototype.md +46 -48
  113. package/pp-core/workflows/discovery-sprint/session-13-gates.md +2 -0
  114. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +1 -3
  115. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +1 -3
  116. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +1 -3
  117. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +20 -24
  118. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +1 -3
  119. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +1 -3
  120. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +1 -3
  121. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +1 -3
  122. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +1 -3
  123. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +1 -6
  124. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +1 -3
  125. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +1 -3
  126. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -3
  127. package/pp-core/workflows/discovery-sprint/templates/context-template.md +1 -1
  128. package/pp-core/workflows/discovery-sprint/vault-context.md +213 -0
  129. package/pp-core/workflows/discovery-sprint.md +7 -3
  130. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +4 -4
  131. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +43 -1
  132. package/pp-core/workflows/discovery-to-build.md +9 -1
  133. package/pp-core/workflows/discuss-phase.md +3 -10
  134. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +329 -0
  135. package/pp-core/workflows/execute-phase/steps/client-context-gate.md +3 -2
  136. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -1
  137. package/pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md +98 -0
  138. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
  139. package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +6 -8
  140. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +115 -14
  141. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +322 -18
  142. package/pp-core/workflows/execute-phase/steps/verifier-practice-payload.md +165 -0
  143. package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +163 -0
  144. package/pp-core/workflows/execute-phase.md +154 -110
  145. package/pp-core/workflows/execute-plan.md +58 -4
  146. package/pp-core/workflows/fast.md +53 -0
  147. package/pp-core/workflows/help/modes/full.md +1 -0
  148. package/pp-core/workflows/init/steps/client-context-capture.md +101 -0
  149. package/pp-core/workflows/init/steps/gitlab-provisioning.md +130 -0
  150. package/pp-core/workflows/init/steps/project-md-deferral-narration.md +66 -0
  151. package/pp-core/workflows/init/steps/reachability-outcome.md +70 -0
  152. package/pp-core/workflows/init.md +232 -272
  153. package/pp-core/workflows/new-project.md +31 -23
  154. package/pp-core/workflows/plan-phase.md +99 -43
  155. package/pp-core/workflows/quick.md +120 -2
  156. package/pp-core/workflows/secure-phase.md +1 -1
  157. package/pp-core/workflows/ship.md +57 -24
  158. package/pp-core/workflows/update.md +3 -3
  159. package/pp-core/workflows/validate-phase.md +60 -1
  160. package/pp-core/workflows/vault-review.md +325 -0
  161. package/pp-core/workflows/verify-work.md +13 -12
  162. package/scripts/changeset/cli.cjs +6 -5
  163. package/scripts/changeset/lint.cjs +0 -0
  164. package/scripts/changeset/new.cjs +33 -50
  165. package/scripts/dev-env.cjs +25 -85
  166. package/scripts/gen-capability-registry.cjs +199 -0
  167. package/scripts/gen-practice-arithmetic-contract.cjs +412 -0
  168. package/scripts/lib/allowlist-ratchet.cjs +1 -103
  169. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -2
  170. package/scripts/lint-docs-required.cjs +0 -0
  171. package/scripts/lint-pr-check-project-dir.cjs +1 -3
  172. package/scripts/lint-test-file-count.allowlist.json +13 -2
  173. package/scripts/practice-arithmetic-contract.allow.json +35 -0
  174. package/scripts/prompt-injection-scan.sh +22 -15
  175. package/scripts/record-vault-mode-golden.cjs +89 -0
  176. package/scripts/release-channel.cjs +0 -0
  177. package/scripts/sync-manifest-versions.cjs +5 -4
  178. package/scripts/sync-rulesets.sh +1 -1
  179. package/scripts/update-size-baseline.cjs +30 -274
  180. package/scripts/vault-calibrate.cjs +996 -0
  181. package/scripts/verify-opengsd-sweep.cjs +3 -5
  182. package/scripts/workflow-size.cjs +9 -61
  183. package/pp-core/bin/lib/dispatch-plan.cjs +0 -472
  184. package/pp-core/bin/lib/install-run.cjs +0 -536
  185. package/pp-core/bin/lib/phase-tier.cjs +0 -467
  186. package/pp-core/bin/lib/plan-graph.cjs +0 -368
  187. package/pp-core/references/planner-plan-graph.md +0 -66
  188. package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +0 -45
  189. package/pp-core/workflows/execute-phase/steps/install-run.md +0 -29
  190. package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +0 -23
  191. package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +0 -72
  192. package/pp-core/workflows/plan-phase/steps/phase-tier.md +0 -34
  193. package/scripts/lib/size-budget.cjs +0 -311
  194. package/scripts/lint-neutrality-names.cjs +0 -694
  195. package/scripts/lint-org-coordinates.allowlist.json +0 -35
  196. package/scripts/lint-org-coordinates.cjs +0 -426
@@ -44,6 +44,10 @@ exports.CLUSTERS = Object.freeze({
44
44
  'eval-review',
45
45
  'add-tests',
46
46
  'secure-phase',
47
+ // Retroactive, non-gating review of a completed phase against the client
48
+ // vault's practices (Vault Assurance #71924, 06-07) — the same family as
49
+ // secure-phase / ui-review / validate-phase.
50
+ 'vault-review',
47
51
  ]),
48
52
  milestone: Object.freeze([
49
53
  'new-milestone',
@@ -698,20 +698,6 @@ exports.NON_FAMILY_COMMAND_ALIASES = [
698
698
  ],
699
699
  "mutation": true
700
700
  },
701
- {
702
- "canonical": "phase.dispatch-plan",
703
- "aliases": [
704
- "phase dispatch-plan"
705
- ],
706
- "mutation": false
707
- },
708
- {
709
- "canonical": "phase.install-run",
710
- "aliases": [
711
- "phase install-run"
712
- ],
713
- "mutation": true
714
- },
715
701
  {
716
702
  "canonical": "phase.mvp-mode",
717
703
  "aliases": [
@@ -719,20 +705,6 @@ exports.NON_FAMILY_COMMAND_ALIASES = [
719
705
  ],
720
706
  "mutation": false
721
707
  },
722
- {
723
- "canonical": "phase.tier",
724
- "aliases": [
725
- "phase tier"
726
- ],
727
- "mutation": false
728
- },
729
- {
730
- "canonical": "phase.tier-propose",
731
- "aliases": [
732
- "phase tier-propose"
733
- ],
734
- "mutation": true
735
- },
736
708
  {
737
709
  "canonical": "progress.bar",
738
710
  "aliases": [
@@ -47,6 +47,10 @@ const { extractFrontmatter } = frontmatter;
47
47
  const modelProfiles = require("./model-profiles.cjs");
48
48
  const { MODEL_PROFILES, VALID_PHASE_TYPES } = modelProfiles;
49
49
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
50
+ // 16-03: the one `.planning/` path test. The seal leaf's top level imports only
51
+ // Node built-ins and it never requires this module, so the edge adds no cycle.
52
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
53
+ const vaultSeal = require("./vault-seal.cjs");
50
54
  // ─── Phase Status ─────────────────────────────────────────────────────────────
51
55
  /**
52
56
  * Determine phase status by checking plan/summary counts AND verification state.
@@ -458,7 +462,7 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
458
462
  const phaseMatch = (files || []).join(' ').match(/(\d+(?:\.\d+)*)-/);
459
463
  if (phaseMatch) {
460
464
  const phaseNum = phaseMatch[1];
461
- // D-03: a prior milestone's archived phase whose number the current
465
+ // PBI 75323 D-03: a prior milestone's archived phase whose number the current
462
466
  // ROADMAP reuses names no branch (the phase-scope owner drops it).
463
467
  const phaseInfo = resolvePhaseForRead(cwd, phaseNum);
464
468
  if (phaseInfo) {
@@ -907,7 +911,7 @@ function cmdTodoMatchPhase(cwd, phase, raw) {
907
911
  const phaseKeywords = new Set(phaseText.split(/[\s\-_/.,;:()\[\]{}|]+/)
908
912
  .map(w => w.replace(/[^a-z0-9]/g, ''))
909
913
  .filter(w => w.length > 2 && !stopWords.has(w)));
910
- // Find phase directory to get expected file paths (D-03: through the
914
+ // Find phase directory to get expected file paths (PBI 75323 D-03: through the
911
915
  // phase-scope owner, so a prior milestone's archive never lends this phase its plans)
912
916
  const phaseInfoDisk = resolvePhaseForRead(cwd, phase);
913
917
  const phasePlans = [];
@@ -993,7 +997,7 @@ function cmdScaffold(cwd, type, options, raw) {
993
997
  const { phase, name } = options;
994
998
  const padded = phase ? normalizePhaseName(phase) : '00';
995
999
  const today = new Date().toISOString().split('T')[0];
996
- // Find phase directory. D-02 (todo "template-fill-writes-
1000
+ // Find phase directory. PBI 75323 / quick 261009-gvf D-02 (todo "template-fill-writes-
997
1001
  // into-an-archived-phase-directory"): context / uat / verification never write into a
998
1002
  // closed milestone's archive. The phase-scope owner hands back a live match only; an
999
1003
  // archive this phase still names is refused naming it, and a prior milestone's archive
@@ -1223,7 +1227,8 @@ function cmdCheckCommit(cwd, raw) {
1223
1227
  // commit_docs is false — check if any .planning/ files are staged
1224
1228
  const stagedResult = (0, shell_command_projection_cjs_1.execGit)(['diff', '--cached', '--name-only'], { cwd });
1225
1229
  if (stagedResult.exitCode === 0) {
1226
- const planningFiles = stagedResult.stdout.split('\n').filter(f => f.startsWith('.planning/') || f.startsWith('.planning\\'));
1230
+ // 16-03: the seal leaf owns the `.planning/` path test (both separators).
1231
+ const planningFiles = stagedResult.stdout.split('\n').filter((f) => vaultSeal.isPlanningPath(f));
1227
1232
  if (planningFiles.length > 0) {
1228
1233
  error(`commit_docs is false but ${planningFiles.length} .planning/ file(s) are staged:\n` +
1229
1234
  planningFiles.map(f => ` ${f}`).join('\n') +
@@ -31,8 +31,9 @@
31
31
  * `JSON.stringify`/object literals — never by `.md` shell string
32
32
  * concatenation.
33
33
  *
34
- * This module sits at the top of the CommonJS graph — nothing imports it —
35
- * so its top-level imports of `./org-profile.cjs`, `./platform-bindings.cjs`,
34
+ * Its importers today are `src/vault-update.cts`, `src/init-contract.cts`,
35
+ * `src/vault-practices.cts` and (quick 261003-lqi) `src/check-command-router.cts`
36
+ * — and this module imports none of them, so its top-level imports of `./org-profile.cjs`, `./platform-bindings.cjs`,
36
37
  * `./git-provider.cjs`, `./vault-coordinates.cjs`, `./io.cjs`,
37
38
  * `./project-root.cjs`, `./package-identity.cjs` and
38
39
  * `./shell-command-projection.cjs` create no require cycle: none of those
@@ -47,13 +48,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
47
48
  Object.defineProperty(exports, "__esModule", { value: true });
48
49
  exports.COMPOSE_BINDINGS_FLAGS = exports.SEED_VALUE_MAX_LENGTH = void 0;
49
50
  exports._resetPlatformTagMemoForTests = _resetPlatformTagMemoForTests;
51
+ exports.listStableTagsAt = listStableTagsAt;
52
+ exports.pickLatestStableTag = pickLatestStableTag;
53
+ exports.resolveLatestVaultTag = resolveLatestVaultTag;
54
+ exports.resolvePlatformRepoUrl = resolvePlatformRepoUrl;
55
+ exports.resolveLatestPlatformTag = resolveLatestPlatformTag;
50
56
  exports.resolveCreatedWithFrom = resolveCreatedWithFrom;
51
57
  exports.composeBindings = composeBindings;
52
58
  exports.redactComposeResultForEmission = redactComposeResultForEmission;
53
59
  exports.cmdComposeBindings = cmdComposeBindings;
60
+ exports.cmdPlatformLatestTag = cmdPlatformLatestTag;
54
61
  const node_fs_1 = __importDefault(require("node:fs"));
55
62
  const node_path_1 = __importDefault(require("node:path"));
56
- const node_child_process_1 = __importDefault(require("node:child_process"));
57
63
  // eslint-disable-next-line @typescript-eslint/no-require-imports
58
64
  const io = require("./io.cjs");
59
65
  const { output } = io;
@@ -354,17 +360,112 @@ const platformTagMemo = new Map();
354
360
  * `src/active-workstream-store.cts`'s controlling-TTY probe cache. */
355
361
  function _resetPlatformTagMemoForTests() {
356
362
  platformTagMemo.clear();
363
+ vaultTagMemo.clear();
364
+ }
365
+ /** Per-process memo for `resolveLatestVaultTag`, keyed by clone target. */
366
+ const vaultTagMemo = new Map();
367
+ /**
368
+ * List the stable `vX.Y.Z` tags at `url`, never shell-eval'd (T-34-25):
369
+ * `git ls-remote` is spawned with an argv array through `execGitLocal`, the
370
+ * owner of a vault git invocation's environment (D-116, D-117): the URL names
371
+ * the remote, but git still reads the config of the repository an inherited
372
+ * location variable selects, so the location is unset and transport is kept.
373
+ * Bounded by a 10 s `timeout`, `windowsHide` and spawnSync's default buffer
374
+ * (T-34-26), with the child's stderr piped and discarded — the owner prints
375
+ * nothing (`init-tag-lookup-raw-git-error`). `--` separates the target from
376
+ * the options so a flag-shaped value can never be read as one (T-29-26).
377
+ * Filtered with `isStableTripletSemver`, the single blessed predicate. THROWS
378
+ * on any git fault (a non-zero exit, which the owner also reports for a
379
+ * timeout or a missing git) — callers own the degrade-to-warning policy.
380
+ */
381
+ function listStableTagsAt(url, opts = {}) {
382
+ const lsOutput = (() => {
383
+ if (typeof opts._lsRemote === 'function')
384
+ return opts._lsRemote(url);
385
+ const result = (0, shell_command_projection_cjs_1.execGitLocal)(['ls-remote', '--tags', '--refs', '--sort=-v:refname', '--', url, 'v*'], { timeout: 10_000 });
386
+ if (result.exitCode !== 0 || result.error)
387
+ throw new Error(`git ls-remote exited ${result.exitCode}`);
388
+ return result.stdout;
389
+ })();
390
+ return lsOutput
391
+ .split('\n')
392
+ .map((line) => {
393
+ const m = line.match(/\brefs\/tags\/([^\s^]+)$/);
394
+ return m ? m[1].trim() : '';
395
+ })
396
+ .filter((t) => t && (0, semver_compare_cjs_1.isStableTripletSemver)(t));
397
+ }
398
+ /** The highest tag by `compareSemverCore`, or `null` for an empty list. */
399
+ function pickLatestStableTag(tags) {
400
+ if (tags.length === 0)
401
+ return null;
402
+ let latest = tags[0];
403
+ for (const t of tags) {
404
+ if ((0, semver_compare_cjs_1.compareSemverCore)(t, latest) > 0)
405
+ latest = t;
406
+ }
407
+ return latest;
408
+ }
409
+ /**
410
+ * FORK-ONLY (PBI 74316). Propose the vault version a NEW project builds on:
411
+ * the latest stable tag of the client vault repo. The embedded org profile
412
+ * seeds no vault ref on purpose — the version is a per-project choice,
413
+ * committed in `.platform-bindings.json`, so no pp-core release can move it.
414
+ * The proposal reaches the operator through the provenance table
415
+ * (`resolved-tag`) they confirm before anything is written.
416
+ *
417
+ * The clone target is composed by `composeCloneTarget`, so the scp-style SSH
418
+ * form passes through verbatim. Tagless, offline and unreachable all degrade
419
+ * to `{ tag: null }` plus one bounded warning naming the consequence: with no
420
+ * `client.vaultRef` the vault resolves `main`, i.e. the project is unpinned.
421
+ */
422
+ function resolveLatestVaultTag(repo, host, provider, opts = {}) {
423
+ const unpinned = (why) => ({
424
+ tag: null,
425
+ warnings: [
426
+ bound(`client.vaultRef could not be proposed — ${why}; the vault resolves "main" until the project is pinned with "pp-tools vault update <tag>".`),
427
+ ],
428
+ });
429
+ const target = (0, git_provider_cjs_1.composeCloneTarget)(provider, repo, host);
430
+ const cached = vaultTagMemo.get(target.url);
431
+ if (cached)
432
+ return cached;
433
+ let result;
434
+ try {
435
+ const tag = pickLatestStableTag(listStableTagsAt(target.url, opts));
436
+ result = tag ? { tag, warnings: [] } : unpinned('the vault repo has no stable v* tag');
437
+ }
438
+ catch {
439
+ result = unpinned('the tag lookup failed or was unreachable');
440
+ }
441
+ vaultTagMemo.set(target.url, result);
442
+ return result;
443
+ }
444
+ /**
445
+ * The platform repo URL rule — the ONE place the platform URL string is
446
+ * composed (quick 261003-my0). The host is `resolvePlatformHost(cwd).host`
447
+ * when it resolves, else `defaultHostForProvider(provider)`; it is never
448
+ * derived from the repo string (Phase 31 D-09). `resolveLatestPlatformTag`
449
+ * and, through it, `pp-tools init`, `check ship.platform-drift` and
450
+ * `query platform-latest-tag` all read this rule, so a shell copy cannot
451
+ * drift from it. May throw when `resolvePlatformHost` throws; the owner's
452
+ * try/catch is the degrade policy.
453
+ */
454
+ function resolvePlatformRepoUrl(cwd, repo, provider) {
455
+ const host = (0, vault_coordinates_cjs_1.resolvePlatformHost)(cwd).host ?? (0, vault_coordinates_cjs_1.defaultHostForProvider)(provider);
456
+ return `https://${host}/${repo}`;
357
457
  }
358
458
  /**
359
459
  * Resolve the latest STABLE tag for `repo` over the resolved host, never
360
- * shell-eval'd (T-34-25): `git ls-remote` is spawned with an argv array via
361
- * `childProcess.execFileSync`, bounded by `timeout`/`maxBuffer`/`windowsHide`
362
- * (T-34-26). The host is `resolvePlatformHost(cwd).host` when it resolves,
363
- * else `defaultHostForProvider(provider)` — the host is authoritative and
364
- * never derived from the repo string (Phase 31 D-09).
460
+ * shell-eval'd (T-34-25): `git ls-remote` is spawned with an argv array
461
+ * through `execGitLocal` (D-116, D-117), bounded by `timeout`/`windowsHide`
462
+ * (T-34-26). The URL comes from `resolvePlatformRepoUrl`: the host is
463
+ * `resolvePlatformHost(cwd).host` when it resolves, else
464
+ * `defaultHostForProvider(provider)` — the host is authoritative and never
465
+ * derived from the repo string (Phase 31 D-09).
365
466
  *
366
467
  * The child's stderr is piped into the parent's buffer rather than inherited
367
- * — an explicit `stdio: ['ignore', 'pipe', 'pipe']` — and deliberately
468
+ * — the owner spawns with `stdio: 'pipe'` — and deliberately
368
469
  * discarded rather than logged: the bounded warning this function returns on
369
470
  * failure is the single user-visible surface for a lookup failure. Inheriting
370
471
  * the child's stream let a raw git `fatal:` line reach the terminal during an
@@ -372,22 +473,29 @@ function _resetPlatformTagMemoForTests() {
372
473
  *
373
474
  * Every candidate ref is filtered with `isStableTripletSemver` and ordered
374
475
  * with `compareSemverCore` — the single blessed comparison source, never
375
- * reimplemented. An empty result degrades to `{ tag: null, warnings: [] }`
376
- * (tagless is valid, D-01/D-02); any thrown fault (offline, unreachable,
377
- * DNS failure, non-zero git exit) degrades to `{ tag: null, warnings: [one
476
+ * reimplemented. An empty result degrades to `{ tag: null, outcome:
477
+ * 'tagless', warnings: [] }` (tagless is valid, D-01/D-02); any thrown fault
478
+ * (offline, unreachable, DNS failure, non-zero git exit, an unresolvable
479
+ * host) degrades to `{ tag: null, outcome: 'unreachable', warnings: [one
378
480
  * bounded warning] }` — offline, unreachable and tagless all resolve to the
379
- * platform being unpinned, never a compose failure.
481
+ * platform being unpinned, never a compose failure, and `outcome` says which
482
+ * one was measured (quick 261003-my0).
483
+ *
484
+ * Quick 261003-lqi: the ship-time drift check (`cmdShipPlatformDrift`) calls
485
+ * this same owner, so init and ship resolve one URL from one host rule.
486
+ * Quick 261003-my0: the init.md / new-project.md preflights reach it through
487
+ * `query platform-latest-tag` (`cmdPlatformLatestTag`).
380
488
  */
381
489
  function resolveLatestPlatformTag(cwd, repo, provider, opts) {
382
490
  let url;
383
491
  try {
384
- const hostRes = (0, vault_coordinates_cjs_1.resolvePlatformHost)(cwd);
385
- const host = hostRes.host ?? (0, vault_coordinates_cjs_1.defaultHostForProvider)(provider);
386
- url = `https://${host}/${repo}`;
492
+ url = resolvePlatformRepoUrl(cwd, repo, provider);
387
493
  }
388
494
  catch {
389
495
  return {
390
496
  tag: null,
497
+ url: null,
498
+ outcome: 'unreachable',
391
499
  warnings: [
392
500
  bound('platform.tag could not be resolved — the tag lookup failed or was unreachable; the platform is unpinned.'),
393
501
  ],
@@ -398,37 +506,14 @@ function resolveLatestPlatformTag(cwd, repo, provider, opts) {
398
506
  return cached;
399
507
  let result;
400
508
  try {
401
- const lsOutput = typeof opts._lsRemote === 'function'
402
- ? opts._lsRemote(url)
403
- : node_child_process_1.default.execFileSync('git', ['ls-remote', '--tags', '--refs', '--sort=-v:refname', url, 'v*'], {
404
- encoding: 'utf-8',
405
- timeout: 10_000,
406
- maxBuffer: 512 * 1024,
407
- windowsHide: true,
408
- stdio: ['ignore', 'pipe', 'pipe'],
409
- });
410
- const tags = lsOutput
411
- .split('\n')
412
- .map((line) => {
413
- const m = line.match(/\brefs\/tags\/([^\s^]+)$/);
414
- return m ? m[1].trim() : '';
415
- })
416
- .filter((t) => t && (0, semver_compare_cjs_1.isStableTripletSemver)(t));
417
- if (tags.length === 0) {
418
- result = { tag: null, warnings: [] };
419
- }
420
- else {
421
- let latest = tags[0];
422
- for (const t of tags) {
423
- if ((0, semver_compare_cjs_1.compareSemverCore)(t, latest) > 0)
424
- latest = t;
425
- }
426
- result = { tag: latest, warnings: [] };
427
- }
509
+ const tag = pickLatestStableTag(listStableTagsAt(url, opts));
510
+ result = tag ? { tag, url, outcome: 'resolved', warnings: [] } : { tag: null, url, outcome: 'tagless', warnings: [] };
428
511
  }
429
512
  catch {
430
513
  result = {
431
514
  tag: null,
515
+ url,
516
+ outcome: 'unreachable',
432
517
  warnings: [
433
518
  bound('platform.tag could not be resolved — the tag lookup failed or was unreachable; the platform is unpinned.'),
434
519
  ],
@@ -533,6 +618,25 @@ function composeCandidate(cwd, seed, provider, opts) {
533
618
  setField('client.vaultRepo', seed.clientVaultRepo, 'profile-seed');
534
619
  setField('client.vaultRef', seed.clientVaultRef, 'profile-seed');
535
620
  setField('client.vaultHost', seed.clientVaultHost, 'profile-seed');
621
+ // client.vaultRef (resolved-tag) — FORK-ONLY (PBI 74316). When the seed
622
+ // names a vault repo but no ref, propose the repo's latest stable tag.
623
+ if (isUsableString(seed.clientVaultRepo) && !isUsableString(seed.clientVaultRef)) {
624
+ if (opts.offline === true) {
625
+ pushDeduped(warnings, [
626
+ bound('client.vaultRef was not proposed (offline) — the vault resolves "main" until the project is pinned with "pp-tools vault update <tag>".'),
627
+ ]);
628
+ }
629
+ else {
630
+ const vaultHost = isUsableString(seed.clientVaultHost)
631
+ ? seed.clientVaultHost
632
+ : (hostRes.host ?? (0, vault_coordinates_cjs_1.defaultHostForProvider)(provider));
633
+ const vaultTag = resolveLatestVaultTag(seed.clientVaultRepo, vaultHost, provider, opts);
634
+ pushDeduped(warnings, vaultTag.warnings);
635
+ if (vaultTag.tag) {
636
+ setField('client.vaultRef', vaultTag.tag, 'resolved-tag');
637
+ }
638
+ }
639
+ }
536
640
  // project.name / project.slug (project-answer)
537
641
  setField('project.name', cfgGet(cwd, 'project.name', undefined), 'project-answer');
538
642
  setField('project.slug', cfgGet(cwd, 'project.slug', undefined), 'project-answer');
@@ -796,6 +900,41 @@ function composeBindings(cwd, opts = {}) {
796
900
  };
797
901
  }
798
902
  }
903
+ // ─── Emission projection (VAULT-EGRESS-1) ─────────────────────────────────────
904
+ /** The seed short names whose bindings field is a coordinate — DERIVED from
905
+ * `SEED_KEY_SHORT_NAMES` and the coordinate field list, never retyped. */
906
+ const SEED_COORDINATE_SHORT_NAMES = Object.freeze(Object.entries(SEED_KEY_SHORT_NAMES)
907
+ .filter(([seedKey]) => platform_bindings_cjs_1.BINDINGS_COORDINATE_FIELDS.includes(seedKey.slice(org_profile_cjs_1.ORG_PROFILE_SEED_SECTION.length + 1)))
908
+ .map(([, short]) => short));
909
+ /**
910
+ * VAULT-EGRESS-1 (quick 261005-8py gap closure): the display-safe copy of a
911
+ * compose result, for EMISSION only. Every place a coordinate field can carry
912
+ * a value — `composed`, `seed`, each `provenance[].value` and each
913
+ * `drift[].profileValue/effectiveValue` — goes through the one owner. The
914
+ * comparisons that decided `drift` and `action` already ran on the RAW values
915
+ * inside `composeBindings`, and `composeBindings(..., { write: true })` writes
916
+ * the RAW composed object; neither path ever sees this projection.
917
+ */
918
+ function redactComposeResultForEmission(result) {
919
+ const seed = result.seed === undefined ? undefined : { ...result.seed };
920
+ if (seed !== undefined) {
921
+ for (const short of SEED_COORDINATE_SHORT_NAMES) {
922
+ if (typeof seed[short] === 'string')
923
+ seed[short] = (0, platform_bindings_cjs_1.redactCoordinateForEmission)(seed[short]);
924
+ }
925
+ }
926
+ return {
927
+ ...result,
928
+ ...(seed === undefined ? {} : { seed }),
929
+ composed: result.composed === null ? null : (0, platform_bindings_cjs_1.redactBindingsForEmission)(result.composed),
930
+ provenance: result.provenance.map((e) => ({ ...e, value: (0, platform_bindings_cjs_1.redactBindingsField)(e.field, e.value) })),
931
+ drift: result.drift.map((d) => ({
932
+ ...d,
933
+ profileValue: (0, platform_bindings_cjs_1.redactBindingsField)(d.key, d.profileValue),
934
+ effectiveValue: (0, platform_bindings_cjs_1.redactBindingsField)(d.key, d.effectiveValue),
935
+ })),
936
+ };
937
+ }
799
938
  // ─── cmdComposeBindings ───────────────────────────────────────────────────────
800
939
  /**
801
940
  * CLI adapter for `pp-tools query compose-bindings`. Resolves the project
@@ -803,39 +942,6 @@ function composeBindings(cwd, opts = {}) {
803
942
  * does, then calls `composeBindings` and emits the result as data — a
804
943
  * verdict is never a process error.
805
944
  */
806
- /**
807
- * Pack 6 D-07: the display-safe copy of a compose result, for EMISSION only.
808
- * Every remote coordinate field (`platform.repo`, `client.vaultRepo`) in
809
- * `composed`, `seed`, `provenance` and `drift` goes through the one owner
810
- * (`redactCloneTarget`, src/vault.cts); warnings go through the prose
811
- * redactor. `composeBindings` itself, its drift/action comparison and every
812
- * write keep the RAW values — never write or compare the result of this.
813
- */
814
- function redactComposeResultForEmission(result) {
815
- // eslint-disable-next-line @typescript-eslint/no-require-imports
816
- const v = require('./vault.cjs');
817
- const COORD = new Set(['platform.repo', 'client.vaultRepo']);
818
- const safe = (x) => (typeof x === 'string' ? v.redactCloneTarget(x) : x);
819
- const out = JSON.parse(JSON.stringify(result));
820
- if (out.composed && typeof out.composed === 'object') {
821
- for (const field of COORD) {
822
- const [ns, key] = field.split('.');
823
- const parent = out.composed[ns];
824
- if (parent && typeof parent === 'object' && typeof parent[key] === 'string')
825
- parent[key] = safe(parent[key]);
826
- }
827
- }
828
- if (out.seed) {
829
- if (typeof out.seed.platformRepo === 'string')
830
- out.seed.platformRepo = safe(out.seed.platformRepo);
831
- if (typeof out.seed.clientVaultRepo === 'string')
832
- out.seed.clientVaultRepo = safe(out.seed.clientVaultRepo);
833
- }
834
- out.provenance = (out.provenance ?? []).map((p) => (COORD.has(p.field) ? { ...p, value: safe(p.value) } : p));
835
- out.drift = (out.drift ?? []).map((d) => COORD.has(d.key) ? { ...d, profileValue: safe(d.profileValue), effectiveValue: safe(d.effectiveValue) } : d);
836
- out.warnings = (out.warnings ?? []).map((w) => (typeof w === 'string' ? v.redactRemoteText(w) : w));
837
- return out;
838
- }
839
945
  function cmdComposeBindings(cwd, options, raw) {
840
946
  let root;
841
947
  try {
@@ -847,3 +953,74 @@ function cmdComposeBindings(cwd, options, raw) {
847
953
  const result = composeBindings(root, { write: options.write === true, offline: options.offline === true });
848
954
  output(redactComposeResultForEmission(result), raw);
849
955
  }
956
+ // ─── cmdPlatformLatestTag ─────────────────────────────────────────────────────
957
+ /**
958
+ * CLI adapter for `pp-tools query platform-latest-tag` (quick 261003-my0).
959
+ * Its whole job is to run the owner — `resolveLatestPlatformTag`, the same
960
+ * lookup `pp-tools init` and `check ship.platform-drift` use — and print
961
+ * `{ present, pinned, repo, provider, url, latest, outcome }`, so a shell
962
+ * preflight (init.md / new-project.md Step 0) reads `--pick latest` instead
963
+ * of composing a URL of its own.
964
+ *
965
+ * Absent bindings → `{ present: false }` with NO network call (the parity
966
+ * `--help` smoke runs from a repo root that has no bindings file). Malformed
967
+ * bindings → `outcome: 'malformed'` and no lookup — inert data, never
968
+ * `error()`, the same posture as the drift check's malformed arm. No
969
+ * `platform.repo` → `outcome: 'no-repo'`. `args` is accepted for the
970
+ * dispatcher's uniform shape; no flags are read (`--pick`/`--raw`/`--cwd`
971
+ * are stripped before dispatch, and `--pick` prints `''` for a null field —
972
+ * the shell contract). The owner's warnings are init-time prose and are not
973
+ * forwarded.
974
+ */
975
+ function cmdPlatformLatestTag(cwd, args, raw) {
976
+ void args;
977
+ let root;
978
+ try {
979
+ root = (0, project_root_cjs_1.findProjectRoot)(cwd) || cwd;
980
+ }
981
+ catch {
982
+ root = cwd;
983
+ }
984
+ const bindingsPath = node_path_1.default.join(root, '.platform-bindings.json');
985
+ if (!node_fs_1.default.existsSync(bindingsPath)) {
986
+ output({ present: false }, raw);
987
+ return;
988
+ }
989
+ let provider;
990
+ try {
991
+ provider = (0, git_provider_cjs_1.resolveProvider)(root).provider;
992
+ }
993
+ catch {
994
+ provider = git_provider_cjs_1.DEFAULT_PROVIDER;
995
+ }
996
+ let parsed;
997
+ try {
998
+ parsed = JSON.parse(node_fs_1.default.readFileSync(bindingsPath, 'utf-8'));
999
+ }
1000
+ catch {
1001
+ parsed = null;
1002
+ }
1003
+ if (!isObject(parsed)) {
1004
+ output({ present: true, pinned: null, repo: null, provider, url: null, latest: null, outcome: 'malformed' }, raw);
1005
+ return;
1006
+ }
1007
+ const platform = isObject(parsed.platform) ? parsed.platform : null;
1008
+ const trimmedOrNull = (v) => typeof v === 'string' && v.trim() !== '' ? v.trim() : null;
1009
+ const pinned = trimmedOrNull(platform?.tag);
1010
+ const repo = trimmedOrNull(platform?.repo);
1011
+ if (repo === null) {
1012
+ output({ present: true, pinned, repo: null, provider, url: null, latest: null, outcome: 'no-repo' }, raw);
1013
+ return;
1014
+ }
1015
+ const result = resolveLatestPlatformTag(root, repo, provider, {});
1016
+ // VAULT-EGRESS-1: `repo` and `url` are display-safe; the lookup above used the raw repo.
1017
+ output({
1018
+ present: true,
1019
+ pinned,
1020
+ repo: (0, platform_bindings_cjs_1.redactCoordinateForEmission)(repo),
1021
+ provider,
1022
+ url: (0, platform_bindings_cjs_1.redactCoordinateForEmission)(result.url),
1023
+ latest: result.tag,
1024
+ outcome: result.outcome,
1025
+ }, raw);
1026
+ }
@@ -43,9 +43,6 @@ const model_catalog_cjs_1 = require("./model-catalog.cjs");
43
43
  // eslint-disable-next-line @typescript-eslint/no-require-imports
44
44
  const federatedConfigModule = require("./federated-config.cjs");
45
45
  const { mergeFederatedConfig } = federatedConfigModule;
46
- // eslint-disable-next-line @typescript-eslint/no-require-imports
47
- const capabilityActivationModule = require("./capability-activation.cjs");
48
- const { activationConfigFilesFor } = capabilityActivationModule;
49
46
  // The capability-registry.cjs is generated and lives in the same gsd-core/bin/lib/ output dir.
50
47
  // Both config-loader.cjs and capability-registry.cjs land in gsd-core/bin/lib/ at build time.
51
48
  // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
@@ -373,16 +370,16 @@ function _readsGlobalDefaults(cwd, ws) {
373
370
  }
374
371
  /**
375
372
  * Every config file `loadConfig(cwd, options)` reads, as absolute paths in read
376
- * order. ONE owner of "which config files does the loader read": `loadConfig`
377
- * takes its paths from the same `_configPathsFor`, so a reader that must refuse
378
- * an unreadable config (`unreadableConfigFiles` below) parses exactly these
379
- * files without restating the workstream ladder.
373
+ * order (16-04, D-28). ONE owner of "which config files does the gate read":
374
+ * `loadConfig` takes its paths from the same `_configPathsFor`, so a reader that
375
+ * must refuse an unreadable config (`vaultAdmissionVerdict` in src/vault.cts)
376
+ * can parse exactly these files without restating the workstream ladder.
380
377
  *
381
378
  * Why a caller needs it: `loadConfig` swallows a parse failure in every one of
382
379
  * these files (root → "workstream config stands alone", active → the defaults),
383
- * so its RESULT cannot say whether it read the project's config or read
384
- * nothing. Files that do not exist are still listed; absence is not a failure
385
- * for `loadConfig`, and the caller decides what it means.
380
+ * so its RESULT cannot say whether the gate read the project's config or read
381
+ * nothing (research C-4). Files that do not exist are still listed; absence is
382
+ * not a failure for `loadConfig`, and the caller decides what it means.
386
383
  */
387
384
  function configFilesFor(cwd, options = {}) {
388
385
  const ws = _activeWorkstreamFor(options);
@@ -395,97 +392,6 @@ function configFilesFor(cwd, options = {}) {
395
392
  files.push(_globalDefaultsPath());
396
393
  return files;
397
394
  }
398
- /** The error text is bounded; composed text never reaches the cap, but it holds anyway. */
399
- const UNREADABLE_ERROR_CAP = 200;
400
- /**
401
- * Describe why a config file could not be used WITHOUT echoing it. V8's
402
- * `JSON.parse` messages sometimes quote the input (`Unexpected token '}',
403
- * "{"a":tru}" is not valid JSON`), so only the position is carried over.
404
- */
405
- function _describeConfigReadFailure(err) {
406
- const e = err;
407
- if (e && typeof e.code === 'string' && /^[A-Z][A-Z0-9_]*$/.test(e.code))
408
- return `cannot be read (${e.code})`;
409
- const message = e && typeof e.message === 'string' ? e.message : '';
410
- const lineCol = /\(line (\d+) column (\d+)\)/.exec(message);
411
- if (lineCol)
412
- return `not valid JSON (line ${lineCol[1]}, column ${lineCol[2]})`;
413
- const position = /at position (\d+)/.exec(message);
414
- if (position)
415
- return `not valid JSON (at position ${position[1]})`;
416
- if (/Unexpected end of JSON input/.test(message))
417
- return 'not valid JSON (the text ends before the JSON does)';
418
- return 'not valid JSON';
419
- }
420
- /** How a refusal names a config file: never an absolute user path. */
421
- function _configDisplayPath(cwd, abs) {
422
- const under = (base) => {
423
- const rel = node_path_1.default.relative(base, abs);
424
- return rel !== '' && !rel.startsWith('..') && !node_path_1.default.isAbsolute(rel) ? rel.split(node_path_1.default.sep).join('/') : null;
425
- };
426
- const inProject = under(cwd);
427
- if (inProject !== null)
428
- return inProject;
429
- const homeOverride = process.env['GSD_HOME'];
430
- const inHomeOverride = homeOverride ? under(homeOverride) : null;
431
- if (inHomeOverride !== null)
432
- return `$GSD_HOME/${inHomeOverride}`;
433
- const inHome = under(node_os_1.default.homedir());
434
- if (inHome !== null)
435
- return `~/${inHome}`;
436
- return node_path_1.default.basename(abs);
437
- }
438
- /**
439
- * Every config file the hook resolver would read that EXISTS but cannot be
440
- * used: unreadable, not JSON, or JSON that is not an object (`loadConfig`
441
- * reads none of an array's or a null's keys and answers the defaults, the same
442
- * silent fallback as a parse failure). Empty when every file is usable or
443
- * absent: a MISSING config is a fresh project and reads as the defaults.
444
- *
445
- * The files are the union, in first-seen order, of the loader's list
446
- * (`configFilesFor`) and the activation resolver's raw levels
447
- * (`activationConfigFilesFor`, which can name a root config the loader never
448
- * opens under `GSD_PROJECT`). Reads go through `platformReadSync`, the reader
449
- * `loadConfig` uses, so a file the loader can read is never called unreadable.
450
- *
451
- * This is the ONE owner of "can the project's config be read". Both swallowing
452
- * readers (`loadConfig`, `resolveConfigKey`) stay tolerant for their many
453
- * callers; a caller whose answer governs a gate (`loop render-hooks`, the vault
454
- * load verdict) asks this first and refuses. Never throws.
455
- */
456
- function unreadableConfigFiles(cwd, options = {}) {
457
- const out = [];
458
- let files;
459
- try {
460
- files = [...new Set([...configFilesFor(cwd, options), ...activationConfigFilesFor(cwd)])];
461
- }
462
- catch (err) {
463
- // An invalid GSD_WORKSTREAM / GSD_PROJECT segment: the config cannot be located, so it cannot be read.
464
- return [{ file: '.planning/config.json', error: `cannot be located (${err instanceof Error ? err.message : String(err)})`.slice(0, UNREADABLE_ERROR_CAP) }];
465
- }
466
- for (const file of files) {
467
- let error = null;
468
- try {
469
- const raw = (0, shell_command_projection_cjs_1.platformReadSync)(file);
470
- if (raw === null)
471
- continue;
472
- const parsed = JSON.parse(raw);
473
- if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
474
- error = 'valid JSON but not an object, so none of its settings can be read';
475
- }
476
- }
477
- catch (err) {
478
- error = _describeConfigReadFailure(err);
479
- }
480
- if (error !== null)
481
- out.push({ file: _configDisplayPath(cwd, file), error: error.slice(0, UNREADABLE_ERROR_CAP) });
482
- }
483
- return out;
484
- }
485
- /** One line per unreadable file, `<file>: <error>`, for a refusal message. */
486
- function describeUnreadableConfig(unreadable) {
487
- return unreadable.map((u) => `${u.file}: ${u.error}`).join('; ');
488
- }
489
395
  function loadConfig(cwd, options = {}) {
490
396
  // When GSD_WORKSTREAM is set, load root config first so workstream config
491
397
  // can inherit from it. This prevents users from duplicating model_overrides,
@@ -839,8 +745,6 @@ function loadConfig(cwd, options = {}) {
839
745
  module.exports = {
840
746
  loadConfig,
841
747
  configFilesFor,
842
- unreadableConfigFiles,
843
- describeUnreadableConfig,
844
748
  isGitIgnored,
845
749
  CONFIG_DEFAULTS,
846
750
  _getConfigDefault,
@@ -29,7 +29,7 @@ function isCapabilityConfigKey(keyPath) {
29
29
  /**
30
30
  * The declared `values` of a capability `type: "enum"` config key, or `null`
31
31
  * when the key is not a capability key, is not an enum, or carries no value
32
- * list. 13-05 / D-07: this is the ONE lookup of enum values —
32
+ * list. Vault Assurance 13-05 / D-07: this is the ONE lookup of enum values —
33
33
  * `config-set` asks it rather than restating any list, so the registry stays
34
34
  * the single source of what an enum key may hold.
35
35
  */