@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
@@ -29,9 +29,14 @@ const { VALID_PROFILES, getAgentToModelMapForProfile, formatAgentToModelMapAsTab
29
29
  const configSchema = require("./config-schema.cjs");
30
30
  const { VALID_CONFIG_KEYS, isValidConfigKey, capabilityEnumValues } = configSchema;
31
31
  const secrets_cjs_1 = require("./secrets.cjs");
32
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
33
+ const capabilityActivation = require("./capability-activation.cjs");
34
+ // GATE-05: reuse the activation resolver's flat-form lookup rather than growing
35
+ // a second copy of the rule here. capability-activation.cjs depends only on
36
+ // planning-workspace.cjs and node builtins, so this import adds no cycle.
37
+ const { _getFlatConfigValue } = capabilityActivation;
32
38
  const review_reviewer_selection_cjs_1 = require("./review-reviewer-selection.cjs");
33
39
  const configuration_cjs_1 = require("./configuration.cjs");
34
- const phase_tier_cjs_1 = require("./phase-tier.cjs");
35
40
  // ─── Constants ────────────────────────────────────────────────────────────────
36
41
  const CONFIG_KEY_SUGGESTIONS = {
37
42
  'workflow.nyquist_validation_enabled': 'workflow.nyquist_validation',
@@ -465,6 +470,20 @@ function setConfigValues(cwd, entries) {
465
470
  }
466
471
  });
467
472
  }
473
+ /**
474
+ * The complete `init.probe_status` vocabulary, as `pp-tools config-set`
475
+ * enforces it (07-11 / WR-04).
476
+ *
477
+ * The vocabulary's OWNER is `INIT_PROBE_STATUS` in `init-contract.cts`, which
478
+ * is where the record writers (`--record-probe` / `--defer-probe`) take it
479
+ * from. This is a deliberate copy, not a re-export: `config.cts` is a leaf and
480
+ * `init-contract.cjs` pulls in capability-activation, compose-bindings and
481
+ * org-profile, so importing it here would introduce a leaf-to-adapter edge.
482
+ * What keeps the copy honest is the two-direction parity test in
483
+ * `tests/config.test.cjs` — widening either vocabulary without the other
484
+ * reddens it.
485
+ */
486
+ const VALID_INIT_PROBE_STATUSES = Object.freeze(['passed', 'failed', 'deferred']);
468
487
  /**
469
488
  * The ONE membership test every enum guard in `cmdConfigSet` asks (13-REVIEW
470
489
  * CR-01). The raw value is parsed before the guards — `true`/`false`/numbers
@@ -485,14 +504,21 @@ function isStringMember(list, value) {
485
504
  * (`"FALSE"`, `"no"`, `"off"`) or a number. `capability-activation`'s
486
505
  * `_resolveActivationValue` ends in `Boolean(r.value)`, so those strings read as
487
506
  * ON — an operator typing `config-set vault.enabled FALSE` to switch the vault
488
- * off instead ARMED every step `capabilities/vault` hangs on that key, against
489
- * the constraint that a `vault.enabled=false` project stays completely inert.
507
+ * off instead ARMED the two `blocking: true` / `onError: halt` gates
508
+ * `capabilities/vault` hangs on that key, against CLAUDE.md's constraint that a
509
+ * `vault.enabled=false` project stays completely inert.
490
510
  *
491
- * The list, not ten copies of the same `if`: the first seven are capability
492
- * activation keys read through that `Boolean()` (the vault switches plus every
493
- * key that gates a blocking hook in the shipped registry). The last three were already guarded individually and are folded in here
494
- * unchanged, message included, so there is one mechanism in this file rather
495
- * than two.
511
+ * The list, not eleven copies of the same `if`: the first eight are every key
512
+ * that gates a blocking hook through that `Boolean()` read, and
513
+ * `tests/config.test.cjs` DERIVES that requirement from the shipped capability
514
+ * registry, so a future blocking gate on a new key reddens until it joins.
515
+ * `vault.required` is the recorded 02-SECURITY UF-01 instance of the same
516
+ * defect: its READ path already refuses a non-boolean (`booleanPolicyValue` in
517
+ * `src/org-profile.cts`), but the CLI door still answered `{"updated": true}` for a
518
+ * value the reader would then discard — the exact "reports success, changes
519
+ * nothing" shape UF-01 was opened for. The last three were already guarded
520
+ * individually and are folded in here unchanged, message included, so there is
521
+ * one mechanism in this file rather than two.
496
522
  *
497
523
  * REFUSE, do not normalise: `FALSE` plainly means false, but `1`, `enabled` and
498
524
  * `on` are guesses, and a config-set that silently rewrites the operator's word
@@ -501,8 +527,8 @@ function isStringMember(list, value) {
501
527
  */
