@pareto4/pp-core 1.21.1 → 1.23.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 (136) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/install.js +205 -28
  3. package/commands/pp-help.md +2 -1
  4. package/gemini-extension.json +1 -1
  5. package/hooks/dist/pp-check-update-worker.js +1 -1
  6. package/hooks/dist/pp-context-monitor.js +3 -3
  7. package/hooks/dist/pp-statusline.js +88 -14
  8. package/hooks/dist/pp-update-banner.js +44 -4
  9. package/hooks/dist/pp-workflow-guard.js +3 -3
  10. package/hooks/pp-check-update-worker.js +1 -1
  11. package/hooks/pp-context-monitor.js +3 -3
  12. package/hooks/pp-statusline.js +88 -14
  13. package/hooks/pp-update-banner.js +44 -4
  14. package/hooks/pp-workflow-guard.js +3 -3
  15. package/package.json +2 -2
  16. package/pp-core/bin/lib/audience-command.cjs +271 -0
  17. package/pp-core/bin/lib/audience.cjs +293 -0
  18. package/pp-core/bin/lib/commands.cjs +26 -17
  19. package/pp-core/bin/lib/discovery-charter.cjs +13 -1
  20. package/pp-core/bin/lib/discovery-flow-page.cjs +191 -0
  21. package/pp-core/bin/lib/discovery-for-phase.cjs +33 -5
  22. package/pp-core/bin/lib/discovery-input-check.cjs +11 -11
  23. package/pp-core/bin/lib/discovery-processes.cjs +97 -4
  24. package/pp-core/bin/lib/discovery-session-check.cjs +7 -7
  25. package/pp-core/bin/lib/discovery-status.cjs +419 -0
  26. package/pp-core/bin/lib/discovery-stories.cjs +36 -5
  27. package/pp-core/bin/lib/discovery-tests.cjs +398 -0
  28. package/pp-core/bin/lib/explain-command.cjs +48 -0
  29. package/pp-core/bin/lib/gate-card.cjs +97 -0
  30. package/pp-core/bin/lib/hook-messages.cjs +132 -0
  31. package/pp-core/bin/lib/init-contract.cjs +116 -76
  32. package/pp-core/bin/lib/init-interactive.cjs +110 -49
  33. package/pp-core/bin/lib/init.cjs +44 -3
  34. package/pp-core/bin/lib/io.cjs +38 -0
  35. package/pp-core/bin/lib/message-labels.cjs +75 -0
  36. package/pp-core/bin/lib/messages/core.cjs +20 -0
  37. package/pp-core/bin/lib/messages/discovery.cjs +267 -0
  38. package/pp-core/bin/lib/messages/gate.cjs +257 -0
  39. package/pp-core/bin/lib/messages/hook.cjs +114 -0
  40. package/pp-core/bin/lib/messages/init.cjs +513 -0
  41. package/pp-core/bin/lib/messages/progress.cjs +110 -0
  42. package/pp-core/bin/lib/messages.cjs +208 -0
  43. package/pp-core/bin/lib/vault-command-router.cjs +16 -3
  44. package/pp-core/bin/lib/violation-render.cjs +78 -0
  45. package/pp-core/bin/pp-tools.cjs +92 -23
  46. package/pp-core/bin/shared/config-schema.manifest.json +1 -0
  47. package/pp-core/references/audience-moments.md +119 -0
  48. package/pp-core/references/audience.md +114 -0
  49. package/pp-core/references/discovery-build-consumption.md +5 -2
  50. package/pp-core/references/discovery-charter.md +2 -2
  51. package/pp-core/references/discovery-id-conventions.md +1 -1
  52. package/pp-core/references/discovery-processes.md +31 -7
  53. package/pp-core/references/discovery-session-output.md +1 -1
  54. package/pp-core/references/discovery-tests.md +186 -0
  55. package/pp-core/templates/project-charter.md +3 -4
  56. package/pp-core/workflows/add-phase.md +2 -0
  57. package/pp-core/workflows/add-tests.md +2 -0
  58. package/pp-core/workflows/add-todo.md +2 -0
  59. package/pp-core/workflows/ai-integration-phase.md +2 -0
  60. package/pp-core/workflows/audit-milestone.md +2 -0
  61. package/pp-core/workflows/autonomous.md +2 -0
  62. package/pp-core/workflows/check-todos.md +2 -0
  63. package/pp-core/workflows/code-review-fix.md +2 -0
  64. package/pp-core/workflows/code-review.md +2 -0
  65. package/pp-core/workflows/complete-milestone.md +10 -1
  66. package/pp-core/workflows/discovery-sprint/charter.md +3 -2
  67. package/pp-core/workflows/discovery-sprint/prefill.md +3 -0
  68. package/pp-core/workflows/discovery-sprint/prototype.md +14 -0
  69. package/pp-core/workflows/discovery-sprint/session-13-gates.md +2 -0
  70. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +4 -1
  71. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +135 -76
  72. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +94 -64
  73. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +3 -2
  74. package/pp-core/workflows/discovery-sprint/vault-context.md +15 -0
  75. package/pp-core/workflows/discovery-sprint.md +16 -2
  76. package/pp-core/workflows/discovery-to-build/steps/handoff-gates.md +16 -9
  77. package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +1 -1
  78. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +9 -1
  79. package/pp-core/workflows/discovery-to-build.md +10 -4
  80. package/pp-core/workflows/discuss-phase/discovery.md +2 -2
  81. package/pp-core/workflows/discuss-phase-assumptions.md +2 -0
  82. package/pp-core/workflows/discuss-phase.md +1 -1
  83. package/pp-core/workflows/edit-phase.md +2 -0
  84. package/pp-core/workflows/eval-review.md +2 -0
  85. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +25 -1
  86. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +1 -0
  87. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +2 -0
  88. package/pp-core/workflows/execute-phase.md +1 -1
  89. package/pp-core/workflows/execute-plan.md +2 -0
  90. package/pp-core/workflows/extract-learnings.md +2 -0
  91. package/pp-core/workflows/help/modes/business.md +56 -0
  92. package/pp-core/workflows/help.md +12 -2
  93. package/pp-core/workflows/import.md +10 -1
  94. package/pp-core/workflows/ingest-docs.md +2 -0
  95. package/pp-core/workflows/init.md +17 -1
  96. package/pp-core/workflows/insert-phase.md +2 -0
  97. package/pp-core/workflows/list-workspaces.md +2 -0
  98. package/pp-core/workflows/manager.md +2 -0
  99. package/pp-core/workflows/map-codebase.md +2 -0
  100. package/pp-core/workflows/milestone-summary.md +2 -0
  101. package/pp-core/workflows/new-milestone.md +10 -1
  102. package/pp-core/workflows/new-project.md +12 -2
  103. package/pp-core/workflows/new-workspace.md +2 -0
  104. package/pp-core/workflows/next.md +8 -0
  105. package/pp-core/workflows/plan-milestone-gaps.md +10 -1
  106. package/pp-core/workflows/plan-phase.md +2 -1
  107. package/pp-core/workflows/plan-review-convergence.md +1 -1
  108. package/pp-core/workflows/progress.md +10 -0
  109. package/pp-core/workflows/quick.md +11 -1
  110. package/pp-core/workflows/remove-phase.md +2 -0
  111. package/pp-core/workflows/remove-workspace.md +2 -0
  112. package/pp-core/workflows/resume-project.md +25 -0
  113. package/pp-core/workflows/review.md +2 -0
  114. package/pp-core/workflows/scan.md +2 -0
  115. package/pp-core/workflows/secure-phase.md +2 -0
  116. package/pp-core/workflows/settings.md +41 -0
  117. package/pp-core/workflows/ship.md +2 -0
  118. package/pp-core/workflows/spec-phase.md +1 -1
  119. package/pp-core/workflows/ui-phase.md +2 -0
  120. package/pp-core/workflows/ui-review.md +2 -0
  121. package/pp-core/workflows/ultraplan-phase.md +2 -0
  122. package/pp-core/workflows/validate-phase.md +2 -0
  123. package/pp-core/workflows/vault-review.md +2 -0
  124. package/pp-core/workflows/verify-phase.md +2 -0
  125. package/pp-core/workflows/verify-work.md +2 -0
  126. package/scripts/affected-tests-lib.cjs +26 -12
  127. package/scripts/ci-release-only.cjs +158 -0
  128. package/scripts/live-runs/driver.cjs +326 -0
  129. package/scripts/live-runs/run.cjs +906 -0
  130. package/scripts/live-runs/scenarios.cjs +326 -0
  131. package/scripts/live-runs/scratch-project.cjs +132 -0
  132. package/scripts/live-runs/scrub.cjs +291 -0
  133. package/scripts/live-runs/turn-guard.cjs +107 -0
  134. package/scripts/live-runs/walk-setup.cjs +198 -0
  135. package/scripts/run-tests.cjs +151 -132
  136. package/scripts/test-env.cjs +64 -0
