@pareto4/pp-core 1.22.0 → 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 (119) 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 +1 -1
  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-input-check.cjs +11 -11
  21. package/pp-core/bin/lib/discovery-session-check.cjs +7 -7
  22. package/pp-core/bin/lib/discovery-status.cjs +419 -0
  23. package/pp-core/bin/lib/explain-command.cjs +48 -0
  24. package/pp-core/bin/lib/gate-card.cjs +97 -0
  25. package/pp-core/bin/lib/hook-messages.cjs +132 -0
  26. package/pp-core/bin/lib/init-contract.cjs +116 -76
  27. package/pp-core/bin/lib/init-interactive.cjs +110 -49
  28. package/pp-core/bin/lib/init.cjs +44 -3
  29. package/pp-core/bin/lib/io.cjs +33 -0
  30. package/pp-core/bin/lib/message-labels.cjs +75 -0
  31. package/pp-core/bin/lib/messages/core.cjs +20 -0
  32. package/pp-core/bin/lib/messages/discovery.cjs +267 -0
  33. package/pp-core/bin/lib/messages/gate.cjs +257 -0
  34. package/pp-core/bin/lib/messages/hook.cjs +114 -0
  35. package/pp-core/bin/lib/messages/init.cjs +513 -0
  36. package/pp-core/bin/lib/messages/progress.cjs +110 -0
  37. package/pp-core/bin/lib/messages.cjs +208 -0
  38. package/pp-core/bin/lib/vault-command-router.cjs +16 -3
  39. package/pp-core/bin/lib/violation-render.cjs +78 -0
  40. package/pp-core/bin/pp-tools.cjs +68 -23
  41. package/pp-core/bin/shared/config-schema.manifest.json +1 -0
  42. package/pp-core/references/audience-moments.md +119 -0
  43. package/pp-core/references/audience.md +114 -0
  44. package/pp-core/references/discovery-charter.md +2 -2
  45. package/pp-core/templates/project-charter.md +3 -4
  46. package/pp-core/workflows/add-phase.md +2 -0
  47. package/pp-core/workflows/add-tests.md +2 -0
  48. package/pp-core/workflows/add-todo.md +2 -0
  49. package/pp-core/workflows/ai-integration-phase.md +2 -0
  50. package/pp-core/workflows/audit-milestone.md +2 -0
  51. package/pp-core/workflows/autonomous.md +2 -0
  52. package/pp-core/workflows/check-todos.md +2 -0
  53. package/pp-core/workflows/code-review-fix.md +2 -0
  54. package/pp-core/workflows/code-review.md +2 -0
  55. package/pp-core/workflows/complete-milestone.md +10 -1
  56. package/pp-core/workflows/discovery-sprint/charter.md +3 -2
  57. package/pp-core/workflows/discovery-sprint/prefill.md +3 -0
  58. package/pp-core/workflows/discovery-sprint/prototype.md +14 -0
  59. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +4 -1
  60. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -1
  61. package/pp-core/workflows/discovery-sprint/vault-context.md +15 -0
  62. package/pp-core/workflows/discovery-sprint.md +14 -0
  63. package/pp-core/workflows/discovery-to-build.md +8 -2
  64. package/pp-core/workflows/discuss-phase-assumptions.md +2 -0
  65. package/pp-core/workflows/discuss-phase.md +1 -1
  66. package/pp-core/workflows/edit-phase.md +2 -0
  67. package/pp-core/workflows/eval-review.md +2 -0
  68. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +25 -1
  69. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +1 -0
  70. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +2 -0
  71. package/pp-core/workflows/execute-phase.md +1 -1
  72. package/pp-core/workflows/execute-plan.md +2 -0
  73. package/pp-core/workflows/extract-learnings.md +2 -0
  74. package/pp-core/workflows/help/modes/business.md +56 -0
  75. package/pp-core/workflows/help.md +12 -2
  76. package/pp-core/workflows/import.md +10 -1
  77. package/pp-core/workflows/ingest-docs.md +2 -0
  78. package/pp-core/workflows/init.md +17 -1
  79. package/pp-core/workflows/insert-phase.md +2 -0
  80. package/pp-core/workflows/list-workspaces.md +2 -0
  81. package/pp-core/workflows/manager.md +2 -0
  82. package/pp-core/workflows/map-codebase.md +2 -0
  83. package/pp-core/workflows/milestone-summary.md +2 -0
  84. package/pp-core/workflows/new-milestone.md +10 -1
  85. package/pp-core/workflows/new-project.md +12 -2
  86. package/pp-core/workflows/new-workspace.md +2 -0
  87. package/pp-core/workflows/next.md +8 -0
  88. package/pp-core/workflows/plan-milestone-gaps.md +10 -1
  89. package/pp-core/workflows/plan-phase.md +2 -1
  90. package/pp-core/workflows/plan-review-convergence.md +1 -1
  91. package/pp-core/workflows/progress.md +10 -0
  92. package/pp-core/workflows/quick.md +11 -1
  93. package/pp-core/workflows/remove-phase.md +2 -0
  94. package/pp-core/workflows/remove-workspace.md +2 -0
  95. package/pp-core/workflows/resume-project.md +25 -0
  96. package/pp-core/workflows/review.md +2 -0
  97. package/pp-core/workflows/scan.md +2 -0
  98. package/pp-core/workflows/secure-phase.md +2 -0
  99. package/pp-core/workflows/settings.md +41 -0
  100. package/pp-core/workflows/ship.md +2 -0
  101. package/pp-core/workflows/spec-phase.md +1 -1
  102. package/pp-core/workflows/ui-phase.md +2 -0
  103. package/pp-core/workflows/ui-review.md +2 -0
  104. package/pp-core/workflows/ultraplan-phase.md +2 -0
  105. package/pp-core/workflows/validate-phase.md +2 -0
  106. package/pp-core/workflows/vault-review.md +2 -0
  107. package/pp-core/workflows/verify-phase.md +2 -0
  108. package/pp-core/workflows/verify-work.md +2 -0
  109. package/scripts/affected-tests-lib.cjs +26 -12
  110. package/scripts/ci-release-only.cjs +158 -0
  111. package/scripts/live-runs/driver.cjs +326 -0
  112. package/scripts/live-runs/run.cjs +906 -0
  113. package/scripts/live-runs/scenarios.cjs +326 -0
  114. package/scripts/live-runs/scratch-project.cjs +132 -0
  115. package/scripts/live-runs/scrub.cjs +291 -0
  116. package/scripts/live-runs/turn-guard.cjs +107 -0
  117. package/scripts/live-runs/walk-setup.cjs +198 -0
  118. package/scripts/run-tests.cjs +151 -135
  119. 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
  }
