@pareto4/pp-core 1.13.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/.claude-plugin/plugin.json +3 -3
  2. package/README.md +31 -12
  3. package/agents/pp-code-fixer.md +0 -11
  4. package/agents/pp-code-reviewer.md +1 -77
  5. package/agents/pp-debug-session-manager.md +9 -6
  6. package/agents/pp-executor.md +12 -29
  7. package/agents/pp-plan-checker.md +4 -4
  8. package/agents/pp-planner.md +34 -20
  9. package/agents/pp-vault-reviewer.md +100 -23
  10. package/agents/pp-verifier.md +1 -3
  11. package/bin/install.js +10 -3
  12. package/commands/pp-execute-phase.md +1 -3
  13. package/commands/pp-ns-manage.md +1 -1
  14. package/commands/pp-ns-review.md +1 -1
  15. package/commands/pp-plan-phase.md +1 -2
  16. package/commands/pp-vault-promote.md +7 -8
  17. package/commands/pp-vault-review.md +6 -5
  18. package/commands/pp-verify-work.md +1 -2
  19. package/gemini-extension.json +1 -1
  20. package/hooks/dist/pp-check-update.js +0 -0
  21. package/hooks/dist/pp-cursor-session-start.js +1 -1
  22. package/hooks/dist/pp-statusline.js +1 -1
  23. package/hooks/dist/pp-update-banner.js +0 -0
  24. package/hooks/pp-check-update.js +0 -0
  25. package/hooks/pp-cursor-session-start.js +1 -1
  26. package/hooks/pp-statusline.js +1 -1
  27. package/hooks/pp-update-banner.js +0 -0
  28. package/package.json +10 -18
  29. package/pareto-skills/_shared/create-project-board.sh +0 -0
  30. package/pareto-skills/pp-project-setup/SKILL.md +1 -1
  31. package/pp-core/bin/check-latest-version.cjs +0 -0
  32. package/pp-core/bin/detect-build-commands.sh +2 -2
  33. package/pp-core/bin/lib/capability-activation.cjs +83 -9
  34. package/pp-core/bin/lib/capability-registry.cjs +201 -229
  35. package/pp-core/bin/lib/capability-state.cjs +93 -1
  36. package/pp-core/bin/lib/capability-writer.cjs +3 -3
  37. package/pp-core/bin/lib/check-command-router.cjs +1906 -1514
  38. package/pp-core/bin/lib/clusters.cjs +3 -0
  39. package/pp-core/bin/lib/command-aliases.cjs +0 -35
  40. package/pp-core/bin/lib/commands.cjs +9 -4
  41. package/pp-core/bin/lib/compose-bindings.cjs +251 -88
  42. package/pp-core/bin/lib/config-loader.cjs +7 -103
  43. package/pp-core/bin/lib/config-schema.cjs +1 -1
  44. package/pp-core/bin/lib/config.cjs +151 -62
  45. package/pp-core/bin/lib/decisions.cjs +4 -4
  46. package/pp-core/bin/lib/discovery-vault-check.cjs +246 -0
  47. package/pp-core/bin/lib/frontmatter.cjs +1 -38
  48. package/pp-core/bin/lib/git-base-branch.cjs +11 -12
  49. package/pp-core/bin/lib/git-provider.cjs +9 -10
  50. package/pp-core/bin/lib/init-command-router.cjs +5 -8
  51. package/pp-core/bin/lib/init-contract.cjs +620 -738
  52. package/pp-core/bin/lib/init-interactive.cjs +62 -4
  53. package/pp-core/bin/lib/init.cjs +24 -51
  54. package/pp-core/bin/lib/install-profiles.cjs +1 -1
  55. package/pp-core/bin/lib/io.cjs +4 -2
  56. package/pp-core/bin/lib/loop-resolver.cjs +165 -129
  57. package/pp-core/bin/lib/org-profile.cjs +402 -161
  58. package/pp-core/bin/lib/package-identity.cjs +3 -3
  59. package/pp-core/bin/lib/phase-command-router.cjs +1 -29
  60. package/pp-core/bin/lib/phase-scope.cjs +1 -1
  61. package/pp-core/bin/lib/phase.cjs +11 -253
  62. package/pp-core/bin/lib/platform-bindings.cjs +40 -19
  63. package/pp-core/bin/lib/profile-output.cjs +1 -1
  64. package/pp-core/bin/lib/shell-command-projection.cjs +104 -1
  65. package/pp-core/bin/lib/state.cjs +11 -85
  66. package/pp-core/bin/lib/template.cjs +1 -1
  67. package/pp-core/bin/lib/vault-command-router.cjs +1682 -477
  68. package/pp-core/bin/lib/vault-coordinates.cjs +16 -7
  69. package/pp-core/bin/lib/vault-index.cjs +656 -0
  70. package/pp-core/bin/lib/vault-seal.cjs +1851 -0
  71. package/pp-core/bin/lib/vault-standards-promote.cjs +577 -0
  72. package/pp-core/bin/lib/vault-standards-review.cjs +293 -0
  73. package/pp-core/bin/lib/vault-standards.cjs +1504 -0
  74. package/pp-core/bin/lib/vault-trace.cjs +155 -2778
  75. package/pp-core/bin/lib/vault-update.cjs +10 -59
  76. package/pp-core/bin/lib/vault.cjs +2570 -950
  77. package/pp-core/bin/lib/verify.cjs +0 -11
  78. package/pp-core/bin/portable-timeout.sh +1 -1
  79. package/pp-core/bin/pp-tools.cjs +157 -54
  80. package/pp-core/bin/shared/config-defaults.manifest.json +0 -7
  81. package/pp-core/bin/shared/config-schema.manifest.json +0 -3
  82. package/pp-core/bin/shared/model-catalog.json +2 -2
  83. package/pp-core/bin/verify-reapply-patches.cjs +0 -0
  84. package/pp-core/org-profile.json +26 -0
  85. package/pp-core/references/autonomous-best-effort.md +8 -7
  86. package/pp-core/references/loop-hook-dispatch.md +0 -25
  87. package/pp-core/references/planner-gap-closure.md +0 -1
  88. package/pp-core/references/planner-tdd.md +0 -1
  89. package/pp-core/references/planner-vault-standards.md +311 -0
  90. package/pp-core/references/planning-config.md +0 -8
  91. package/pp-core/references/tdd.md +0 -1
  92. package/pp-core/templates/README.md +1 -0
  93. package/pp-core/templates/VAULT-REVIEW.md +56 -0
  94. package/pp-core/templates/phase-prompt.md +2 -6
  95. package/pp-core/workflows/_runtime-launcher.snippet.sh +0 -0
  96. package/pp-core/workflows/add-tests.md +55 -4
  97. package/pp-core/workflows/audit-fix.md +63 -2
  98. package/pp-core/workflows/audit-milestone.md +1 -1
  99. package/pp-core/workflows/autonomous.md +85 -11
  100. package/pp-core/workflows/check-todos.md +13 -2
  101. package/pp-core/workflows/code-review-fix.md +59 -3
  102. package/pp-core/workflows/code-review.md +10 -57
  103. package/pp-core/workflows/complete-milestone.md +1 -1
  104. package/pp-core/workflows/debug.md +54 -4
  105. package/pp-core/workflows/discovery-sprint/prefill.md +19 -7
  106. package/pp-core/workflows/discovery-sprint/prototype.md +46 -49
  107. package/pp-core/workflows/discovery-sprint/session-13-gates.md +2 -0
  108. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +1 -3
  109. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +1 -3
  110. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +1 -3
  111. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +20 -24
  112. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +1 -3
  113. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +1 -3
  114. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +1 -3
  115. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +1 -3
  116. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +1 -3
  117. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +1 -6
  118. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +1 -3
  119. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +1 -3
  120. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -3
  121. package/pp-core/workflows/discovery-sprint/templates/context-template.md +1 -1
  122. package/pp-core/workflows/discovery-sprint/vault-context.md +207 -0
  123. package/pp-core/workflows/discovery-sprint.md +7 -3
  124. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +4 -4
  125. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +43 -1
  126. package/pp-core/workflows/discovery-to-build.md +10 -2
  127. package/pp-core/workflows/discuss-phase.md +3 -10
  128. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +327 -0
  129. package/pp-core/workflows/execute-phase/steps/client-context-gate.md +3 -2
  130. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -1
  131. package/pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md +78 -0
  132. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
  133. package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +6 -8
  134. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +115 -14
  135. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +219 -20
  136. package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +147 -0
  137. package/pp-core/workflows/execute-phase.md +152 -128
  138. package/pp-core/workflows/execute-plan.md +54 -6
  139. package/pp-core/workflows/fast.md +53 -4
  140. package/pp-core/workflows/help/modes/full.md +3 -3
  141. package/pp-core/workflows/init/steps/client-context-capture.md +101 -0
  142. package/pp-core/workflows/init/steps/gitlab-provisioning.md +130 -0
  143. package/pp-core/workflows/init/steps/project-md-deferral-narration.md +66 -0
  144. package/pp-core/workflows/init/steps/reachability-outcome.md +70 -0
  145. package/pp-core/workflows/init.md +232 -272
  146. package/pp-core/workflows/new-project.md +31 -23
  147. package/pp-core/workflows/plan-phase.md +98 -45
  148. package/pp-core/workflows/quick.md +117 -3
  149. package/pp-core/workflows/secure-phase.md +1 -1
  150. package/pp-core/workflows/ship.md +57 -28
  151. package/pp-core/workflows/undo.md +1 -1
  152. package/pp-core/workflows/update.md +3 -3
  153. package/pp-core/workflows/validate-phase.md +58 -3
  154. package/pp-core/workflows/vault-review.md +161 -23
  155. package/pp-core/workflows/vault-write-back.md +73 -110
  156. package/pp-core/workflows/verify-work.md +12 -14
  157. package/scripts/changeset/cli.cjs +6 -5
  158. package/scripts/changeset/lint.cjs +0 -0
  159. package/scripts/changeset/new.cjs +33 -50
  160. package/scripts/dev-env.cjs +25 -85
  161. package/scripts/gen-capability-registry.cjs +120 -13
  162. package/scripts/lib/allowlist-ratchet.cjs +1 -103
  163. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -2
  164. package/scripts/lint-docs-required.cjs +0 -0
  165. package/scripts/lint-pr-check-project-dir.cjs +1 -3
  166. package/scripts/lint-test-file-count.allowlist.json +3 -19
  167. package/scripts/lint-test-file-count.cjs +5 -6
  168. package/scripts/prompt-injection-scan.sh +19 -17
  169. package/scripts/release-channel.cjs +0 -0
  170. package/scripts/sync-manifest-versions.cjs +5 -4
  171. package/scripts/sync-rulesets.sh +1 -1
  172. package/scripts/update-size-baseline.cjs +30 -274
  173. package/scripts/verify-opengsd-sweep.cjs +3 -5
  174. package/scripts/workflow-size.cjs +9 -61
  175. package/pp-core/bin/lib/dispatch-plan.cjs +0 -483
  176. package/pp-core/bin/lib/install-run.cjs +0 -536
  177. package/pp-core/bin/lib/loop-gates.cjs +0 -213
  178. package/pp-core/bin/lib/phase-manifest.cjs +0 -527
  179. package/pp-core/bin/lib/phase-tier.cjs +0 -480
  180. package/pp-core/bin/lib/plan-graph.cjs +0 -368
  181. package/pp-core/bin/lib/practices-review-check.cjs +0 -938
  182. package/pp-core/bin/lib/vault-audit.cjs +0 -2070
  183. package/pp-core/bin/lib/vault-practices.cjs +0 -1990
  184. package/pp-core/bin/lib/vault-review.cjs +0 -2359
  185. package/pp-core/bin/lib/vault-write-back.cjs +0 -877
  186. package/pp-core/references/executor-vault-trace.md +0 -47
  187. package/pp-core/references/planner-plan-graph.md +0 -66
  188. package/pp-core/references/planner-vault-practices.md +0 -112
  189. package/pp-core/templates/practices/README.md +0 -251
  190. package/pp-core/templates/practices/error-handling.md +0 -18
  191. package/pp-core/templates/practices/minimal-comments.md +0 -14
  192. package/pp-core/templates/practices/naming-convention.md +0 -18
  193. package/pp-core/templates/practices/practice-template.md +0 -23
  194. package/pp-core/workflows/discovery-sprint/vault-fetch.md +0 -37
  195. package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +0 -45
  196. package/pp-core/workflows/execute-phase/steps/install-run.md +0 -29
  197. package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +0 -23
  198. package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +0 -73
  199. package/pp-core/workflows/execute-phase/steps/vault-door-gate.md +0 -19
  200. package/pp-core/workflows/plan-phase/steps/phase-tier.md +0 -43
  201. package/scripts/lib/size-budget.cjs +0 -311
  202. package/scripts/lint-neutrality-names.cjs +0 -694
  203. package/scripts/lint-org-coordinates.allowlist.json +0 -35
  204. package/scripts/lint-org-coordinates.cjs +0 -426