@@ -13,6 +13,7 @@ exports.readVaultRequiredState = readVaultRequiredState;
13
13
  exports.buildInitQuestions = buildInitQuestions;
14
14
  exports.deriveSlug = deriveSlug;
15
15
  exports.readProfileInitAnswers = readProfileInitAnswers;
16
+ exports.collectInvariantViolations = collectInvariantViolations;
16
17
  exports.verifyInitInvariants = verifyInitInvariants;
17
18
  exports.armVaultKeysWhereAbsent = armVaultKeysWhereAbsent;
18
19
  exports.recordProbeOutcome = recordProbeOutcome;
@@ -21,6 +22,7 @@ exports.initSessionNext = initSessionNext;
21
22
  exports.initSessionAnswer = initSessionAnswer;
22
23
  exports.initSessionReset = initSessionReset;
23
24
  exports.armVaultFromProfile = armVaultFromProfile;
25
+ exports.withoutDisplay = withoutDisplay;
24
26
  exports.cmdInitContract = cmdInitContract;
25
27
  const node_fs_1 = __importDefault(require("node:fs"));
26
28
  const node_path_1 = __importDefault(require("node:path"));
@@ -34,6 +36,7 @@ const org_profile_cjs_1 = require("./org-profile.cjs");
34
36
  const platform_bindings_cjs_1 = require("./platform-bindings.cjs");
