@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
@@ -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
  */
@@ -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
@@ -502,7 +528,6 @@ function isStringMember(list, value) {
502
528
  const BOOLEAN_ONLY_CONFIG_KEYS = Object.freeze([
503
529
  'vault.enabled',
504
530
  'vault.required',
505
- 'vault.practices_enabled',
506
531
  'workflow.schema_drift_gate',
507
532
  'workflow.nyquist_validation',
508
533
  'workflow.security_enforcement',
@@ -578,10 +603,6 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
578
603
  if (kp === 'ship.pr_body_sections') {
579
604
  validateShipPrBodySections(parsedValue);
580
605
  }
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
606
  // Human verification checkpoint mode (#3309)
586
607
  const VALID_HUMAN_VERIFY_MODES = ['mid-flight', 'end-of-phase'];
587
608
  if (kp === 'workflow.human_verify_mode' && !isStringMember(VALID_HUMAN_VERIFY_MODES, parsedValue)) {
@@ -601,14 +622,27 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
601
622
  if (kp === 'code_quality.fallow.profile' && !isStringMember(VALID_FALLOW_PROFILES, parsedValue)) {
602
623
  error(`Invalid code_quality.fallow.profile '${val}'. Valid values: ${VALID_FALLOW_PROFILES.join(', ')}`);
603
624
  }
625
+ // init.probe_status enum validation (07-11 / WR-04). 07-02 registered the
626
+ // four `init.probe_*` keys so config-set would not reject them as unknown;
627
+ // two shipped descriptions then claimed an out-of-vocabulary value "is
628
+ // refused at write time", which was true only of the record writers. This
629
+ // guard makes the claim true on this path too. Membership is tested on the
630
+ // parsed value's TYPE as well (13-REVIEW CR-01): `true`/`false`/numbers are
631
+ // coerced above and a `[`/`{` value is JSON-parsed, so only a plain string
632
+ // can be a member. A VALID value set here is still accepted — that is T-07-09's disclosed
633
+ // accepted residual, not an oversight.
634
+ if (kp === 'init.probe_status' && !isStringMember(VALID_INIT_PROBE_STATUSES, parsedValue)) {
635
+ error(`Invalid init.probe_status '${val}'. Valid values: ${VALID_INIT_PROBE_STATUSES.join(', ')}`, ERROR_REASON.USAGE);
636
+ }
604
637
  // plan_review.source_grounding_authority (#22) — enum
605
638
  const VALID_SOURCE_GROUNDING_AUTHORITIES = ['grep', 'intel', 'treesitter', 'lsp', 'scip'];
606
639
  if (kp === 'plan_review.source_grounding_authority' && !isStringMember(VALID_SOURCE_GROUNDING_AUTHORITIES, parsedValue)) {
607
640
  error(`Invalid plan_review.source_grounding_authority '${val}'. Valid values: ${VALID_SOURCE_GROUNDING_AUTHORITIES.join(', ')}`);
608
641
  }
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
642
+ // Capability enum keys, in ONE place (Vault Assurance 13-05, D-07). Measured
643
+ // 2026-10-09: `config-set workflow.code_review_depth bogus` wrote at exit 0,
644
+ // because no guard
645
+ // consulted the registry's declared `values`. This runs AFTER every
612
646
  // hand-coded guard above, so a key that already has its own guard
613
647
  // (`workflow.drift_action`) keeps its message byte-identical; the
614
648
  // registry, through `capabilityEnumValues`, is the only list consulted
@@ -618,8 +652,7 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
618
652
  // The TYPE is checked before membership (13-REVIEW CR-01): a value starting
619
653
  // with `[` is JSON-parsed above, and `String(['standards']) === 'standards'`,
620
654
  // 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
655
+ // which `federated-config` silently reads back as the default. Every declared capability enum value is a string, so a non-string
623
656
  // parse (array, object, number, boolean) can never be a member.
624
657
  const enumValues = capabilityEnumValues(kp);
625
658
  if (enumValues && !isStringMember(enumValues, parsedValue)) {
@@ -651,8 +684,7 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
651
684
  output(maskedResult, raw, `${kp}=${masked}`);
652
685
  return;
653
686
  }
654
- // Pack 6 D-07: the WRITTEN value stays raw; its echo (and the value it
655
- // replaced) is display-safe.
687
+ // VAULT-EGRESS-1: the written value stays raw; the echo of it (and of the value it replaced) is display-safe.
656
688
  const shownValue = redactConfigCoordinates(kp, parsedValue);
657
689
  const shownResult = {
658
690
  ...setConfigValueResult,
@@ -662,15 +694,16 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
662
694
  output(shownResult, raw, `${kp}=${String(shownValue)}`);
663
695
  }
664
696
  /**
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.
697
+ * VAULT-EGRESS-1 (quick 261005-8py gap closure): the display-safe form of a
698
+ * config value about to be EMITTED, for the one config key that is a remote
699
+ * repo coordinate (the shared-tier repo, `SHARED_REPO_CONFIG_KEY`). `keyPath`
700
+ * is the dotted path the value was read at (`''` for the whole config): the
701
+ * key itself, or any ancestor whose object carries it, is projected through
702
+ * the ONE owner (`redactCloneTarget`, `src/vault.cts`); every other value is
703
+ * returned untouched. The file on disk and every in-process reader keep the
704
+ * raw value — this runs at emission only, the same posture `isSecretKey`
705
+ * masking already takes on this surface. The key is read from its owner
706
+ * module rather than written here (this file names no allowlisted key).
674
707
  */
675
708
  function redactConfigCoordinates(keyPath, value) {
676
709
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -688,7 +721,7 @@ function redactConfigCoordinates(keyPath, value) {
688
721
  if (value === null || typeof value !== 'object' || Array.isArray(value))
689
722
  return value;
690
723
  const copy = JSON.parse(JSON.stringify(value));
691
- // The flat form: the whole dotted key written as one top-level property.
724
+ // GATE-05's flat form: the whole dotted key written as one top-level property.
692
725
  if (at.length === 0 && typeof copy[SHARED_REPO_CONFIG_KEY] === 'string') {
693
726
  copy[SHARED_REPO_CONFIG_KEY] = safe(copy[SHARED_REPO_CONFIG_KEY]);
694
727
  }
@@ -712,23 +745,77 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
712
745
  }
713
746
  // After the error() guard, keyPath is narrowed to string.
714
747
  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'));
748
+ /**
749
+ * 02-04: the fall-back tail shared by every `config-get` miss.
750
+ *
751
+ * PRECEDENCE — project config > org profile > `--default` > SCHEMA_DEFAULTS >
752
+ * error. This helper is only ever reached AFTER both dotted forms of the
753
+ * project read have missed, so project config outranking the profile is
754
+ * structural here rather than a comparison someone has to keep correct. That
755
+ * ordering is T-02-17's mitigation and is load-bearing: a fleet-wide profile
756
+ * must never be able to overrule what the project itself configured.
757
+ *
758
+ * Wiring this closes the gap `resolveVaultRequired` documents — the vault
759
+ * policy key's workflow line reads it through `config-get`, so before this
760
+ * existed a profile-seeded value reached that resolver's callers but never
761
+ * that workflow. (The key is named in `org-profile.cts`, not here: this file
762
+ * stays free of any allowlisted key literal so the "resolution is generic"
763
+ * check stays a meaningful grep.)
764
+ *
765
+ * NO KEY IS NAMED HERE, deliberately. `readProfileValue` self-gates on
766
+ * ORG_PROFILE_ALLOWLIST and does zero filesystem access for a key that is not
767
+ * on it, so consulting it on every miss is free for the overwhelmingly common
768
+ * non-allowlisted key and the wiring stays generic — adding a key to that
769
+ * allowlist is sufficient to make it readable here.
770
+ *
771
+ * Warnings go to fd 2 only; stdout stays byte-clean so `--raw` still emits a
772
+ * bare scalar (UIBR-02).
773
+ */
774
+ const missFallback = (missMessage, missReason) => {
775
+ // Call-time lazy require, matching org-profile.cts's own use of the pattern
776
+ // for git-provider.cjs. org-profile.cjs does not import config.cjs, so this
777
+ // adds no cycle — keep it that way.
778
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
779
+ const orgProfile = require('./org-profile.cjs');
780
+ let profileValue;
781
+ try {
782
+ const profile = orgProfile.readProfileValue(cwd, kp);
783
+ profileValue = profile.value;
784
+ for (const w of profile.warnings)
785
+ console.error('Warning: ' + w);
786
+ }
787
+ catch {
788
+ // A malformed or unreadable profile must never take down a config read
789
+ // that would otherwise have resolved a default. Degrade to "no profile".
790
+ profileValue = undefined;
791
+ }
792
+ if (profileValue !== undefined) {
793
+ const shownProfileValue = redactConfigCoordinates(kp, profileValue);
794
+ output(shownProfileValue, raw, String(shownProfileValue));
795
+ return;
719
796
  }
720
- else if (hasDefault) {
721
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
797
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
798
+ if (hasDefault) {
722
799
  output(defaultValue, raw, String(defaultValue));
723
800
  return;
724
801
  }
725
- else if (Object.prototype.hasOwnProperty.call(SCHEMA_DEFAULTS, kp)) {
802
+ if (Object.prototype.hasOwnProperty.call(SCHEMA_DEFAULTS, kp)) {
726
803
  const def = SCHEMA_DEFAULTS[kp];
727
804
  output(def, raw, String(def));
728
805
  return;
729
806
  }
807
+ error(missMessage, missReason);
808
+ };
809
+ let config = {};
810
+ try {
811
+ if (node_fs_1.default.existsSync(configPath)) {
812
+ config = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf-8'));
813
+ }
730
814
  else {
731
- error('No config.json found at ' + configPath, ERROR_REASON.CONFIG_NO_FILE);
815
+ // No config.json at all: the profile still gets its turn before the
816
+ // defaults, or a config-less project could never see a profile value.
817
+ missFallback('No config.json found at ' + configPath, ERROR_REASON.CONFIG_NO_FILE);
818
+ return;
732
819
  }
733
820
  }
734
821
  catch (err) {
@@ -736,37 +823,35 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
736
823
  throw err;
737
824
  error('Failed to read config.json: ' + err.message, ERROR_REASON.CONFIG_PARSE_FAILED);
738
825
  }
826
+ // GATE-05: the same dotted key may be written flat — one top-level property
827
+ // whose name contains dots — which the nested walk below cannot see. Resolved
828
+ // here as a FALLBACK only: the walk runs first and, when it finds a value,
829
+ // this is never consulted. Nested therefore wins whenever both forms are
830
+ // present, matching capability-activation.cjs's getConfigValueEitherForm so
831
+ // the two read paths a gate uses answer identically. The rejected-segment and
832
+ // hasOwnProperty guards live in that shared helper.
833
+ const flatFallback = _getFlatConfigValue(config, kp);
739
834
  // Traverse dot-notation path (e.g., "workflow.auto_advance")
740
835
  const keys = kp.split('.');
741
836
  let current = config;
742
837
  for (const key of keys) {
743
838
  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;
839
+ if (flatFallback.found) {
840
+ current = flatFallback.value;
841
+ break;
748
842
  }
749
- if (Object.prototype.hasOwnProperty.call(SCHEMA_DEFAULTS, kp)) {
750
- const def = SCHEMA_DEFAULTS[kp];
751
- output(def, raw, String(def));
752
- return;
753
- }
754
- error(`Key not found: ${kp}`, ERROR_REASON.CONFIG_KEY_NOT_FOUND);
843
+ missFallback(`Key not found: ${kp}`, ERROR_REASON.CONFIG_KEY_NOT_FOUND);
844
+ return;
755
845
  }
756
846
  current = current[key];
757
847
  }
848
+ // GATE-05: the walk completed but resolved nothing — flat form gets its turn.
849
+ if (current === undefined && flatFallback.found) {
850
+ current = flatFallback.value;
851
+ }
758
852
  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);
853
+ missFallback(`Key not found: ${kp}`, ERROR_REASON.CONFIG_KEY_NOT_FOUND);
854
+ return;
770
855
  }
771
856
  // Never echo plaintext for sensitive keys via config-get. Plaintext lives
772
857
  // in config.json on disk; the CLI surface always shows the masked form.
@@ -775,7 +860,10 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
775
860
  output(masked, raw, masked);
776
861
  return;
777
862
  }
778
- // Pack 6 D-07: a remote coordinate (or a section carrying one) is emitted display-safe.
863
+ // Pre-existing behaviour, now explicit: an object-valued key under --raw
864
+ // stringifies to '[object Object]'. Replacing the trailing `error()` (typed
865
+ // `never`) with the shared miss-tail cost TypeScript the narrowing that used
866
+ // to keep this quiet; the emitted bytes are unchanged.
779
867
  const shown = redactConfigCoordinates(kp, current);
780
868
  output(shown, raw, String(shown));
781
869
  }
@@ -880,6 +968,7 @@ function cmdMigrateConfig(cwd, raw) {
880
968
  }
881
969
  module.exports = {
882
970
  VALID_CONFIG_KEYS,
971
+ VALID_INIT_PROBE_STATUSES,
883
972
  BOOLEAN_ONLY_CONFIG_KEYS,
884
973
  cmdConfigEnsureSection,
885
974
  cmdConfigSet,
@@ -890,7 +979,7 @@ module.exports = {
890
979
  cmdMigrateConfig,
891
980
  // Exported for programmatic use by capability-writer and tests
892
981
  setConfigValue,
893
- // Pack 6 D-07: `state load` emits the whole config through the same projection.
894
- redactConfigCoordinates,
895
982
  setConfigValues,
983
+ // VAULT-EGRESS-1: `state load` emits the whole config through the same projection.
984
+ redactConfigCoordinates,
896
985
  };
@@ -18,10 +18,10 @@ const DISCRETION_HEADINGS = new Set([
18
18
  'claude discretion',
19
19
  ]);
20
20
  const NON_TRACKABLE_TAGS = new Set(['informational', 'folded', 'deferred']);
21
- // ── The decision-bullet grammar ────────────────────────────────────────────────────────────────────────────
21
+ // ── The decision-bullet grammar (260910-8ea) ────────────────────────────────
22
22
  //
23
23
  // Why a grammar rather than one regex: this repository writes THREE bullet
24
- // forms, and originally only the first was readable. The two others were
24
+ // forms, and until 260910-8ea only the first was readable. The two others were
25
25
  // not exotic — they are what `discuss-phase` emits and what hand-authored
26
26
  // CONTEXT.md files use — so a parser that reads only form A hands a blocking
27
27
  // coverage gate an empty decision set and lets it report a clean pass. That is
@@ -111,7 +111,7 @@ function extractDecisionsBlock(content) {
111
111
  *
112
112
  * The second channel exists because "the block was empty" and "the parser
113
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.
114
+ * to report identically — as a clean pass on a total of zero (260910-8ea).
115
115
  */
116
116
  function parseDecisionsReport(content) {
117
117
  if (!content || typeof content !== 'string')
@@ -170,7 +170,7 @@ function parseDecisionsReport(content) {
170
170
  // be silently dropped — a narrowed trackable set lets a blocking coverage gate
171
171
  // report a false pass. Surface it loudly instead.
172
172
  //
173
- // The miss is now recorded as DATA as well, so a gate can fail
173
+ // 260910-8ea: the miss is now recorded as DATA as well, so a gate can fail
174
174
  // on it rather than warning past it. The `console.warn` stays: the stderr
175
175
  // signal is what a human running the command by hand sees, and it is pinned
176
176
  // by the #1343 regression tests.