502
528
  const BOOLEAN_ONLY_CONFIG_KEYS = Object.freeze([
503
529
  'vault.enabled',
504
- 'vault.required',
505
530
  'vault.practices_enabled',
531
+ 'vault.required',
506
532
  'workflow.schema_drift_gate',
507
533
  'workflow.nyquist_validation',
508
534
  'workflow.security_enforcement',
@@ -578,10 +604,6 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
578
604
  if (kp === 'ship.pr_body_sections') {
579
605
  validateShipPrBodySections(parsedValue);
580
606
  }
581
- // Phase ceremony tier (pack 9) — enum, src/phase-tier.cts owns the list
582
- if (kp === 'phase.tier' && !isStringMember(phase_tier_cjs_1.PHASE_TIERS, parsedValue)) {
583
- error(`Invalid phase.tier '${val}'. Valid values: ${phase_tier_cjs_1.PHASE_TIERS.join(', ')}`, ERROR_REASON.CONFIG_INVALID_VALUE);
584
- }
585
607
  // Human verification checkpoint mode (#3309)
586
608
  const VALID_HUMAN_VERIFY_MODES = ['mid-flight', 'end-of-phase'];
587
609
  if (kp === 'workflow.human_verify_mode' && !isStringMember(VALID_HUMAN_VERIFY_MODES, parsedValue)) {
@@ -601,14 +623,27 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
601
623
  if (kp === 'code_quality.fallow.profile' && !isStringMember(VALID_FALLOW_PROFILES, parsedValue)) {
602
624
  error(`Invalid code_quality.fallow.profile '${val}'. Valid values: ${VALID_FALLOW_PROFILES.join(', ')}`);
603
625
  }
626
+ // init.probe_status enum validation (07-11 / WR-04). 07-02 registered the
627
+ // four `init.probe_*` keys so config-set would not reject them as unknown;
628
+ // two shipped descriptions then claimed an out-of-vocabulary value "is
629
+ // refused at write time", which was true only of the record writers. This
630
+ // guard makes the claim true on this path too. Membership is tested on the
631
+ // parsed value's TYPE as well (13-REVIEW CR-01): `true`/`false`/numbers are
632
+ // coerced above and a `[`/`{` value is JSON-parsed, so only a plain string
633
+ // can be a member. A VALID value set here is still accepted — that is T-07-09's disclosed
634
+ // accepted residual, not an oversight.
635
+ if (kp === 'init.probe_status' && !isStringMember(VALID_INIT_PROBE_STATUSES, parsedValue)) {
636
+ error(`Invalid init.probe_status '${val}'. Valid values: ${VALID_INIT_PROBE_STATUSES.join(', ')}`, ERROR_REASON.USAGE);
637
+ }
604
638
  // plan_review.source_grounding_authority (#22) — enum
605
639
  const VALID_SOURCE_GROUNDING_AUTHORITIES = ['grep', 'intel', 'treesitter', 'lsp', 'scip'];
606
640
  if (kp === 'plan_review.source_grounding_authority' && !isStringMember(VALID_SOURCE_GROUNDING_AUTHORITIES, parsedValue)) {
607
641
  error(`Invalid plan_review.source_grounding_authority '${val}'. Valid values: ${VALID_SOURCE_GROUNDING_AUTHORITIES.join(', ')}`);
608
642
  }
609
- // Capability enum keys, in ONE place (13-05, D-07). Measured 2026-10-09:
610
- // `config-set workflow.code_review_depth bogus` wrote at exit 0, as did every
611
- // other enum key, because no guard consulted the registry's declared `values`. This runs AFTER every
643
+ // Capability enum keys, in ONE place (Vault Assurance 13-05, D-07). Measured
644
+ // 2026-10-09: `config-set workflow.code_review_depth bogus` and
645
+ // `config-set vault.mode whatever` both wrote at exit 0, because no guard
646
+ // consulted the registry's declared `values`. This runs AFTER every
612
647
  // hand-coded guard above, so a key that already has its own guard
613
648
  // (`workflow.drift_action`) keeps its message byte-identical; the
614
649
  // registry, through `capabilityEnumValues`, is the only list consulted
@@ -618,8 +653,9 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
618
653
  // The TYPE is checked before membership (13-REVIEW CR-01): a value starting
619
654
  // with `[` is JSON-parsed above, and `String(['standards']) === 'standards'`,
620
655
  // so a membership-only test let a one-element array through and wrote it —
621
- // which `federated-config` silently reads back as the default for an enum
622
- // key. Every declared capability enum value is a string, so a non-string
656
+ // which `resolveVaultMode` then refused on every vault command, and which
657
+ // `federated-config` silently reads back as the default for the other enum
658
+ // keys. Every declared capability enum value is a string, so a non-string
623
659
  // parse (array, object, number, boolean) can never be a member.
624
660
  const enumValues = capabilityEnumValues(kp);
625
661
  if (enumValues && !isStringMember(enumValues, parsedValue)) {
@@ -651,8 +687,7 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
651
687
  output(maskedResult, raw, `${kp}=${masked}`);
652
688
  return;
653
689
  }
654
- // Pack 6 D-07: the WRITTEN value stays raw; its echo (and the value it
655
- // replaced) is display-safe.
690
+ // VAULT-EGRESS-1: the written value stays raw; the echo of it (and of the value it replaced) is display-safe.
656
691
  const shownValue = redactConfigCoordinates(kp, parsedValue);
657
692
  const shownResult = {
658
693
  ...setConfigValueResult,
@@ -662,15 +697,16 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
662
697
  output(shownResult, raw, `${kp}=${String(shownValue)}`);
663
698
  }
664
699
  /**
665
- * Pack 6 D-07: the display-safe form of a config value about to be EMITTED,
666
- * for the config keys that hold a remote repo coordinate (the shared-tier repo,
667
- * `SHARED_REPO_CONFIG_KEY`). `keyPath` is the dotted path the value was read
668
- * at (`''` for the whole config): the key itself, or any ancestor object that
669
- * carries it, is projected through the one owner (`redactCloneTarget` in
670
- * src/vault.cts); every other value is returned untouched (and a
671
- * credential-free coordinate is unchanged by the owner). The file on disk and
672
- * every in-process reader keep the raw value. Lazily required, so this
673
- * module's load graph is unchanged.
700
+ * VAULT-EGRESS-1 (quick 261005-8py gap closure): the display-safe form of a
701
+ * config value about to be EMITTED, for the one config key that is a remote
702
+ * repo coordinate (the shared-tier repo, `SHARED_REPO_CONFIG_KEY`). `keyPath`
703
+ * is the dotted path the value was read at (`''` for the whole config): the
704
+ * key itself, or any ancestor whose object carries it, is projected through
705
+ * the ONE owner (`redactCloneTarget`, `src/vault.cts`); every other value is
706
+ * returned untouched. The file on disk and every in-process reader keep the
707
+ * raw value — this runs at emission only, the same posture `isSecretKey`
708
+ * masking already takes on this surface. The key is read from its owner
709
+ * module rather than written here (this file names no allowlisted key).
674
710
  */
675
711
  function redactConfigCoordinates(keyPath, value) {
676
712
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -688,7 +724,7 @@ function redactConfigCoordinates(keyPath, value) {
688
724
  if (value === null || typeof value !== 'object' || Array.isArray(value))
689
725
  return value;
690
726
  const copy = JSON.parse(JSON.stringify(value));
691
- // The flat form: the whole dotted key written as one top-level property.
727
+ // GATE-05's flat form: the whole dotted key written as one top-level property.
692
728
  if (at.length === 0 && typeof copy[SHARED_REPO_CONFIG_KEY] === 'string') {
693
729
  copy[SHARED_REPO_CONFIG_KEY] = safe(copy[SHARED_REPO_CONFIG_KEY]);
694
730
  }
@@ -712,23 +748,77 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
712
748
  }
713
749
  // After the error() guard, keyPath is narrowed to string.
714
750
  const kp = keyPath;
715
- let config = {};
716
- try {
717
- if (node_fs_1.default.existsSync(configPath)) {
718
- config = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf-8'));
751
+ /**
752
+ * 02-04: the fall-back tail shared by every `config-get` miss.
753
+ *
754
+ * PRECEDENCE — project config > org profile > `--default` > SCHEMA_DEFAULTS >
755
+ * error. This helper is only ever reached AFTER both dotted forms of the
756
+ * project read have missed, so project config outranking the profile is
757
+ * structural here rather than a comparison someone has to keep correct. That
758
+ * ordering is T-02-17's mitigation and is load-bearing: a fleet-wide profile
759
+ * must never be able to overrule what the project itself configured.
760
+ *
761
+ * Wiring this closes the gap `resolveVaultRequired` documents — the vault
762
+ * policy key's workflow line reads it through `config-get`, so before this
763
+ * existed a profile-seeded value reached that resolver's callers but never
764
+ * that workflow. (The key is named in `org-profile.cts`, not here: this file
765
+ * stays free of any allowlisted key literal so the "resolution is generic"
766
+ * check stays a meaningful grep.)
767
+ *
768
+ * NO KEY IS NAMED HERE, deliberately. `readProfileValue` self-gates on
769
+ * ORG_PROFILE_ALLOWLIST and does zero filesystem access for a key that is not
770
+ * on it, so consulting it on every miss is free for the overwhelmingly common
771
+ * non-allowlisted key and the wiring stays generic — adding a key to that
772
+ * allowlist is sufficient to make it readable here.
773
+ *
774
+ * Warnings go to fd 2 only; stdout stays byte-clean so `--raw` still emits a
775
+ * bare scalar (UIBR-02).
776
+ */
777
+ const missFallback = (missMessage, missReason) => {
778
+ // Call-time lazy require, matching org-profile.cts's own use of the pattern
779
+ // for git-provider.cjs. org-profile.cjs does not import config.cjs, so this
780
+ // adds no cycle — keep it that way.
781
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
782
+ const orgProfile = require('./org-profile.cjs');
783
+ let profileValue;
784
+ try {
785
+ const profile = orgProfile.readProfileValue(cwd, kp);
786
+ profileValue = profile.value;
787
+ for (const w of profile.warnings)
788
+ console.error('Warning: ' + w);
789
+ }
790
+ catch {
791
+ // A malformed or unreadable profile must never take down a config read
792
+ // that would otherwise have resolved a default. Degrade to "no profile".
793
+ profileValue = undefined;
794
+ }
795
+ if (profileValue !== undefined) {
796
+ const shownProfileValue = redactConfigCoordinates(kp, profileValue);
797
+ output(shownProfileValue, raw, String(shownProfileValue));
798
+ return;
719
799
  }
720
- else if (hasDefault) {
721
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
800
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
801
+ if (hasDefault) {
722
802
  output(defaultValue, raw, String(defaultValue));
723
803
  return;
724
804
  }
725
- else if (Object.prototype.hasOwnProperty.call(SCHEMA_DEFAULTS, kp)) {
805
+ if (Object.prototype.hasOwnProperty.call(SCHEMA_DEFAULTS, kp)) {
726
806
  const def = SCHEMA_DEFAULTS[kp];
727
807
  output(def, raw, String(def));
728
808
  return;
729
809
  }
810
+ error(missMessage, missReason);
811
+ };
812
+ let config = {};
813
+ try {
814
+ if (node_fs_1.default.existsSync(configPath)) {
815
+ config = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf-8'));
816
+ }
730
817
  else {
731
- error('No config.json found at ' + configPath, ERROR_REASON.CONFIG_NO_FILE);
818
+ // No config.json at all: the profile still gets its turn before the
819
+ // defaults, or a config-less project could never see a profile value.
820
+ missFallback('No config.json found at ' + configPath, ERROR_REASON.CONFIG_NO_FILE);
821
+ return;
732
822
  }
733
823
  }
734
824
  catch (err) {
@@ -736,37 +826,35 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
736
826
  throw err;
737
827
  error('Failed to read config.json: ' + err.message, ERROR_REASON.CONFIG_PARSE_FAILED);
738
828
  }
829
+ // GATE-05: the same dotted key may be written flat — one top-level property
830
+ // whose name contains dots — which the nested walk below cannot see. Resolved
831
+ // here as a FALLBACK only: the walk runs first and, when it finds a value,
832
+ // this is never consulted. Nested therefore wins whenever both forms are
833
+ // present, matching capability-activation.cjs's getConfigValueEitherForm so
834
+ // the two read paths a gate uses answer identically. The rejected-segment and
835
+ // hasOwnProperty guards live in that shared helper.
836
+ const flatFallback = _getFlatConfigValue(config, kp);
739
837
  // Traverse dot-notation path (e.g., "workflow.auto_advance")
740
838
  const keys = kp.split('.');
741
839
  let current = config;
742
840
  for (const key of keys) {
743
841
  if (current === undefined || current === null || typeof current !== 'object') {
744
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
745
- if (hasDefault) {
746
- output(defaultValue, raw, String(defaultValue));
747
- return;
748
- }
749
- if (Object.prototype.hasOwnProperty.call(SCHEMA_DEFAULTS, kp)) {
750
- const def = SCHEMA_DEFAULTS[kp];
751
- output(def, raw, String(def));
752
- return;
842
+ if (flatFallback.found) {
843
+ current = flatFallback.value;
844
+ break;
753
845
  }
754
- error(`Key not found: ${kp}`, ERROR_REASON.CONFIG_KEY_NOT_FOUND);
846
+ missFallback(`Key not found: ${kp}`, ERROR_REASON.CONFIG_KEY_NOT_FOUND);
847
+ return;
755
848
  }
756
849
  current = current[key];
757
850
  }
851
+ // GATE-05: the walk completed but resolved nothing — flat form gets its turn.
852
+ if (current === undefined && flatFallback.found) {
853
+ current = flatFallback.value;
854
+ }
758
855
  if (current === undefined) {
759
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
760
- if (hasDefault) {
761
- output(defaultValue, raw, String(defaultValue));
762
- return;
763
- }
764
- if (Object.prototype.hasOwnProperty.call(SCHEMA_DEFAULTS, kp)) {
765
- const def = SCHEMA_DEFAULTS[kp];
766
- output(def, raw, String(def));
767
- return;
768
- }
769
- error(`Key not found: ${kp}`, ERROR_REASON.CONFIG_KEY_NOT_FOUND);
856
+ missFallback(`Key not found: ${kp}`, ERROR_REASON.CONFIG_KEY_NOT_FOUND);
857
+ return;
770
858
  }
771
859
  // Never echo plaintext for sensitive keys via config-get. Plaintext lives
772
860
  // in config.json on disk; the CLI surface always shows the masked form.
@@ -775,7 +863,10 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
775
863
  output(masked, raw, masked);
776
864
  return;
777
865
  }
778
- // Pack 6 D-07: a remote coordinate (or a section carrying one) is emitted display-safe.
866
+ // Pre-existing behaviour, now explicit: an object-valued key under --raw
867
+ // stringifies to '[object Object]'. Replacing the trailing `error()` (typed
868
+ // `never`) with the shared miss-tail cost TypeScript the narrowing that used
869
+ // to keep this quiet; the emitted bytes are unchanged.
779
870
  const shown = redactConfigCoordinates(kp, current);
780
871
  output(shown, raw, String(shown));
781
872
  }
@@ -880,6 +971,7 @@ function cmdMigrateConfig(cwd, raw) {
880
971
  }
881
972
  module.exports = {
882
973
  VALID_CONFIG_KEYS,
974
+ VALID_INIT_PROBE_STATUSES,
883
975
  BOOLEAN_ONLY_CONFIG_KEYS,
884
976
  cmdConfigEnsureSection,
885
977
  cmdConfigSet,
@@ -890,7 +982,7 @@ module.exports = {
890
982
  cmdMigrateConfig,
891
983
  // Exported for programmatic use by capability-writer and tests
892
984
  setConfigValue,
893
- // Pack 6 D-07: `state load` emits the whole config through the same projection.
894
- redactConfigCoordinates,
895
985
  setConfigValues,
986
+ // VAULT-EGRESS-1: `state load` emits the whole config through the same projection.
987
+ redactConfigCoordinates,
896
988
  };
@@ -10,6 +10,7 @@
10
10
  * CJS callers that only use {id, text} safely ignore the extra fields.
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.parseDecisionsReport = parseDecisionsReport;
13
14
  exports.parseDecisions = parseDecisions;
14
15
  const DISCRETION_HEADINGS = new Set([
15
16
  "claude's discretion",
@@ -17,6 +18,67 @@ const DISCRETION_HEADINGS = new Set([
17
18
  'claude discretion',
18
19
  ]);
19
20
  const NON_TRACKABLE_TAGS = new Set(['informational', 'folded', 'deferred']);
21
+ // ── The decision-bullet grammar (260910-8ea) ────────────────────────────────
22
+ //
23
+ // Why a grammar rather than one regex: this repository writes THREE bullet
24
+ // forms, and until 260910-8ea only the first was readable. The two others were
25
+ // not exotic — they are what `discuss-phase` emits and what hand-authored
26
+ // CONTEXT.md files use — so a parser that reads only form A hands a blocking
27
+ // coverage gate an empty decision set and lets it report a clean pass. That is
28
+ // the defect this grammar retires (todo: decision-coverage-gate-false-pass).
29
+ //
30
+ // Form A `- **D-NN:** text` → arm A
31
+ // Form B `- **D-NN: Title.** text` → arm B (title may wrap: arm D)
32
+ // Form C `- **D-NN** text` → arm C
33
+ //
34
+ // HEAD matches the bullet opener and splits off `rest` (everything after the id
35
+ // and its optional `[tags]` group). The id rule is #3575's, unchanged: the first
36
+ // character after `D-` must be alphanumeric, so `D--x` and `D-_x` still fail
37
+ // HEAD and reach the parse-miss guard.
38
+ const BULLET_HEAD_RE = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+)\])?(.*)$/;
39
+ // Arm A — form A, widened. The run before the FIRST `:**` may now contain `:`.
40
+ // #1343 wrote that run as `[^:*]*`, which is what dropped every bold-title
41
+ // bullet AND `- **D-07 ratio 3:1:** w` (that miss used to be pinned as a floor;
42
+ // it now parses as D-07 / "w"). Lazy `[^*]*?` still stops at the first `:**`.
43
+ const ARM_A_RE = /^([^*]*?):\*\*\s*(.*)$/;
44
+ // Arm B — form B, the bold title. The title MAY contain `:` because real
45
+ // titles carry backticked loop points (`plan:post`, `execute:post`, `check:`);
46
+ // a title regex that excluded `:` measured four misses across two phases.
47
+ const ARM_B_RE = /^([^*]*?):\s*([^*]*?)\s*\*\*\s*(.*)$/;
48
+ // Arm C — the bold closes with no colon before it (`- **D-VH-1** body`, and
49
+ // `- **D-13 → see D-14.** *(reserved)*`). The only thing this form lacks is
50
+ // punctuation; the id is unambiguous, so it is admitted.
51
+ const ARM_C_RE = /^([^*]*?)\s*\*\*\s*(.*)$/;
52
+ // Arm D — no closing `**` on this line: a form-B title that wraps onto the
53
+ // next line. The existing continuation logic appends the following indented
54
+ // lines, stray `**` included (they are cosmetic in the resulting text).
55
+ const ARM_D_RE = /^([^*]*)$/;
56
+ /**
57
+ * Read a decision bullet's text out of `rest` (the remainder of the bullet line
58
+ * after the id and optional `[tags]`), trying arms A → B → C → D; first match
59
+ * wins. Returns null when no arm matches, which sends the line to the
60
+ * parse-miss guard.
61
+ *
62
+ * The guard is NOT vacuous after this widening: a malformed id fails HEAD, and
63
+ * a `rest` carrying a `*` in no arm shape (`- **D-01 *italic* no close`)
64
+ * matches no arm.
65
+ */
66
+ function readBulletText(rest) {
67
+ const a = rest.match(ARM_A_RE);
68
+ if (a)
69
+ return a[2];
70
+ const b = rest.match(ARM_B_RE);
71
+ // Headline first, so a six-word `softPhrase` is drawn from the title.
72
+ if (b)
73
+ return (b[2] + ' ' + b[3]).trim();
74
+ const c = rest.match(ARM_C_RE);
75
+ if (c)
76
+ return (c[1] + ' ' + c[2]).trim();
77
+ const d = rest.match(ARM_D_RE);
78
+ if (d)
79
+ return d[1].replace(/^\s*:?\s*/, '').trim();
80
+ return null;
81
+ }
20
82
  /**
21
83
  * Strip fenced code blocks from `content` so example `<decisions>` snippets
22
84
  * inside ```` ``` ```` do not pollute the parser (review F11).
@@ -40,34 +102,28 @@ function extractDecisionsBlock(content) {
40
102
  return matches.map((m) => m[1]).join('\n\n');
41
103
  }
42
104
  /**
43
- * Parse trackable decisions from CONTEXT.md content.
105
+ * Parse CONTEXT.md content into BOTH channels: the decisions that were read,
106
+ * and the bullets that could not be.
44
107
  *
45
- * Returns ALL D-NN decisions found inside `<decisions>` (including
46
- * non-trackable ones, with `trackable: false`). Callers that only want the
47
- * gate-enforced decisions should filter `.filter(d => d.trackable)`.
108
+ * `decisions` holds ALL D-NN decisions found inside `<decisions>` (including
109
+ * non-trackable ones, with `trackable: false`). `unparsed` holds every line
110
+ * that opened like a decision bullet and matched no arm of the grammar.
111
+ *
112
+ * The second channel exists because "the block was empty" and "the parser
113
+ * could not read any of it" are opposite situations that a coverage gate used
114
+ * to report identically — as a clean pass on a total of zero (260910-8ea).
48
115
  */
49
- function parseDecisions(content) {
116
+ function parseDecisionsReport(content) {
50
117
  if (!content || typeof content !== 'string')
51
- return [];
118
+ return { decisions: [], unparsed: [] };
52
119
  const block = extractDecisionsBlock(content);
53
120
  if (block === null)
54
- return [];
121
+ return { decisions: [], unparsed: [] };
55
122
  const lines = block.split(/\r?\n/);
56
123
  const out = [];
124
+ const unparsed = [];
57
125
  let category = '';
58
126
  let inDiscretion = false;
59
- // Bullet line: `- **D-NN[ [tags]]:** text`
60
- // Phase 6 (#3575): aligned to CJS regex — accepts alphanumeric IDs (D-01, D-INFRA-01, D-FOO_BAR)
61
- // in addition to numeric-only IDs (D-42). The first character after `D-` must
62
- // be alphanumeric, so malformed shapes like `D--foo` or `D-_bar` are rejected.
63
- // CJS callers consume {id, text} and ignore the optional extras.
64
- // #1343: `[^:*]*` replaces the old `\s*` before `:**` so that a freeform run
65
- // such as `(parenthetical)`, an em-dash, or other prose between the optional
66
- // bracket-tag group and the closing `:**` is tolerated rather than silently
67
- // dropping the whole decision. `[^:*]*` subsumes plain whitespace and stops
68
- // correctly at `:**`. Capture groups 1 (id), 2 (bracket tags), 3 (text) are
69
- // unchanged.
70
- const bulletRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+)\])?[^:*]*:\*\*\s*(.*)$/;
71
127
  let current = null;
72
128
  const flush = () => {
73
129
  if (current) {
@@ -94,29 +150,36 @@ function parseDecisions(content) {
94
150
  inDiscretion = DISCRETION_HEADINGS.has(normalized);
95
151
  continue;
96
152
  }
97
- const bulletMatch = line.match(bulletRe);
98
- if (bulletMatch) {
153
+ const headMatch = line.match(BULLET_HEAD_RE);
154
+ const bulletText = headMatch ? readBulletText(headMatch[3]) : null;
155
+ if (headMatch && bulletText !== null) {
99
156
  flush();
100
- const id = `D-${bulletMatch[1]}`;
101
- const tags = bulletMatch[2]
102
- ? bulletMatch[2]
157
+ const id = `D-${headMatch[1]}`;
158
+ const tags = headMatch[2]
159
+ ? headMatch[2]
103
160
  .split(',')
104
161
  .map((t) => t.trim().toLowerCase())
105
162
  .filter(Boolean)
106
163
  : [];
107
164
  const trackable = !inDiscretion && !tags.some((t) => NON_TRACKABLE_TAGS.has(t));
108
- current = { id, text: bulletMatch[3], category, tags, trackable };
165
+ current = { id, text: bulletText, category, tags, trackable };
109
166
  continue;
110
167
  }
111
168
  // Parse-miss guard (#1343): a line that looks like a `D-NN` decision bullet
112
- // but failed `bulletRe` (e.g. a `:` or `*` inside the pre-colon run) must NOT
169
+ // but failed HEAD (malformed id) or matched no arm of the grammar must NOT
113
170
  // be silently dropped — a narrowed trackable set lets a blocking coverage gate
114
171
  // report a false pass. Surface it loudly instead.
172
+ //
173
+ // 260910-8ea: the miss is now recorded as DATA as well, so a gate can fail
174
+ // on it rather than warning past it. The `console.warn` stays: the stderr
175
+ // signal is what a human running the command by hand sees, and it is pinned
176
+ // by the #1343 regression tests.
115
177
  if (/^\s*-\s+\*\*D-/.test(line)) {
116
178
  // A malformed D-bullet still starts a (failed) new decision, so it ends the
117
179
  // previous one — flush before warning so a following continuation line cannot
118
180
  // be mis-appended to the prior valid decision.
119
181
  flush();
182
+ unparsed.push({ line: trimmed, category });
120
183
  console.warn(`parseDecisions: ignored unparseable decision bullet: ${trimmed}`);
121
184
  continue;
122
185
  }
@@ -132,5 +195,19 @@ function parseDecisions(content) {
132
195
  }
133
196
  }
134
197
  flush();
135
- return out;
198
+ return { decisions: out, unparsed };
199
+ }
200
+ /**
201
+ * Parse trackable decisions from CONTEXT.md content.
202
+ *
203
+ * Returns ALL D-NN decisions found inside `<decisions>` (including
204
+ * non-trackable ones, with `trackable: false`). Callers that only want the
205
+ * gate-enforced decisions should filter `.filter(d => d.trackable)`.
206
+ *
207
+ * This signature is unchanged, and so is its behaviour. A caller that must not
208
+ * treat an unreadable bullet as an ABSENT decision — any gate that reports a
209
+ * verdict — should call `parseDecisionsReport` and read `unparsed`.
210
+ */
211
+ function parseDecisions(content) {
212
+ return parseDecisionsReport(content).decisions;
136
213
  }