35
37
  const compose_bindings_cjs_1 = require("./compose-bindings.cjs");
36
38
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
39
+ const messages_cjs_1 = require("./messages.cjs");
37
40
  // ─── Constants ───────────────────────────────────────────────────────────────
38
41
  /** Set when the operator declines `compose_confirm` — the only way to tell "declined" from "never ran". */
39
42
  exports.INIT_COMPOSE_DECLINED_KEY = 'init.compose_declined';
@@ -234,30 +237,30 @@ function buildInitQuestions(cwd, opts = {}) {
234
237
  preselected = git_provider_cjs_1.DEFAULT_PROVIDER;
235
238
  }
236
239
  const providerOptions = Object.freeze([...git_provider_cjs_1.PROVIDERS, ...IDENTITY_ONLY_PROVIDER_OPTIONS]);
240
+ const ask = (id, params = {}, code = `init.step.${id}`) => ({
241
+ prompt: (0, messages_cjs_1.msgDeveloper)(code, params),
242
+ display: { prompt: { code, params } },
243
+ });
237
244
  return [
238
- { id: 'project_name', prompt: 'Project name (human-readable, e.g. "Acme Dashboard")', optionalAnswer: false },
239
- { id: 'project_slug', prompt: 'Project slug (kebab-case, e.g. "acme-dashboard")', optionalAnswer: false },
240
- {
241
- id: 'client_name',
242
- prompt: 'Client name (company / team, e.g. "Acme Corp") — type "internal" if none',
243
- optionalAnswer: false,
244
- },
245
- {
246
- id: 'client_slug',
247
- prompt: 'Client slug (kebab-case, e.g. "acme") — type "internal" if none',
248
- optionalAnswer: false,
249
- },
245
+ { id: 'project_name', ...ask('project_name'), optionalAnswer: false },
246
+ { id: 'project_slug', ...ask('project_slug'), optionalAnswer: false },
247
+ { id: 'client_name', ...ask('client_name'), optionalAnswer: false },
248
+ { id: 'client_slug', ...ask('client_slug'), optionalAnswer: false },
250
249
  ...clientVaultQuestion(cwd, opts),
251
- { id: 'description', prompt: 'One-line description (what this project does)', optionalAnswer: false },
250
+ { id: 'description', ...ask('description'), optionalAnswer: false },
252
251
  {
253
252
  id: 'provider',
254
- prompt: 'Provider (gitlab / azure / github / other / none)',
253
+ prompt: (0, messages_cjs_1.msgDeveloper)('init.step.provider'),
255
254
  optionalAnswer: false,
256
255
  options: providerOptions,
257
256
  preselected,
258
- preselectedLabel: `${preselected} (resolved default)`,
257
+ preselectedLabel: (0, messages_cjs_1.msgDeveloper)('init.chrome.resolved_default', { provider: preselected }),
258
+ display: {
259
+ prompt: { code: 'init.step.provider', params: {} },
260
+ preselectedLabel: { code: 'init.chrome.resolved_default', params: { provider: preselected } },
261
+ },
259
262
  },
260
- { id: 'visibility', prompt: 'Visibility (private / public / internal)', optionalAnswer: false },
263
+ { id: 'visibility', ...ask('visibility'), optionalAnswer: false },
261
264
  ];
262
265
  }