@@ -2786,6 +2823,14 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
2786
2823
  } catch (testsCheckErr) {
2787
2824
  error(`discovery tests-check: ${testsCheckErr && testsCheckErr.message ? testsCheckErr.message : String(testsCheckErr)}`, ERROR_REASON.UNKNOWN);
2788
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
+ }
2789
2834
  } else if (subcommand === 'flow-page') {
2790
2835
  // discovery flow-page <MP-nn> [--out <file.html>] — render one main
2791
2836
  // process (swimlane, sub-processes, step matrix) from _processes.md
@@ -2803,7 +2848,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
2803
2848
  // the engine's exported exit identity, splits this string on commas and
2804
2849
  // asserts membership. A spelling missing here is a command the engine
2805
2850
  // claims to be and the router does not dispatch.
2806
- 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', 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);
2807
2852
  }
2808
2853
  break;
2809
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",
@@ -0,0 +1,119 @@
1
+ # Audience moments
2
+
3
+ The business and engineer forms for five moments (Next Up, a stopped check, a verification result, the fallback-standards question and a confirmation before an irreversible action) plus the discovery narration rule. Read from `audience.md`'s pointer line, never `@`-included.
4
+
5
+ ## When to use this
6
+
7
+ Use a form below only when the payload (or `pp_run query audience fields`) carries `audience`; pick the section for that audience. A developer never reads this file, and a developer's text stays as it is.
8
+
9
+ Fill every `{…}` from facts you hold (the `ROADMAP.md` goal, the plan's objective, `VERIFICATION.md`, the check's own output) and never invent one. Keep commands, file names, IDs and paths in backticks, unchanged (`audience.md`, Never reword). When the CLI already printed a card (a `GATE_CARDS=` line, or the lines `vault admission --explain` printed), relay that card word for word and never compose a second one.
10
+
11
+ ## Business user forms
12
+
13
+ ### Next Up
14
+
15
+ ```
16
+ Next: {plain action}, because {reason}. Start a fresh conversation first (`/clear`) so the assistant has its full working memory, then type `{command}`.
17
+ ```
18
+
19
+ - {plain action} names the step and the plan by its name, taken from the plan's objective. Never show a plan ID.
20
+ - {reason} is one clause from that stage's `ROADMAP.md` goal or from the result just shown.
21
+ - Say "stage N of the work", never the internal word for it.
22
+ - A line that would show a command to print a report file becomes "Ask me to show the full report."
23
+ - Several options become one Next line plus "Also possible:" lines in the same shape.
24
+
25
+ ### Stopped check
26
+
27
+ `The {name} check stopped this step: {why}. What to do: {action}.`
28
+
29
+ Then `Ref: {code}`, then `Details:` followed by the check's own message exactly as printed. An advisory says the step carries on. Use the card the CLI printed; compose this shape only when none was printed (an older engine).
30
+
31
+ ### Verification result
32
+
33
+ Status line: `Done` when the report status is `passed`, `Not finished yet` for `gaps_found`, `Needs your check` for `human_needed`. Show the internal names only inside backticks.
34
+
35
+ Then three headed lists, each item one plain sentence:
36
+
37
+ - What works ({N} of {M})
38
+ - What is missing
39
+ - What needs your own test
40
+
41
+ End with the Next Up line.
42
+
43
+ ### Vault fallback question
44
+
45
+ Your organisation's own standards could not be loaded ({reason}). May the build follow the general Pareto standards instead?
46
+
47
+ Then: Type `confirmed` to go ahead. This answer is never given for the person and never assumed in an unattended run. {reason} is the plain reason, for example "the connection to your organisation's standards did not answer".
48
+
49
+ ### Irreversible action
50
+
51
+ Say the action and its target before the person answers, and keep the workflow's own way of answering: its options, or the typed name for `remove-workspace`. One row per action:
52
+
53
+ - `undo`: This will undo `{commits}`, the saved changes of `{plan name}`; they stay in the history. Then ask exactly as the workflow does, with the same options.
54
+ - `remove-workspace`: This will delete the folder `{workspace path}` and every file in it; it cannot be undone. Type `{workspace name}` to go ahead.
55
+ - `complete-milestone`: In the workflow's question, describe its "Delete without merging" option as: This will delete `{branch}`, the separate line of work for this release; use it when that work is already on the main line or not needed. Keep all of the workflow's options.
56
+ - `transition`: This will skip `{plan names}`, which are then not built in this stage of the work. Then ask exactly as the workflow does, with the same options.
57
+ - `vault-write-back`: This will write `{file}` into your organisation's standards at `{target}`, where others will see it. Then ask exactly as the workflow does, with the same options.
58
+ - `discovery-redo`: This will replace the notes of session {n} in `session-{nn}.md` and hold the session again. Then ask exactly as the workflow does, with the same options.
59
+ - `charter-redraft`: This will reopen `PROJECT-CHARTER.md` as version `{version}`; it must be signed again. Then ask exactly as the workflow does, with the same options.
60
+ - `init-compose`: This will write `.platform-bindings.json` with the answers you gave. Then ask exactly as the workflow does, with the same options.
61
+
62
+ ### Discovery narration
63
+
64
+ In a discovery session with a business user, never show file paths, internal ID counts (a tally per prefix), check codes or the `vault:` lines. Never ask them to design ID prefixes: choose the prefixes yourself from the people and areas they named and say the split in one plain sentence. Refer to a recorded item as "decision 3" or "open item 2". Keep `/pp-discovery N` commands in backticks.
65
+
66
+ ## Business engineer forms
67
+
68
+ ### Next Up
69
+
70
+ ```
71
+ Next: {plain action} (phase {N}), because {reason}. Run `/clear` first so the assistant starts with a fresh context window, then run `{command}`.
72
+ ```
73
+
74
+ - {plain action} names the step and the plan by its name from the plan's objective; never a plan ID.
75
+ - {reason} is one clause from the phase goal in `ROADMAP.md` or from the result just shown.
76
+ - Explain a real term such as "context window" once, with its line from the Explain once table in `audience.md`.
77
+ - A line that would show a command to print a report file becomes "Ask me to show the full report."
78
+ - Several options become one Next line plus "Also possible:" lines in the same shape.
79
+
80
+ ### Stopped check
81
+
82
+ `The {name} check stopped this step: {why}. What to do: {action}.`
83
+
84
+ Then `Ref: {code}`, then `Details:` followed by the check's own message exactly as printed. An advisory says the step carries on. Use the card the CLI printed, word for word; its `Ref:` line already names the query. Compose this shape only when none was printed (an older engine), and then name the check by its query, for example `vault.trace`.
85
+
86
+ ### Verification result
87
+
88
+ Status line: `Done` for `passed`, `Not finished yet` for `gaps_found` (a required outcome is missing; name the status once in backticks), `Needs your check` for `human_needed`.
89
+
90
+ Then three headed lists, each item one plain sentence:
91
+
92
+ - What works ({N} of {M})
93
+ - What is missing
94
+ - What needs your own test
95
+
96
+ End with the Next Up line.
97
+
98
+ ### Vault fallback question
99
+
100
+ Your client's standards vault could not be loaded ({reason}); the fallback tier would apply the general Pareto standards instead. Proceed with the fallback? Type `confirmed` to go ahead.
101
+
102
+ This answer is never given for the person and never assumed in an unattended run.
103
+
104
+ ### Irreversible action
105
+
106
+ Say the action and its target before the person answers, and keep the workflow's own way of answering: its options, or the typed name for `remove-workspace`. One row per action:
107
+
108
+ - `undo`: This will revert `{commits}` with `git revert`; history is kept. Then ask exactly as the workflow does, with the same options.
109
+ - `remove-workspace`: This will delete the folder `{workspace path}` and every file in it; it cannot be undone. Type `{workspace name}` to go ahead.
110
+ - `complete-milestone`: In the workflow's question, describe its "Delete without merging" option as: This will delete the branch `{branch}` of this milestone. Keep all of the workflow's options.
111
+ - `transition`: This will skip `{plan names}`; they are not built in phase {N}. Then ask exactly as the workflow does, with the same options.
112
+ - `vault-write-back`: This will write `{file}` to the repository `{repo}` at `{target}`, visible to everyone with access. Then ask exactly as the workflow does, with the same options.
113
+ - `discovery-redo`: This will overwrite the notes of session {n} in `session-{nn}.md` and run the session again. Then ask exactly as the workflow does, with the same options.
114
+ - `charter-redraft`: This will reopen `PROJECT-CHARTER.md` as version `{version}`; it must be signed again. Then ask exactly as the workflow does, with the same options.
115
+ - `init-compose`: This will write `.platform-bindings.json` with the answers you gave. Then ask exactly as the workflow does, with the same options.
116
+
117
+ ### Discovery narration
118
+
119
+ Show an ID next to its meaning ("decision D-03-2: one database"). Paths and check codes may appear in backticks. Keep `/pp-discovery N` commands in backticks.