@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
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+ /**
3
+ * Message catalog engine (Phase 41: MSG-02, MSG-03, MSG-04; D-01..D-06, D-20).
4
+ *
5
+ * ADR-457 build-at-publish: this TypeScript file is the SOURCE OF TRUTH. It
6
+ * compiles to the gitignored `pp-core/bin/lib/messages.cjs`; do NOT hand-edit
7
+ * the compiled output.
8
+ *
9
+ * One catalog keyed by a stable code. Each entry carries `developer` (today's
10
+ * text, frozen byte for byte), optional `engineer`, `business` and `explain`.
11
+ * A business lookup resolves business, then engineer, then developer; an
12
+ * engineer lookup engineer, then developer; a developer lookup developer
13
+ * (D-01). Business and engineer messages end with a `Ref: <code>` line (D-05,
14
+ * D-06); developer text never does.
15
+ *
16
+ * Params are data. Substitution is a single pass with a function replacer, so
17
+ * a value that holds `{x}` or `$&` is never re-expanded (T-41-12). For business
18
+ * and engineer every param except the slots an entry marks `rawParams` is
19
+ * stripped of control characters, whitespace-collapsed and capped (T-41-11): at
20
+ * ECHO_CAP for an echo, at DIAG_CAP for the slots an entry marks `longParams`
21
+ * (diagnostics and approval values the person must read in full); the developer path
22
+ * passes params through untouched (D-08). An unknown code renders the code
23
+ * itself, never blank and never a throw (D-04). Lookups use a Map only, so
24
+ * `__proto__` and `constructor` are unknown codes (T-41-13).
25
+ *
26
+ * Machine channels never route through the audience path: `io.errorCode` writes
27
+ * `msgDeveloper` to the `--json-errors` message for every audience. This module
28
+ * must never import `io.cjs` (io imports this module). It imports Node built-ins
29
+ * (through the audience leaf), `audience.cjs` and the slices only, so hooks can
30
+ * load it later (D-02).
31
+ *
32
+ * The audience is resolved through the Phase 39 leaf (`PP_AUDIENCE`, then the
33
+ * person file, then developer), silently (D-20): warnings are printed by the
34
+ * commands that own them, never by a lookup.
35
+ */
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.SLICES = exports.DIAG_CAP = exports.ECHO_CAP = void 0;
38
+ exports.duplicateCodesOf = duplicateCodesOf;
39
+ exports.registry = registry;
40
+ exports.has = has;
41
+ exports.audienceOf = audienceOf;
42
+ exports.sanitizeParam = sanitizeParam;
43
+ exports.fill = fill;
44
+ exports.pick = pick;
45
+ exports.refLine = refLine;
46
+ exports.msgDeveloper = msgDeveloper;
47
+ exports.text = text;
48
+ exports.msg = msg;
49
+ exports.explainText = explainText;
50
+ const audience_cjs_1 = require("./audience.cjs");
51
+ const core_cjs_1 = require("./messages/core.cjs");
52
+ const discovery_cjs_1 = require("./messages/discovery.cjs");
53
+ const init_cjs_1 = require("./messages/init.cjs");
54
+ const progress_cjs_1 = require("./messages/progress.cjs");
55
+ const gate_cjs_1 = require("./messages/gate.cjs");
56
+ const hook_cjs_1 = require("./messages/hook.cjs");
57
+ /**
58
+ * Every control character `\s` does not match (C0 minus `\t\n\v\f\r`, DEL and
59
+ * C1), plus every Unicode format character (category Cf: bidi overrides and
60
+ * isolates, zero-width characters, the BOM, tag characters). A right-to-left
61
+ * override in a file name would make the shown name differ from the real one
62
+ * (Trojan Source). Stripped BEFORE the whitespace collapse.
63
+ */
64
+ const CONTROL_CHARS_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f\p{Cf}]/gu;
65
+ const SLOT_RE = /\{([a-z][a-z0-9_]*)\}/g;
66
+ /** The cap for a slot that echoes a typed value (T-41-11). */
67
+ exports.ECHO_CAP = 120;
68
+ /**
69
+ * The cap for a `longParams` slot. `composeBindings` bounds each warning at 200
70
+ * characters, so this holds five full warnings joined with `; `.
71
+ */
72
+ exports.DIAG_CAP = 1000;
73
+ exports.SLICES = Object.freeze([
74
+ ['core', core_cjs_1.core],
75
+ ['discovery', discovery_cjs_1.discovery],
76
+ ['init', init_cjs_1.init],
77
+ ['progress', progress_cjs_1.progress],
78
+ ['gate', gate_cjs_1.gate],
79
+ ['hook', hook_cjs_1.hook],
80
+ ]);
81
+ /** Codes defined in more than one slice, sorted. */
82
+ function duplicateCodesOf(slices) {
83
+ const seen = new Set();
84
+ const dup = new Set();
85
+ for (const [, slice] of slices) {
86
+ for (const code of Object.keys(slice)) {
87
+ if (seen.has(code))
88
+ dup.add(code);
89
+ seen.add(code);
90
+ }
91
+ }
92
+ return [...dup].sort();
93
+ }
94
+ let _registry = null;
95
+ /** The catalog as one Map, built once. On a duplicate code the first slice wins; the gate test reports duplicates. */
96
+ function registry() {
97
+ if (_registry === null) {
98
+ const map = new Map();
99
+ for (const [, slice] of exports.SLICES) {
100
+ for (const code of Object.keys(slice)) {
101
+ if (!map.has(code))
102
+ map.set(code, slice[code]);
103
+ }
104
+ }
105
+ _registry = map;
106
+ }
107
+ return _registry;
108
+ }
109
+ function has(code) {
110
+ return registry().has(code);
111
+ }
112
+ /** The audience for this process (D-20). Never prints, never throws; developer on any failure. */
113
+ function audienceOf(env = process.env) {
114
+ try {
115
+ return (0, audience_cjs_1.resolveAudience)(env, (0, audience_cjs_1.readPerson)(env)).audience;
116
+ }
117
+ catch {
118
+ return 'developer';
119
+ }
120
+ }
121
+ /** Strip control and format characters, collapse whitespace, trim, cap at `cap` (T-41-11). Order matters. */
122
+ function sanitizeParam(value, cap = exports.ECHO_CAP) {
123
+ let raw;
124
+ if (typeof value === 'number')
125
+ raw = String(value);
126
+ else if (typeof value === 'string')
127
+ raw = value;
128
+ else
129
+ return '';
130
+ const collapsed = raw.replace(CONTROL_CHARS_RE, '').replace(/\s+/g, ' ').trim();
131
+ if (collapsed.length <= cap)
132
+ return collapsed;
133
+ return `${collapsed.slice(0, cap)}…`;
134
+ }
135
+ function rawParam(value) {
136
+ return typeof value === 'string' || typeof value === 'number' ? String(value) : '';
137
+ }
138
+ /**
139
+ * One pass of `{name}` substitution with a function replacer. A missing param
140
+ * renders empty in the developer text (unchanged, D-08) and as its own `{name}`
141
+ * slot for business and engineer, so a renderer that fails to supply a value
142
+ * shows visibly instead of leaving a silent blank (D-04). When sanitising, only
143
+ * the slots in `opts.rawParams` (the entry's own marking) pass through as written,
144
+ * and the slots in `opts.longParams` are capped at DIAG_CAP instead of ECHO_CAP.
145
+ */
146
+ function fill(template, params, opts) {
147
+ return template.replace(SLOT_RE, (match, name) => {
148
+ if (!Object.hasOwn(params, name))
149
+ return opts.sanitize ? match : '';
150
+ const value = params[name];
151
+ if (!opts.sanitize || (opts.rawParams?.includes(name) ?? false))
152
+ return rawParam(value);
153
+ return sanitizeParam(value, (opts.longParams?.includes(name) ?? false) ? exports.DIAG_CAP : exports.ECHO_CAP);
154
+ });
155
+ }
156
+ /** The wording for an audience: business, engineer, then developer fallbacks (D-01). */
157
+ function pick(entry, audience) {
158
+ if (audience === 'business')
159
+ return entry.business ?? entry.engineer ?? entry.developer;
160
+ if (audience === 'engineer')
161
+ return entry.engineer ?? entry.developer;
162
+ return entry.developer;
163
+ }
164
+ function refLine(code) {
165
+ return `Ref: ${code}`;
166
+ }
167
+ /** The developer text: today's wording, params untouched (D-05, D-08). Unknown code renders the code. */
168
+ function msgDeveloper(code, params = {}, _registry) {
169
+ const entry = (_registry ?? registry()).get(code);
170
+ if (entry === undefined)
171
+ return code;
172
+ return fill(entry.developer, params, { sanitize: false });
173
+ }
174
+ /** The text for an audience with no Ref line. Unknown code renders the code. */
175
+ function text(code, params = {}, opts) {
176
+ const entry = (opts._registry ?? registry()).get(code);
177
+ if (entry === undefined)
178
+ return code;
179
+ if (opts.audience === 'developer')
180
+ return msgDeveloper(code, params, opts._registry);
181
+ return fill(pick(entry, opts.audience), params, {
182
+ sanitize: true,
183
+ rawParams: entry.rawParams,
184
+ longParams: entry.longParams,
185
+ });
186
+ }
187
+ /** The message for an audience: business and engineer messages end with the Ref line (D-05, D-06). */
188
+ function msg(code, params = {}, opts) {
189
+ const entry = (opts._registry ?? registry()).get(code);
190
+ if (entry === undefined)
191
+ return code;
192
+ if (opts.audience === 'developer')
193
+ return msgDeveloper(code, params, opts._registry);
194
+ const body = text(code, params, opts);
195
+ if (entry.part === true)
196
+ return body;
197
+ return `${body}\n${refLine(code)}`;
198
+ }
199
+ /** What `pp-tools explain <code>` shows, or null for an unknown code. */
200
+ function explainText(code, opts) {
201
+ const entry = (opts._registry ?? registry()).get(code);
202
+ if (entry === undefined)
203
+ return null;
204
+ const body = typeof entry.explain === 'string' && entry.explain !== ''
205
+ ? entry.explain
206
+ : pick(entry, opts.audience).replace(SLOT_RE, (_m, name) => `<${name}>`);
207
+ return `${body}\n${refLine(code)}`;
208
+ }
@@ -39,6 +39,7 @@ const node_path_1 = __importDefault(require("node:path"));
39
39
  // eslint-disable-next-line @typescript-eslint/no-require-imports