263
266
  /** The client-vault question, asked while no vault is bound and a binding can be composed (`clientVaultChoice`).
@@ -267,13 +270,14 @@ function clientVaultQuestion(cwd, opts) {
267
270
  const choice = (0, compose_bindings_cjs_1.clientVaultChoice)(cwd, opts);
268
271
  if (!choice.offered)
269
272
  return [];
273
+ const code = choice.defaultRepo === null ? 'init.step.client_vault' : 'init.step.client_vault_default';
274
+ const params = choice.defaultRepo === null ? {} : { repo: choice.defaultRepo };
270
275
  return [
271
276
  {
272
277
  id: 'client_vault',
273
- prompt: choice.defaultRepo === null
274
- ? 'Client vault repo ("owner/name" or a full clone URL) — type "none" to bind no client vault'
275
- : `Client vault repo ("owner/name" or a full clone URL) — type "none" to use the default vault (${choice.defaultRepo})`,
278
+ prompt: (0, messages_cjs_1.msgDeveloper)(code, params),
276
279
  optionalAnswer: true,
280
+ display: { prompt: { code, params } },
277
281
  },
278
282
  ];
279
283
  }
@@ -380,9 +384,10 @@ function readConfigFile(cwd) {
380
384
  return { exists, parsed: {} };
381
385
  }
382
386
  }
383
- /** Report every invariant the project's state at `cwd` violates. Read-only and never throws;
384
- * each check needs a positive signal that init was attempted, so an empty directory is ok. */
385
- function verifyInitInvariants(cwd, opts = {}) {
387
+ /** Report every invariant the project's state at `cwd` violates, each with the params its message was built from.
388
+ * Read-only and never throws; each check needs a positive signal that init was attempted, so an empty directory is ok.
389
+ * The three in-scope messages are the catalog's developer text; the rest keep their literals with no params. */
390
+ function collectInvariantViolations(cwd, opts = {}) {
386
391
  const violations = [];
387
392
  const { exists: configExists, parsed } = readConfigFile(cwd);
388
393
  // INIT_IDENTITY_MISSING — only meaningful once a config file exists at
@@ -394,7 +399,8 @@ function verifyInitInvariants(cwd, opts = {}) {
394
399
  if (!hasProjectBlock) {
395
400
  violations.push({
396
401
  code: INVARIANT_CODE.IDENTITY_MISSING,
397
- message: 'No "project" block found in .planning/config.json — Step 2 identity capture did not complete.',
402
+ message: (0, messages_cjs_1.msgDeveloper)(INVARIANT_CODE.IDENTITY_MISSING),
403
+ params: {},
398
404
  });
399
405
  }
400
406
  }
@@ -407,9 +413,11 @@ function verifyInitInvariants(cwd, opts = {}) {
407
413
  const projectProvider = getDotted(parsed, 'project.provider');
408
414
  const vaultProvider = getDotted(parsed, 'vault.provider');
409
415
  if (git_provider_cjs_1.PROVIDERS.includes(projectProvider) && !isUsableString(vaultProvider)) {
416
+ const params = { provider: String(projectProvider) };
410
417
  violations.push({
411
418
  code: INVARIANT_CODE.VAULT_PROVIDER_UNSET,
412
- message: `project.provider is "${String(projectProvider)}" but vault.provider is unset — the D-13 provisioning gate is disabled.`,
419
+ message: (0, messages_cjs_1.msgDeveloper)(INVARIANT_CODE.VAULT_PROVIDER_UNSET, params),
420
+ params,
413
421
  });
414
422
  }
415
423
  }
@@ -426,8 +434,8 @@ function verifyInitInvariants(cwd, opts = {}) {
426
434
  if (dryRun.composable === true) {
427
435
  violations.push({
428
436
  code: INVARIANT_CODE.COMPOSITION_NOT_RUN,
429
- message: 'A binding is composable from the org profile and project answers, but .platform-bindings.json was never written and no decline was recorded — Step 2.5 appears to have been skipped. ' +
430
- 'Run "pp-tools init --reset" to redo the question set and reach the compose confirmation (init-friction F3: a completed session never re-offers composition on a bare re-run).',
437
+ message: (0, messages_cjs_1.msgDeveloper)(INVARIANT_CODE.COMPOSITION_NOT_RUN),
438
+ params: {},
431
439
  });
432
440
  }
433
441
  }
@@ -458,6 +466,7 @@ function verifyInitInvariants(cwd, opts = {}) {
458
466
  'Store the boolean with "pp-tools config-set vault.enabled true" (which accepts only ' +
459
467
  'true or false, and refuses any other spelling), or "pp-tools config-set vault.enabled ' +
460
468
  'false" to turn the capability off.',
469
+ params: {},
461
470
  });
462
471
  }
463
472
  }