@@ -44,6 +44,9 @@ 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 standards documents (`check vault-review`, a report) — the same
49
+ // family as secure-phase / ui-review / validate-phase.
47
50
  'vault-review',
48
51
  ]),
49
52
  milestone: Object.freeze([
@@ -698,27 +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
- {
716
- "canonical": "phase.manifest",
717
- "aliases": [
718
- "phase manifest"
719
- ],
720
- "mutation": true
721
- },
722
701
  {
723
702
  "canonical": "phase.mvp-mode",
724
703
  "aliases": [
@@ -726,20 +705,6 @@ exports.NON_FAMILY_COMMAND_ALIASES = [
726
705
  ],
727
706
  "mutation": false
728
707
  },
729
- {
730
- "canonical": "phase.tier",
731
- "aliases": [
732
- "phase tier"
733
- ],
734
- "mutation": false
735
- },
736
- {
737
- "canonical": "phase.tier-propose",
738
- "aliases": [
739
- "phase tier-propose"
740
- ],
741
- "mutation": true
742
- },
743
708
  {
744
709
  "canonical": "progress.bar",
745
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
+ * 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
@@ -49,13 +50,16 @@ exports.COMPOSE_BINDINGS_FLAGS = exports.SEED_VALUE_MAX_LENGTH = void 0;
49
50
  exports._resetPlatformTagMemoForTests = _resetPlatformTagMemoForTests;
50
51
  exports.listStableTagsAt = listStableTagsAt;
51
52
  exports.pickLatestStableTag = pickLatestStableTag;
53
+ exports.resolveLatestVaultTag = resolveLatestVaultTag;
54
+ exports.resolvePlatformRepoUrl = resolvePlatformRepoUrl;
55
+ exports.resolveLatestPlatformTag = resolveLatestPlatformTag;
52
56
  exports.resolveCreatedWithFrom = resolveCreatedWithFrom;
53
57
  exports.composeBindings = composeBindings;
54
58
  exports.redactComposeResultForEmission = redactComposeResultForEmission;
55
59
  exports.cmdComposeBindings = cmdComposeBindings;
60
+ exports.cmdPlatformLatestTag = cmdPlatformLatestTag;
56
61
  const node_fs_1 = __importDefault(require("node:fs"));
57
62
  const node_path_1 = __importDefault(require("node:path"));
58
- const node_child_process_1 = __importDefault(require("node:child_process"));
59
63
  // eslint-disable-next-line @typescript-eslint/no-require-imports
60
64
  const io = require("./io.cjs");
61
65
  const { output } = io;
@@ -356,17 +360,112 @@ const platformTagMemo = new Map();
356
360
  * `src/active-workstream-store.cts`'s controlling-TTY probe cache. */
357
361
  function _resetPlatformTagMemoForTests() {
358
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}`;
359
457
  }
360
458
  /**
361
459
  * Resolve the latest STABLE tag for `repo` over the resolved host, never
362
- * shell-eval'd (T-34-25): `git ls-remote` is spawned with an argv array via
363
- * `childProcess.execFileSync`, bounded by `timeout`/`maxBuffer`/`windowsHide`
364
- * (T-34-26). The host is `resolvePlatformHost(cwd).host` when it resolves,
365
- * else `defaultHostForProvider(provider)` — the host is authoritative and
366
- * 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).
367
466
  *
368
467
  * The child's stderr is piped into the parent's buffer rather than inherited
369
- * — an explicit `stdio: ['ignore', 'pipe', 'pipe']` — and deliberately
468
+ * — the owner spawns with `stdio: 'pipe'` — and deliberately
370
469
  * discarded rather than logged: the bounded warning this function returns on
371
470
  * failure is the single user-visible surface for a lookup failure. Inheriting
372
471
  * the child's stream let a raw git `fatal:` line reach the terminal during an
@@ -374,22 +473,29 @@ function _resetPlatformTagMemoForTests() {
374
473
  *
375
474
  * Every candidate ref is filtered with `isStableTripletSemver` and ordered
376
475
  * with `compareSemverCore` — the single blessed comparison source, never
377
- * reimplemented. An empty result degrades to `{ tag: null, warnings: [] }`
378
- * (tagless is valid, D-01/D-02); any thrown fault (offline, unreachable,
379
- * 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
380
480
  * bounded warning] }` — offline, unreachable and tagless all resolve to the
381
- * 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`).
382
488
  */
383
489
  function resolveLatestPlatformTag(cwd, repo, provider, opts) {
384
490
  let url;
385
491
  try {
386
- const hostRes = (0, vault_coordinates_cjs_1.resolvePlatformHost)(cwd);
387
- const host = hostRes.host ?? (0, vault_coordinates_cjs_1.defaultHostForProvider)(provider);
388
- url = `https://${host}/${repo}`;
492
+ url = resolvePlatformRepoUrl(cwd, repo, provider);
389
493
  }
390
494
  catch {
391
495
  return {
392
496
  tag: null,
497
+ url: null,
498
+ outcome: 'unreachable',
393
499
  warnings: [
394
500
  bound('platform.tag could not be resolved — the tag lookup failed or was unreachable; the platform is unpinned.'),
395
501
  ],
@@ -400,11 +506,14 @@ function resolveLatestPlatformTag(cwd, repo, provider, opts) {
400
506
  return cached;
401
507
  let result;
402
508
  try {
403
- result = { tag: pickLatestStableTag(listStableTagsAt(url, opts)), warnings: [] };
509
+ const tag = pickLatestStableTag(listStableTagsAt(url, opts));
510
+ result = tag ? { tag, url, outcome: 'resolved', warnings: [] } : { tag: null, url, outcome: 'tagless', warnings: [] };
404
511
  }
405
512
  catch {
406
513
  result = {
407
514
  tag: null,
515
+ url,
516
+ outcome: 'unreachable',
408
517
  warnings: [
409
518
  bound('platform.tag could not be resolved — the tag lookup failed or was unreachable; the platform is unpinned.'),
410
519
  ],
@@ -413,44 +522,6 @@ function resolveLatestPlatformTag(cwd, repo, provider, opts) {
413
522
  platformTagMemo.set(url, result);
414
523
  return result;
415
524
  }
416
- /**
417
- * List the stable `vX.Y.Z` tags at `url`, never shell-eval'd (T-34-25):
418
- * `git ls-remote` is spawned with an argv array, bounded by
419
- * `timeout`/`maxBuffer`/`windowsHide` (T-34-26), with the child's stderr piped
420
- * and discarded. `--` ends the options, so a flag-shaped target can never be
421
- * read as one. Filtered with `isStableTripletSemver`, the single blessed
422
- * predicate. THROWS on any git fault — callers own the degrade policy. Shared
423
- * by the platform-tag lookup and `vault update`.
424
- */
425
- function listStableTagsAt(url, opts = {}) {
426
- const lsOutput = typeof opts._lsRemote === 'function'
427
- ? opts._lsRemote(url)
428
- : node_child_process_1.default.execFileSync('git', ['ls-remote', '--tags', '--refs', '--sort=-v:refname', '--', url, 'v*'], {
429
- encoding: 'utf-8',
430
- timeout: 10_000,
431
- maxBuffer: 512 * 1024,
432
- windowsHide: true,
433
- stdio: ['ignore', 'pipe', 'pipe'],
434
- });
435
- return lsOutput
436
- .split('\n')
437
- .map((line) => {
438
- const m = line.match(/\brefs\/tags\/([^\s^]+)$/);
439
- return m ? m[1].trim() : '';
440
- })
441
- .filter((t) => t && (0, semver_compare_cjs_1.isStableTripletSemver)(t));
442
- }
443
- /** The highest tag by `compareSemverCore`, or `null` for an empty list. */
444
- function pickLatestStableTag(tags) {
445
- if (tags.length === 0)
446
- return null;
447
- let latest = tags[0];
448
- for (const t of tags) {
449
- if ((0, semver_compare_cjs_1.compareSemverCore)(t, latest) > 0)
450
- latest = t;
451
- }
452
- return latest;
453
- }
454
525
  /**
455
526
  * Read the first readable, semver-shaped `VERSION` file among the candidate
456
527
  * locations resolved against `baseDir`, returning `<binName>@<version>`.
@@ -547,6 +618,25 @@ function composeCandidate(cwd, seed, provider, opts) {
547
618
  setField('client.vaultRepo', seed.clientVaultRepo, 'profile-seed');
548
619
  setField('client.vaultRef', seed.clientVaultRef, 'profile-seed');
549
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
+ }
550
640
  // project.name / project.slug (project-answer)
551
641
  setField('project.name', cfgGet(cwd, 'project.name', undefined), 'project-answer');
552
642
  setField('project.slug', cfgGet(cwd, 'project.slug', undefined), 'project-answer');
@@ -810,6 +900,41 @@ function composeBindings(cwd, opts = {}) {
810
900
  };
811
901
  }
812
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
+ }
813
938
  // ─── cmdComposeBindings ───────────────────────────────────────────────────────
814
939
  /**
815
940
  * CLI adapter for `pp-tools query compose-bindings`. Resolves the project
@@ -817,39 +942,6 @@ function composeBindings(cwd, opts = {}) {
817
942
  * does, then calls `composeBindings` and emits the result as data — a
818
943
  * verdict is never a process error.
819
944
  */
820
- /**
821
- * Pack 6 D-07: the display-safe copy of a compose result, for EMISSION only.
822
- * Every remote coordinate field (`platform.repo`, `client.vaultRepo`) in
823
- * `composed`, `seed`, `provenance` and `drift` goes through the one owner
824
- * (`redactCloneTarget`, src/vault.cts); warnings go through the prose
825
- * redactor. `composeBindings` itself, its drift/action comparison and every
826
- * write keep the RAW values — never write or compare the result of this.
827
- */
828
- function redactComposeResultForEmission(result) {
829
- // eslint-disable-next-line @typescript-eslint/no-require-imports
830
- const v = require('./vault.cjs');
831
- const COORD = new Set(['platform.repo', 'client.vaultRepo']);
832
- const safe = (x) => (typeof x === 'string' ? v.redactCloneTarget(x) : x);
833
- const out = JSON.parse(JSON.stringify(result));
834
- if (out.composed && typeof out.composed === 'object') {
835
- for (const field of COORD) {
836
- const [ns, key] = field.split('.');
837
- const parent = out.composed[ns];
838
- if (parent && typeof parent === 'object' && typeof parent[key] === 'string')
839
- parent[key] = safe(parent[key]);
840
- }
841
- }
842
- if (out.seed) {
843
- if (typeof out.seed.platformRepo === 'string')
844
- out.seed.platformRepo = safe(out.seed.platformRepo);
845
- if (typeof out.seed.clientVaultRepo === 'string')
846
- out.seed.clientVaultRepo = safe(out.seed.clientVaultRepo);
847
- }
848
- out.provenance = (out.provenance ?? []).map((p) => (COORD.has(p.field) ? { ...p, value: safe(p.value) } : p));
849
- out.drift = (out.drift ?? []).map((d) => COORD.has(d.key) ? { ...d, profileValue: safe(d.profileValue), effectiveValue: safe(d.effectiveValue) } : d);
850
- out.warnings = (out.warnings ?? []).map((w) => (typeof w === 'string' ? v.redactRemoteText(w) : w));
851
- return out;
852
- }
853
945
  function cmdComposeBindings(cwd, options, raw) {
854
946
  let root;
855
947
  try {
@@ -861,3 +953,74 @@ function cmdComposeBindings(cwd, options, raw) {
861
953
  const result = composeBindings(root, { write: options.write === true, offline: options.offline === true });
862
954
  output(redactComposeResultForEmission(result), raw);
863
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
+ }