40
40
  const childProcess = require("node:child_process");
41
41
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
42
+ const messages_cjs_1 = require("./messages.cjs");
42
43
  // CR-04 (03-11): this router's path arms resolve their containment boundary the
43
44
  // same way `cmdVaultTrace` in src/check-command-router.cts does. project-root.cjs is a
44
45
  // leaf (node builtins only), so this adds no cycle.
@@ -484,6 +485,12 @@ function routeVaultCommand({ args, cwd, raw, error, _vault, _core, _exec, _orgPr
484
485
  * `config-unreadable`, one more line per unreadable file names the file and the
485
486
  * repair; `--raw` prints the verdict object instead.
486
487
  *
488
+ * Phase 43 (D-12): for a business user or engineer the `--explain` lines are the
489
+ * card for the stopped check (`gate.vault.admission.blocked`), its `Ref:` line,
490
+ * `Details:` and then the same repair lines. The card is display text: the first
491
+ * line is the token for every audience, and without `--explain`, with `--raw`
492
+ * and for every other state the output is unchanged.
493
+ *
487
494
  * The answer comes from `vaultAdmissionVerdict` in the REAL src/vault.cts, never
488
495
  * from the injectable `_vault` seam: a governance decision must not be
489
496
  * switchable by a test mock.
@@ -518,9 +525,15 @@ function admissionVerb({ cwd, rest, raw, error, c }) {
518
525
  }
519
526
  const lines = [verdict.state];
520
527
  if (explain && verdict.state === 'config-unreadable') {
521
- for (const u of verdict.unreadable) {
522
- lines.push(`${u.file}: ${u.error} — repair: fix the JSON in ${u.file} (or remove the key that broke it) and re-run; ` +
523
- 'the gate cannot read this file and refuses to pass having checked nothing');
528
+ const code = 'gate.vault.admission.blocked';
529
+ const repair = verdict.unreadable.map((u) => (0, messages_cjs_1.msgDeveloper)(code, { file: u.file, error: u.error }));
530
+ const audience = (0, messages_cjs_1.audienceOf)(process.env);
531
+ if (audience === 'developer') {
532
+ lines.push(...repair);
533
+ }
534
+ else {
535
+ // D-12: the card for a business user or engineer, then today's repair line(s) under Details.
536
+ lines.push((0, messages_cjs_1.text)(code, {}, { audience }), (0, messages_cjs_1.refLine)(code), 'Details:', ...repair);
524
537
  }
525
538
  }
526
539
  c.output(verdict, true, `${lines.join('\n')}\n`);
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ /**
3
+ * Shared violation renderer (Phase 41: MSG-01, MSG-02, MSG-03; D-07, D-11, D-13).
4
+ *
5
+ * ADR-457 build-at-publish: this TypeScript file is the SOURCE OF TRUTH. It
6
+ * compiles to the gitignored `pp-core/bin/lib/violation-render.cjs`; do NOT
7
+ * hand-edit the compiled output.
8
+ *
9
+ * D-11: the only place a discovery violation becomes text. Built for every
10
+ * discovery domain; only `discovery.session` and `discovery.input` are cut over
11
+ * in this phase (the other 17 formatViolation copies stay behind the
12
+ * shrink-only ratchet in tests/fixtures/format-violation-allowlist.json).
13
+ *
14
+ * A violation is `{ reason, ...fields }`; its catalog code is
15
+ * `<domain>.<reason>`. A developer gets the exact line the old code printed
16
+ * (`msgDeveloper`, params untouched). A business user or engineer gets a block:
17
+ * the message with its own `Ref:` line, the first line prefixed `- ` and every
18
+ * following line indented, so each problem carries its code (D-05, D-07).
19
+ *
20
+ * Canonical values stay canonical (D-13): the engines pass the literals the
21
+ * person must write (`None identified in Discovery.`, `[TO CONFIRM]`,
22
+ * `Completed`) through `extra`; this module only adds a display `status` through
23
+ * `label()`. It never imports a discovery engine (the engines import it).
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.violationParams = violationParams;
27
+ exports.renderViolation = renderViolation;
28
+ const messages_cjs_1 = require("./messages.cjs");
29
+ const message_labels_cjs_1 = require("./message-labels.cjs");
30
+ const SESSION_FILE_RE = /session-(\d{2})\.md$/;
31
+ const SESSION_ID_RE = /^session (\d+)$/;
32
+ function sessionNumberOf(v) {
33
+ for (const field of ['artifact', 'session']) {
34
+ const value = v[field];
35
+ if (typeof value !== 'string')
36
+ continue;
37
+ const m = SESSION_FILE_RE.exec(value);
38
+ if (m !== null)
39
+ return String(Number(m[1]));
40
+ }
41
+ if (typeof v.id === 'string') {
42
+ const m = SESSION_ID_RE.exec(v.id);
43
+ if (m !== null)
44
+ return String(Number(m[1]));
45
+ }
46
+ return null;
47
+ }
48
+ /** The catalog params for a violation: every string or number field except `reason`, plus the session number, `extra`, and a display `status`. */
49
+ function violationParams(v, opts = {}) {
50
+ const fields = { ...v };
51
+ const params = {};
52
+ for (const [key, value] of Object.entries(fields)) {
53
+ if (key === 'reason')
54
+ continue;
55
+ if (typeof value === 'string' || typeof value === 'number')
56
+ params[key] = String(value);
57
+ }
58
+ const n = sessionNumberOf(fields);
59
+ if (n !== null)
60
+ params.session_number = n;
61
+ const extra = opts.extra ?? {};
62
+ for (const [key, value] of Object.entries(extra))
63
+ params[key] = String(value);
64
+ const statusWord = extra.status_word;
65
+ if (typeof statusWord === 'string') {
66
+ params.status = (0, message_labels_cjs_1.label)('discovery.status', statusWord, { audience: opts.audience ?? 'developer' });
67
+ }
68
+ return params;
69
+ }
70
+ /** One violation as text for an audience. An unknown reason renders its code, never throws. */
71
+ function renderViolation(domain, v, opts) {
72
+ const code = `${domain}.${v.reason}`;
73
+ const params = violationParams(v, opts);
74
+ if (opts.audience === 'developer')
75
+ return (0, messages_cjs_1.msgDeveloper)(code, params);
76
+ const lines = (0, messages_cjs_1.msg)(code, params, { audience: opts.audience }).split('\n');
77
+ return lines.map((line, i) => (i === 0 ? `- ${line}` : ` ${line}`)).join('\n');
78
+ }
@@ -71,6 +71,23 @@
71
71
  * [--plan P] [--id ID]
72
72
  * timeline summary <phase> [--banner] Fold a phase's runs: wall-clock, agent time, longest
73
73
  *
74
+ * Audience (Phase 39):
75
+ * audience status [--raw] Show the person's audience, language and where each came from
76
+ * audience set <business|engineer|developer> [--language en|nl | --clear-language]
77
+ * Save the setting in ~/.gsd/person.json (GSD_HOME-rooted); no
78
+ * --language keeps a stored language, --clear-language removes it
79
+ * audience set --clear-language Remove only the stored language; the stored audience is untouched
80
+ * audience answer <reply> Save a first-run reply (1, 2, 3 or a name); refused when the
81
+ * reply is blank or an audience is already saved
82
+ * PP_AUDIENCE=<value> Override the audience for one command or shell; never stored
83
+ *
84
+ * Explain (Phase 41):
85
+ * explain <code> [--raw] Longer explanation of a message's Ref code, in the person's wording
86
+ *
87
+ * Gate card (Phase 43):
88
+ * gate-card <query> [--state blocked|advisory|error] [--details] [--raw]
89
+ * The stopped-check card in the person's wording; prints nothing for a developer
90
+ *
74
91
  * Ceremony (Phase 37):
75
92
  * ceremony classify <phase> [--full] [--research] [--skip-research] [--skip-verify] [--write]
76
93
  * Classify a phase (trust boundary, codebase to match, single-concern), print the
@@ -210,7 +227,7 @@ const path = require('path');
210
227
  const readline = require('readline');
211
228
  const { ExitError, runMain } = require('./lib/cli-exit.cjs');
212
229
  const io = require('./lib/io.cjs');
213
- const { error, ERROR_REASON, setJsonErrorMode, output } = io;
230
+ const { error, errorCode, ERROR_REASON, setJsonErrorMode, output } = io;
214
231
  const projectRoot = require('./lib/project-root.cjs');
215
232
  // Resolve findProjectRoot lazily at call time rather than binding it at module
216
233
  // load. It is sourced from project-root.cjs; a call-time lookup is robust
@@ -226,6 +243,9 @@ const verify = require('./lib/verify.cjs');
226
243
  const config = require('./lib/config.cjs');
227
244
  const platformBindings = require('./lib/platform-bindings.cjs');
228
245
  const timeline = require('./lib/timeline.cjs');
246
+ const audience = require('./lib/audience-command.cjs');
247
+ const explain = require('./lib/explain-command.cjs');
248
+ const gateCard = require('./lib/gate-card.cjs');
229
249
  const composeBindings = require('./lib/compose-bindings.cjs');
230
250
  const initContract = require('./lib/init-contract.cjs');
231
251
  const initInteractive = require('./lib/init-interactive.cjs');
@@ -538,11 +558,11 @@ async function main() {
538
558
  // discovery; previously it was a partial subset that didn't include
539
559
  // phase / roadmap / milestone / progress / etc.
540
560
  const TOP_LEVEL_USAGE = 'Usage: pp-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>] [--json-errors]\n' +
541
- 'Commands: agent, agent-skills, audit-open, audit-uat, check, check-commit, commit, commit-to-subrepo, ' +
561
+ 'Commands: agent, agent-skills, audience, audit-open, audit-uat, check, check-commit, commit, commit-to-subrepo, ' +
542
562
  'compose-bindings, config-ensure-section, config-get, config-new-project, config-path, config-set, migrate-config, ' +
543
- 'current-timestamp, detect-custom-files, discovery, docs-init, drift-guard, effort, extract-messages, find-phase, ' +
563
+ 'current-timestamp, detect-custom-files, discovery, docs-init, drift-guard, effort, explain, extract-messages, find-phase, ' +
544
564
  'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
545
- 'generate-client-context, generate-dev-preferences, generate-slug, graphify, history-digest, init, init-contract, intel, ' +
565
+ 'gate-card, generate-client-context, generate-dev-preferences, generate-slug, graphify, history-digest, init, init-contract, intel, ' +
546
566
  'capability, ceremony, classify-confidence, git, learnings, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, platform-bindings, platform-latest-tag, profile-questionnaire, ' +
547
567
  'profile-sample, progress, prompt-budget, requirements, research-plan, research-store, resolve-granularity, resolve-model, roadmap, scaffold, state, ' +
548
568
  'task, template, timeline, user-story, validate, validate-bindings, verify, verify-path-exists, verify-summary, workstream, worktree\n\n' +
@@ -570,7 +590,8 @@ async function main() {
570
590
  // through this dispatcher. Rendering top-level usage on --help is strictly
571
591
  // better UX than the old short-circuit that printed unrelated usage text.
572
592
  const HELP_FLAGS = new Set(['-h', '--help', '-?', '--h', '--usage']);
573
- if (args.some((a) => HELP_FLAGS.has(a))) {
593
+ // Phase 41: `explain` answers its own help (its usage line names the Ref code), so it is not intercepted here.
594
+ if (command !== 'explain' && args.some((a) => HELP_FLAGS.has(a))) {
574
595
  process.stdout.write(TOP_LEVEL_USAGE + '\n');
575
596
  return;
576
597
  }
@@ -732,14 +753,19 @@ function createReadlinePrompter({ input = process.stdin, output = process.stdout
732
753
  const rl = readline.createInterface({ input, output });
733
754
  return {
734
755
  ask(question) {
756
+ // Phase 41: the chrome is catalog text. A developer (no `audience` on the question) renders
757
+ // exactly what this prompter always printed; business and engineer get their wording and a Ref line.
758
+ const messages = require('./lib/messages.cjs');
759
+ const aud = question.audience === 'business' || question.audience === 'engineer' ? question.audience : 'developer';
760
+ const chrome = (code, params) => messages.text(code, params, { audience: aud });
735
761
  let rendered = question.prompt;
736
762
  if (question.options && question.options.length > 0) {
737
763
  const optionsRendered = question.options
738
764
  .map((opt) =>
739
- opt === question.preselected ? `${opt} (${question.preselectedLabel || 'preselected'})` : opt,
765
+ opt === question.preselected ? `${opt} (${question.preselectedLabel || chrome('init.chrome.preselected', {})})` : opt,
740
766
  )
741
767
  .join(' / ');
742
- rendered += `\n Options: ${optionsRendered}`;
768
+ rendered += `\n ${chrome('init.chrome.options', { options: optionsRendered })}`;
743
769
  }
744
770
  if (question.optionalAnswer && !/press Enter to skip/i.test(question.prompt)) {
745
771
  // UAT-13-F2: a detected org profile is a HINT inside the prompt text
@@ -747,7 +773,10 @@ function createReadlinePrompter({ input = process.stdin, output = process.stdout
747
773
  // text may already carry the skip hint (the org profile prompts do),
748
774
  // so it is added only for an optional question whose text lacks it
749
775
  // (I-1).
750
- rendered += '\n (press Enter to skip)';
776
+ rendered += `\n ${chrome('init.chrome.skip_hint', {})}`;
777
+ }
778
+ if (aud !== 'developer' && typeof question.code === 'string') {
779
+ rendered += `\n${messages.refLine(question.code)}`;
751
780
  }
752
781
  return new Promise((resolve) => {
753
782
  rl.question(`${rendered}\n> `, (answer) => resolve(answer));
@@ -866,6 +895,21 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
866
895
  break;
867
896
  }
868
897
 
898
+ case 'audience': {
899
+ audience.cmdAudience(cwd, args.slice(1), raw);
900
+ break;
901
+ }
902
+
903
+ case 'explain': {
904
+ explain.cmdExplain(cwd, args.slice(1), raw);
905
+ break;
906
+ }
907
+
908
+ case 'gate-card': {
909
+ gateCard.cmdGateCard(cwd, args.slice(1), raw);
910
+ break;
911
+ }
912
+
869
913
  case 'ceremony': {
870
914
  // WR-04: loaded here, not at the top, so a missing ceremony-gates.json fails
871
915
  // only the ceremony command, never every pp-tools command.
@@ -1659,35 +1703,28 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1659
1703
  const INIT_INTERACTIVE_FLAGS = ['--offline', '--reset'];
1660
1704
  const unrecognizedInitFlag = args.slice(1).find((a) => !INIT_INTERACTIVE_FLAGS.includes(a));
1661
1705
  if (unrecognizedInitFlag !== undefined) {
1662
- error(
1663
- `Unrecognized flag "${unrecognizedInitFlag}" for init. Supported: --offline, --reset. ` +
1664
- 'For init workflow subcommands (execute-phase, plan-phase, new-project, ...), use "pp-tools query init.<subcommand>".',
1665
- ERROR_REASON.SDK_FAIL_FAST,
1666
- );
1706
+ errorCode('init.unknown_flag', { flag: unrecognizedInitFlag }, ERROR_REASON.SDK_FAIL_FAST);
1667
1707
  break;
1668
1708
  }
1669
1709
  // Refuse to prompt when stdin is not a TTY — never block on stdin
1670
1710
  // in CI; a hang here would be worse than a failure (T-34-71).
1671
1711
  if (!process.stdin.isTTY) {
1672
- error(
1673
- '"pp-tools init" requires an interactive terminal (stdin is not a TTY) and will not prompt. ' +
1674
- 'This command cannot run inside an AI-agent shell or a piped session — open a regular ' +
1675
- 'terminal window (e.g. Terminal, iTerm, or Windows Terminal) and run it there. ' +
1676
- 'For a non-interactive/CI-driven session, use "pp-tools query init-contract --session-next" / ' +
1677
- '"--session-answer <stepId>=<value>" directly instead.',
1678
- ERROR_REASON.INIT_NON_INTERACTIVE,
1679
- );
1712
+ // The catalog code is the ERROR_REASON value (D-03); --json-errors keeps the developer text for every audience.
1713
+ errorCode(ERROR_REASON.INIT_NON_INTERACTIVE, {}, ERROR_REASON.INIT_NON_INTERACTIVE);
1680
1714
  break;
1681
1715
  }
1682
1716
  if (args.includes('--reset')) {
1683
1717
  initContract.initSessionReset(cwd);
1684
1718
  }
1685
1719
  const prompter = createReadlinePrompter();
1720
+ const initAudience = require('./lib/messages.cjs').audienceOf(process.env);
1686
1721
  try {
1687
1722
  const result = await initInteractive.runInitInteractive(cwd, prompter, {
1688
1723
  offline: args.includes('--offline'),
1724
+ audience: initAudience,
1689
1725
  });
1690
- output(result, raw);
1726
+ // Business users and engineers read the human result above; the JSON stays for developers and --raw.
1727
+ if (initInteractive.printsResultJson(initAudience, raw)) output(result, raw);
1691
1728
  if (!result.ok) {
1692
1729
  process.exitCode = 1;
1693
1730
  }
@@ -2773,13 +2810,45 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
2773
2810
  } catch (deviationsCheckErr) {
2774
2811
  error(`discovery deviations-check: ${deviationsCheckErr && deviationsCheckErr.message ? deviationsCheckErr.message : String(deviationsCheckErr)}`, ERROR_REASON.UNKNOWN);
2775
2812
  }
2813
+ } else if (subcommand === 'tests-check') {
2814
+ // discovery tests-check [--require-complete] — _tests.md structure:
2815
+ // one risk row per main process with the matrix rating, end-to-end
2816
+ // for every high risk unless a D-09 decision says why, focus-area
2817
+ // subjects resolving, one acceptance row per story.
2818
+ // --require-complete also fails on an open story or an uncited focus
2819
+ // area (discovery_tests_incomplete).
2820
+ const testsRegister = require('./lib/discovery-tests.cjs');
2821
+ try {
2822
+ testsRegister.cmdDiscoveryTestsCheck(cwd, args, raw);
2823
+ } catch (testsCheckErr) {
2824
+ error(`discovery tests-check: ${testsCheckErr && testsCheckErr.message ? testsCheckErr.message : String(testsCheckErr)}`, ERROR_REASON.UNKNOWN);
2825
+ }
2826
+ } else if (subcommand === 'status') {
2827
+ // discovery status [--raw] — Phase 42 DOOR-01: discovery-only vs build, sprint state and next step; read-only. Exits 1 only on a usage error, or before it runs when the dispatcher refuses an invalid GSD_WORKSTREAM
2828
+ const discoveryStatus = require('./lib/discovery-status.cjs');
2829
+ try {
2830
+ discoveryStatus.cmdDiscoveryStatus(cwd, args, raw);
2831
+ } catch (statusErr) {
2832
+ error(`discovery status: ${statusErr && statusErr.message ? statusErr.message : String(statusErr)}`, ERROR_REASON.UNKNOWN);
2833
+ }
2834
+ } else if (subcommand === 'flow-page') {
2835
+ // discovery flow-page <MP-nn> [--out <file.html>] — render one main
2836
+ // process (swimlane, sub-processes, step matrix) from _processes.md
2837
+ // as a page the agent publishes and republishes while the room
2838
+ // iterates the diagram (Session 3 Step 5C).
2839
+ const flowPage = require('./lib/discovery-flow-page.cjs');
2840
+ try {
2841
+ flowPage.cmdDiscoveryFlowPage(cwd, args, raw);
2842
+ } catch (flowPageErr) {
2843
+ error(`discovery flow-page: ${flowPageErr && flowPageErr.message ? flowPageErr.message : String(flowPageErr)}`, ERROR_REASON.UNKNOWN);
2844
+ }
2776
2845
  } else {
2777
2846
  // WIRING, NOT HELP TEXT: tests/discovery-artifacts.test.cjs derives each
2778
2847
  // subcommand spelling by regexing `<namespace>_<subcommand>_failed` out of
2779
2848
  // the engine's exported exit identity, splits this string on commas and
2780
2849
  // asserts membership. A spelling missing here is a command the engine
2781
2850
  // claims to be and the router does not dispatch.
2782
- error('Unknown discovery subcommand. Available: rollup, input-check, research-check, screen-check, element-sweep, rebaseline-check, vault-check, charter-check, entity-check, process-check, rules-check, stories-check, phases-check, for-phase, roles-check, integrations-check, migration-check, deviations-check, session-check', ERROR_REASON.SDK_UNKNOWN_COMMAND);
2851
+ error('Unknown discovery subcommand. Available: rollup, input-check, research-check, screen-check, element-sweep, rebaseline-check, vault-check, charter-check, entity-check, process-check, rules-check, stories-check, phases-check, for-phase, roles-check, integrations-check, migration-check, deviations-check, session-check, tests-check, flow-page, status', ERROR_REASON.SDK_UNKNOWN_COMMAND);
2783
2852
  }
2784
2853
  break;
2785
2854
  }
@@ -80,6 +80,7 @@
80
80
  "manager.flags.plan",
81
81
  "manager.flags.execute",
82
82
  "response_language",
83
+ "discovery.language",
83
84
  "context_window",
84
85
  "graphify.build_timeout",
85
86
  "graphify.auto_update",