@@ -491,6 +500,7 @@ function verifyInitInvariants(cwd, opts = {}) {
491
500
  message: `The reachability proof recorded FAILED on ${when} by ${who}: ${note}. ` +
492
501
  'Re-run "/pp-init" after fixing the remote, or record an attributed deferral with ' +
493
502
  '"pp-tools query init-contract --defer-probe <reason>".',
503
+ params: {},
494
504
  });
495
505
  }
496
506
  else if (vaultArmed && status === exports.INIT_PROBE_STATUS.DEFERRED) {
@@ -499,6 +509,7 @@ function verifyInitInvariants(cwd, opts = {}) {
499
509
  code: INVARIANT_CODE.PROBE_DEFERRED,
500
510
  message: `The reachability proof was deferred by ${who} on ${when} for reason: ${note}. ` +
501
511
  'Init remains unproven until a re-run of "/pp-init" records a passing probe.',
512
+ params: {},
502
513
  });
503
514
  }
504
515
  else if (owesAProof) {
@@ -510,6 +521,7 @@ function verifyInitInvariants(cwd, opts = {}) {
510
521
  message: `The org profile "${profile}" resolves, vault.enabled is true and .platform-bindings.json ` +
511
522
  `is present, but ${clause} — /pp-init Step 2.6 appears not to have run.` +
512
523
  notRunRemedy,
524
+ params: {},
513
525
  });
514
526
  if (status === exports.INIT_PROBE_STATUS.PASSED) {
515
527
  // Only an attributed, timestamped pass clears the check.
@@ -530,7 +542,12 @@ function verifyInitInvariants(cwd, opts = {}) {
530
542
  catch {
531
543
  // fail open — one check's fault must not suppress the others
532
544
  }
533
- return { ok: violations.length === 0, violations };
545
+ return violations;
546
+ }
547
+ /** The public invariant check: `{ code, message }` per violation, with no params (the `--verify` JSON is unchanged). */
548
+ function verifyInitInvariants(cwd, opts = {}) {
549
+ const collected = collectInvariantViolations(cwd, opts);
550
+ return { ok: collected.length === 0, violations: collected.map(({ code, message }) => ({ code, message })) };
534
551
  }
535
552
  // ─── Session state machine: one step at a time, every side effect performed by the CLI ───
536
553
  function sessionFilePath(cwd) {
@@ -596,13 +613,14 @@ function loadSession(cwd) {
596
613
  return { malformed: true };
597
614
  }
598
615
  }
599
- /** Format an unexpected write failure as a bounded message — never a raw stack trace. */
600
- function writeFailedMessage(err) {
616
+ /** An unexpected write failure as a bounded message (never a raw stack trace) plus the catalog handle the driver words it from. */
617
+ function writeFailed(err) {
601
618
  const detail = err instanceof Error ? err.message : String(err);
602
- return (`A filesystem write inside the init session failed (${detail}). ` +
603
- 'This project\'s .planning directory (or .platform-bindings.json) may not be writable ' +
604
- '(permissions, a read-only mount, a full disk). Nothing further was recorded for this step — ' +
605
- 'fix the underlying filesystem issue and try the same step again.');
619
+ const ref = { code: exports.INIT_SESSION_WRITE_FAILED, params: { detail } };
620
+ return { message: (0, messages_cjs_1.msgDeveloper)(ref.code, ref.params), ref };
621
+ }
622
+ function writeFailedMessage(err) {
623
+ return writeFailed(err).message;
606
624
  }
607
625
  function persistSession(cwd, session) {
608
626
  const dir = node_path_1.default.join(cwd, '.planning');
@@ -838,24 +856,30 @@ function computeCurrentStep(cwd, session, opts) {
838
856
  persistSession(cwd, session);
839
857
  }
840
858
  // Three different nothing-written end states, each with its own message.
841
- const skippedComposeInfo = session.composeDecision === 'skipped'
842
- ? 'Nothing was composable from the org profile and project answers — no compose_confirm step was shown and nothing was written (the inert path, INIT-02).'
859
+ const skippedCode = session.composeDecision === 'skipped'
860
+ ? 'init.done.skipped'
843
861
  : session.composeDecision === 'unchanged'
844
- ? 'The existing .platform-bindings.json is already up to date with what the org profile and project answers would produce — no compose_confirm step was needed and nothing was written.'
862
+ ? 'init.done.unchanged'
845
863
  : undefined;
864
+ const skippedComposeInfo = skippedCode === undefined ? undefined : { text: (0, messages_cjs_1.msgDeveloper)(skippedCode), ref: { code: skippedCode, params: {} } };
846
865
  // The session verify skips only the INIT_PROBE_* check: Step 2.6 records the proof after init returns.
847
- const verifyResult = verifyInitInvariants(cwd, { _globalRoot: opts._globalRoot, scope: 'session' });
848
- if (verifyResult.ok) {
866
+ const collected = collectInvariantViolations(cwd, { _globalRoot: opts._globalRoot, scope: 'session' });
867
+ if (collected.length === 0) {
849
868
  if (!session.doneAt) {
850
869
  session.doneAt = new Date().toISOString();
851
870
  persistSession(cwd, session);
852
871
  }
853
872
  return { kind: 'done', skippedComposeInfo };
854
873
  }
855
- return { kind: 'verify', violations: verifyResult.violations, skippedComposeInfo };
874
+ return {
875
+ kind: 'verify',
876
+ violations: collected.map(({ code, message }) => ({ code, message })),
877
+ refs: collected.map(({ code, params }) => ({ code, params })),
878
+ skippedComposeInfo,
879
+ };
856
880
  }
857
881
  catch (err) {
858
- return { kind: 'write_failed', message: writeFailedMessage(err) };
882
+ return { kind: 'write_failed', ...writeFailed(err) };
859
883
  }
860
884
  }
861
885
  function currentStepToState(step) {
@@ -869,15 +893,18 @@ function currentStepToState(step) {
869
893
  state.preselected = q.preselected;
870
894
  if (q.preselectedLabel !== undefined)
871
895
  state.preselectedLabel = q.preselectedLabel;
896
+ if (q.display !== undefined)
897
+ state.display = q.display;
872
898
  return state;
873
899
  }
874
900
  case 'compose_confirm':
875
901
  return {
876
902
  done: false,
877
903
  step: 'compose_confirm',
878
- prompt: 'A binding can be composed from the org profile and project answers. Compose and write .platform-bindings.json now? (yes/no)',
904
+ prompt: (0, messages_cjs_1.msgDeveloper)('init.step.compose_confirm'),
879
905
  optionalAnswer: false,
880
906
  info: step.info,
907
+ display: { prompt: { code: 'init.step.compose_confirm', params: {} } },
881
908
  };
882
909
  case 'verify':
883
910
  return {
@@ -885,14 +912,20 @@ function currentStepToState(step) {
885
912
  step: 'verify',
886
913
  prompt: null,
887
914
  violations: step.violations,
888
- ...(step.skippedComposeInfo !== undefined ? { info: step.skippedComposeInfo } : {}),
915
+ ...(step.skippedComposeInfo !== undefined ? { info: step.skippedComposeInfo.text } : {}),
916
+ display: {
917
+ violations: step.refs,
918
+ ...(step.skippedComposeInfo !== undefined ? { info: step.skippedComposeInfo.ref } : {}),
919
+ },
889
920
  };
890
921
  case 'done':
891
922
  return {
892
923
  done: true,
893
924
  step: 'done',
894
925
  prompt: null,
895
- ...(step.skippedComposeInfo !== undefined ? { info: step.skippedComposeInfo } : {}),
926
+ ...(step.skippedComposeInfo !== undefined
927
+ ? { info: step.skippedComposeInfo.text, display: { info: step.skippedComposeInfo.ref } }
928
+ : {}),
896
929
  };
897
930
  case 'write_failed':
898
931
  return {
@@ -901,9 +934,14 @@ function currentStepToState(step) {
901
934
  prompt: null,
902
935
  error: exports.INIT_SESSION_WRITE_FAILED,
903
936
  message: step.message,
937
+ display: { message: step.ref },
904
938
  };
905
939
  }
906
940
  }
941
+ /** A refusal whose text is the catalog's developer wording and whose handle lets the driver word it for the person. */
942
+ function refusal(reason, ref) {
943
+ return { ok: false, reason, message: (0, messages_cjs_1.msgDeveloper)(ref.code, ref.params), display: { message: ref } };
944
+ }
907
945
  /** Return the ONE next step of the session; malformed session state fails closed (recover with `--session-reset`). */
908
946
  function initSessionNext(cwd, opts = {}) {
909
947
  const loaded = loadSession(cwd);
@@ -913,8 +951,8 @@ function initSessionNext(cwd, opts = {}) {
913
951
  step: null,
914
952
  prompt: null,
915
953
  error: exports.INIT_SESSION_MALFORMED,
916
- message: 'The init session file at .planning/.init-session.json exists but is malformed or unreadable. ' +
917
- 'Run "pp_run query init-contract --session-reset" to start over — it is never resumed silently into an unknown state.',
954
+ message: (0, messages_cjs_1.msgDeveloper)(exports.INIT_SESSION_MALFORMED),
955
+ display: { message: { code: exports.INIT_SESSION_MALFORMED, params: {} } },
918
956
  };
919
957
  }
920
958
  const session = loaded.session;
@@ -926,19 +964,14 @@ function initSessionNext(cwd, opts = {}) {
926
964
  function initSessionAnswer(cwd, stepId, value, opts = {}) {
927
965
  const loaded = loadSession(cwd);
928
966
  if (loaded.malformed) {
929
- return {
930
- ok: false,
931
- reason: 'session_malformed',
932
- message: 'The init session file at .planning/.init-session.json exists but is malformed or unreadable. ' +
933
- 'Run "pp_run query init-contract --session-reset" to start over.',
934
- };
967
+ return refusal('session_malformed', { code: 'init.answer.session_malformed', params: {} });
935
968
  }
936
969
  const session = loaded.session;
937
970
  // Any write failure in this function degrades to `write_failed` instead of throwing.
938
971
  try {
939
972
  const step = computeCurrentStep(cwd, session, opts);
940
973
  if (step.kind === 'write_failed') {
941
- return { ok: false, reason: 'write_failed', message: step.message };
974
+ return { ok: false, reason: 'write_failed', message: step.message, display: { message: step.ref } };
942
975
  }
943
976
  if (step.kind === 'done') {
944
977
  return {
@@ -956,13 +989,13 @@ function initSessionAnswer(cwd, stepId, value, opts = {}) {
956
989
  };
957
990
  }
958
991
  if (!step.question.optionalAnswer && (typeof value !== 'string' || value.trim() === '')) {
959
- return { ok: false, reason: 'answer_required', message: `Question "${stepId}" requires a non-blank answer.` };
992
+ return refusal('answer_required', { code: 'init.answer.answer_required', params: { step: stepId } });
960
993
  }
961
994
  const answerValue = typeof value === 'string' ? value : '';
962
995
  if (stepId === 'client_vault') {
963
996
  const checked = (0, compose_bindings_cjs_1.validateClientVaultAnswer)(answerValue);
964
997
  if (!checked.ok) {
965
- return { ok: false, reason: 'invalid_answer', message: `Question "client_vault": ${checked.reason}` };
998
+ return refusal('invalid_answer', { code: 'init.answer.invalid_client_vault', params: { detail: checked.reason } });
966
999
  }
967
1000
  }
968
1001
  session.answers[stepId] = answerValue;
@@ -979,11 +1012,7 @@ function initSessionAnswer(cwd, stepId, value, opts = {}) {
979
1012
  }
980
1013
  const normalized = normalizeYesNo(value);
981
1014
  if (normalized === null) {
982
- return {
983
- ok: false,
984
- reason: 'invalid_answer',
985
- message: 'compose_confirm requires "yes" or "no" (also accepts approve/decline, y/n, true/false).',
986
- };
1015
+ return refusal('invalid_answer', { code: 'init.answer.invalid_compose_confirm', params: {} });
987
1016
  }
988
1017
  if (normalized === true) {
989
1018
  // Check the write result before recording "approved": the file can change between the dry run and this write.
@@ -995,14 +1024,9 @@ function initSessionAnswer(cwd, stepId, value, opts = {}) {
995
1024
  });
996
1025
  if (writeResult.composable !== true || writeResult.wrote !== true) {
997
1026
  // Leave the decision unset so the next call re-evaluates and can re-offer compose_confirm.
998
- return {
999
- ok: false,
1000
- reason: 'compose_write_failed',
1001
- message: 'compose_confirm was approved, but the write to .platform-bindings.json did not succeed — ' +
1002
- 'this was NOT recorded as skipped or declined.' +
1003
- (writeResult.warnings.length > 0 ? ` Warnings: ${writeResult.warnings.join('; ')}.` : '') +
1004
- ' Call --session-next to see the current state and try again.',
1005
- };
1027
+ return refusal('compose_write_failed', writeResult.warnings.length > 0
1028
+ ? { code: 'init.answer.compose_write_failed_warnings', params: { warnings: writeResult.warnings.join('; ') } }
1029
+ : { code: 'init.answer.compose_write_failed', params: {} });
1006
1030
  }
1007
1031
  session.composeDecision = 'approved';
1008
1032
  armVaultKeysWhereAbsent(cwd);
@@ -1022,7 +1046,8 @@ function initSessionAnswer(cwd, stepId, value, opts = {}) {
1022
1046
  };
1023
1047
  }
1024
1048
  catch (err) {
1025
- return { ok: false, reason: 'write_failed', message: writeFailedMessage(err) };
1049
+ const failed = writeFailed(err);
1050
+ return { ok: false, reason: 'write_failed', message: failed.message, display: { message: failed.ref } };
1026
1051
  }
1027
1052
  }
1028
1053
  /** Delete the session file (the recovery from a malformed session). Never throws. */
@@ -1141,20 +1166,35 @@ function armVaultFromProfile(cwd, opts = {}) {
1141
1166
  };
1142
1167
  }
1143
1168
  }
1169
+ /** A copy of `value` without its own top-level `display` handle; an array gets that per item (D-08).
1170
+ * The session JSON is a machine channel the /pp-init fallback path parses; the in-process catalog handles never reach it.
1171
+ * The handles are only ever attached at the top level of a session state, an answer result or a question. Nested data
1172
+ * (compose `info`, org-profile and binding values, auto-answers) is user data and passes through untouched, even a key
1173
+ * named `display`. */
1174
+ function withoutDisplay(value) {
1175
+ if (Array.isArray(value))
1176
+ return value.map((item) => withoutDisplay(item));
1177
+ if (typeof value === 'object' && value !== null && Object.hasOwn(value, 'display')) {
1178
+ const copy = { ...value };
1179
+ delete copy.display;
1180
+ return copy;
1181
+ }
1182
+ return value;
1183
+ }
1144
1184
  // ─── cmdInitContract ────────────────────────────────────────────────────────
1145
1185
  /** CLI adapter for `pp-tools query init-contract`: the session, record, arm-vault, verify and questions flags.
1146
1186
  * Data verdicts set `exitCode = 1` when not ok, but the JSON result is always printed. */
1147
1187
  function cmdInitContract(cwd, options, raw) {
1148
1188
  if (options.sessionReset === true) {
1149
1189
  const result = initSessionReset(cwd);
1150
- output(result, raw);
1190
+ output(withoutDisplay(result), raw);
1151
1191
  return;
1152
1192
  }
1153
1193
  if (options.sessionAnswer !== undefined) {
1154
1194
  const result = initSessionAnswer(cwd, options.sessionAnswer.stepId, options.sessionAnswer.value, {
1155
1195
  offline: options.offline === true,
1156
1196
  });
1157
- output(result, raw);
1197
+ output(withoutDisplay(result), raw);
1158
1198
  if (!result.ok) {
1159
1199
  process.exitCode = 1;
1160
1200
  }
@@ -1162,7 +1202,7 @@ function cmdInitContract(cwd, options, raw) {
1162
1202
  }
1163
1203
  if (options.sessionNext === true) {
1164
1204
  const state = initSessionNext(cwd, { offline: options.offline === true });
1165
- output(state, raw);
1205
+ output(withoutDisplay(state), raw);
1166
1206
  if (state.error !== undefined) {
1167
1207
  process.exitCode = 1;
1168
1208
  }
@@ -1171,7 +1211,7 @@ function cmdInitContract(cwd, options, raw) {
1171
1211
  // Record writes run before `--verify`, so a combined call verifies the written state.
1172
1212
  if (options.recordProbe !== undefined) {
1173
1213
  const result = recordProbeOutcome(cwd, options.recordProbe);
1174
- output(result, raw);
1214
+ output(withoutDisplay(result), raw);
1175
1215
  if (!result.ok) {
1176
1216
  process.exitCode = 1;
1177
1217
  }
@@ -1179,7 +1219,7 @@ function cmdInitContract(cwd, options, raw) {
1179
1219
  }
1180
1220
  if (options.deferProbe !== undefined) {
1181
1221
  const result = recordProbeDeferral(cwd, options.deferProbe);
1182
- output(result, raw);
1222
+ output(withoutDisplay(result), raw);
1183
1223
  if (!result.ok) {
1184
1224
  process.exitCode = 1;
1185
1225
  }
@@ -1188,7 +1228,7 @@ function cmdInitContract(cwd, options, raw) {
1188
1228
  // An inert `armed: false` is a data verdict on exit 0, like the inert init path; only `ok: false` exits 1.
1189
1229
  if (options.armVault === true) {
1190
1230
  const result = armVaultFromProfile(cwd, { offline: options.offline === true });
1191
- output(result, raw);
1231
+ output(withoutDisplay(result), raw);
1192
1232
  if (result.ok === false) {
1193
1233
  process.exitCode = 1;
1194
1234
  }
@@ -1196,11 +1236,11 @@ function cmdInitContract(cwd, options, raw) {
1196
1236
  }
1197
1237
  if (options.verify === true) {
1198
1238
  const result = verifyInitInvariants(cwd);
1199
- output(result, raw);
1239
+ output(withoutDisplay(result), raw);
1200
1240
  if (!result.ok) {
1201
1241
  process.exitCode = 1;
1202
1242
  }
1203
1243
  return;
1204
1244
  }
1205
- output({ questions: buildInitQuestions(cwd) }, raw);
1245
+ output({ questions: buildInitQuestions(cwd).map(withoutDisplay) }, raw);
1206
1246
  }