@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,326 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * scenarios.cjs - Phase 44 REACH-02 scenarios S1-S3 (D-05, D-07, D-08).
5
+ *
6
+ * Each scenario is a fixed starting project plus fixed user turns written in a
7
+ * business user's plain words, kept next to the harness so a run is repeatable.
8
+ *
9
+ * S1 a discovery session that hits a tool error. The error is real: the notes
10
+ * for session 2 are generated from the real session templates with exactly
11
+ * one section left empty, so the installed build's own `discovery
12
+ * session-check 2` fails with its catalogued business text (D-07). No mocks.
13
+ * S2 a subagent returns. Discovery spawns no subagents, so S2 runs `/pp-quick`,
14
+ * the cheapest business-reachable workflow that spawns pp-core agents
15
+ * (`pp-planner`, then `pp-executor`). `/pp-verify-work` gap closure is
16
+ * walked by a person, not run unattended.
17
+ * S3 the early-question line deferred from Phase 40: `/pp-new-project` in an
18
+ * empty repo, where the first question must already be in plain wording.
19
+ *
20
+ * `fixtureCheck` runs offline and says whether a project really is the starting
21
+ * point the scenario needs, so a fixture that fails for the wrong reason is caught
22
+ * before a run spends money.
23
+ */
24
+
25
+ const { execFileSync, spawnSync } = require('node:child_process');
26
+ const fs = require('node:fs');
27
+ const path = require('node:path');
28
+
29
+ const { installEnv } = require('./scratch-project.cjs');
30
+ const { withoutGitRepoEnv } = require('./driver.cjs');
31
+
32
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
33
+ const WORKFLOW_PATH = path.join(REPO_ROOT, 'pp-core', 'workflows', 'discovery-sprint.md');
34
+ const COMPILED_CHECK = path.join(REPO_ROOT, 'pp-core', 'bin', 'lib', 'discovery-session-check.cjs');
35
+ const DISCOVERY_DIR = path.join('.planning', 'discovery');
36
+ const FIXTURE_DAY = '2026-11-02';
37
+ const FIXTURE_LATER_DAY = '2026-11-03';
38
+
39
+ const pad = (n) => String(n).padStart(2, '0');
40
+
41
+ /** The compiled session-check, or a plain instruction when the library is not built yet. */
42
+ function sessionCheckLib() {
43
+ try {
44
+ return require(COMPILED_CHECK);
45
+ } catch (err) {
46
+ if (err && err.code === 'MODULE_NOT_FOUND') throw new Error('run npm run build:lib first (the discovery session check is not built)');
47
+ throw err;
48
+ }
49
+ }
50
+
51
+ /** The `_progress.md` schema, as the discovery workflow writes it (its Bootstrap Templates code block). */
52
+ function progressSchema() {
53
+ const text = fs.readFileSync(WORKFLOW_PATH, 'utf8');
54
+ const marker = text.indexOf('**`.planning/discovery/_progress.md`**');
55
+ if (marker < 0) throw new Error('scenarios: the _progress.md schema was not found in discovery-sprint.md');
56
+ const open = text.indexOf('```markdown\n', marker);
57
+ const close = text.indexOf('\n```', open + 12);
58
+ if (open < 0 || close < 0) throw new Error('scenarios: the _progress.md schema block is not closed in discovery-sprint.md');
59
+ return text.slice(open + '```markdown\n'.length, close);
60
+ }
61
+
62
+ /** One row per session 1-13 from the schema: { n, name, category }. */
63
+ function sessionRows() {
64
+ const rows = [];
65
+ for (const l of progressSchema().split('\n')) {
66
+ const m = /^\| (\d+) \| (.+?) \| (.+?) \| Not Started \|/.exec(l);
67
+ if (m) rows.push({ n: Number(m[1]), name: m[2], category: m[3] });
68
+ }
69
+ if (rows.length !== 13) throw new Error(`scenarios: expected 13 session rows in the schema, found ${rows.length}`);
70
+ return rows;
71
+ }
72
+
73
+ /**
74
+ * The tracker text in the schema's shape. `rows` maps a session number to Completed,
75
+ * In Progress or Not Started (default); started and completed rows get fixed dates.
76
+ */
77
+ function progressFile({ project, client, rows = {} }) {
78
+ const status = (n) => rows[n] || 'Not Started';
79
+ const lines = progressSchema().split('\n').map((l) => {
80
+ const m = /^\| (\d+) \| (.+?) \| (.+?) \| Not Started \|/.exec(l);
81
+ if (!m) return l;
82
+ const n = Number(m[1]);
83
+ const s = status(n);
84
+ const started = s === 'Not Started' ? '' : FIXTURE_DAY;
85
+ const completed = s === 'Completed' ? FIXTURE_LATER_DAY : '';
86
+ const score = s === 'Completed' ? '100' : '';
87
+ return `| ${n} | ${m[2]} | ${m[3]} | ${s} | ${started} | ${completed} | ${score} | 0 | 0 | |`;
88
+ });
89
+ const count = (s) => Object.keys(rows).filter((k) => rows[k] === s).length;
90
+ const done = count('Completed');
91
+ const doing = count('In Progress');
92
+ const text = lines.join('\n')
93
+ .replaceAll('[PROJECT_NAME]', project)
94
+ .replaceAll('[CLIENT_NAME]', client)
95
+ .replaceAll("[today's date]", FIXTURE_LATER_DAY)
96
+ .replace('**Overall Status:** Not Started', `**Overall Status:** ${doing + done > 0 ? 'In Progress' : 'Not Started'}`)
97
+ .replace('- **Completed:** 0/13', `- **Completed:** ${done}/13`)
98
+ .replace('- **In Progress:** 0/13', `- **In Progress:** ${doing}/13`)
99
+ .replace('- **Not Started:** 13/13', `- **Not Started:** ${13 - done - doing}/13`);
100
+ return text.endsWith('\n') ? text : `${text}\n`;
101
+ }
102
+
103
+ const TABLE_PHRASES = Object.freeze([
104
+ 'Taking an order by phone', 'The shop owner', 'Every morning', 'Notes in a paper diary', 'About ten minutes',
105
+ 'Often a little late', 'Two people', 'A shared spreadsheet', 'Mostly fine', 'Needs a tidy up',
106
+ ]);
107
+ const PROSE = 'Harbour Flowers is a small flower shop that takes orders by phone and writes them in a paper diary.';
108
+
109
+ /**
110
+ * Session notes built from the real template, the way a facilitator would fill them:
111
+ * a table gets its header, a separator and one row of short plain phrases, prose gets
112
+ * one plain sentence. A title in `blank` is left as the template leaves it (a table keeps
113
+ * its header and one row of empty cells, prose gets nothing), so the real check reports
114
+ * that section as empty and nothing else.
115
+ */
116
+ function sessionNotes(n, { status = 'Completed', blank = [] } = {}) {
117
+ const { requiredSectionsOf, SESSIONS_DIR } = sessionCheckLib();
118
+ const file = fs.readdirSync(SESSIONS_DIR).find((f) => f.startsWith(`${pad(n)}-`));
119
+ if (!file) throw new Error(`scenarios: no session template for session ${n}`);
120
+ const template = fs.readFileSync(path.join(SESSIONS_DIR, file), 'utf8');
121
+ const heading = /^### Session \d+: (.+)$/m.exec(template);
122
+ const title = heading ? heading[1].trim() : `Session ${n}`;
123
+ const row = sessionRows().find((r) => r.n === n);
124
+ const category = row ? row.category : 'Business Functional';
125
+
126
+ let cell = 0;
127
+ const body = requiredSectionsOf(template).map((r) => {
128
+ if (r.kind === 'table') {
129
+ const head = `| ${r.header.join(' | ')} |`;
130
+ const sep = `|${r.header.map(() => '---').join('|')}|`;
131
+ // A blank table is the template's own untouched table: its header and one row of empty cells.
132
+ if (blank.includes(r.title)) return `**${r.title}**\n\n${head}\n${sep}\n|${r.header.map(() => ' ').join('|')}|\n`;
133
+ const cells = r.header.map(() => TABLE_PHRASES[cell++ % TABLE_PHRASES.length]);
134
+ return `**${r.title}**\n\n${head}\n${sep}\n| ${cells.join(' | ')} |\n`;
135
+ }
136
+ if (blank.includes(r.title)) return `**${r.title}**\n`;
137
+ return `**${r.title}**\n\n${PROSE}\n`;
138
+ }).join('\n');
139
+
140
+ const front = [
141
+ '---',
142
+ `session: ${n}`,
143
+ `title: "${title}"`,
144
+ `category: "${category}"`,
145
+ `status: "${status}"`,
146
+ `started_date: "${FIXTURE_DAY}"`,
147
+ ...(status === 'Completed' ? [`completed_date: "${FIXTURE_LATER_DAY}"`] : []),
148
+ `completeness_score: ${status === 'Completed' ? 100 : 80}`,
149
+ '---',
150
+ ].join('\n');
151
+ return `${front}\n\n# Session ${n} — ${title}\n\n${body}\n`;
152
+ }
153
+
154
+ function write(projectDir, rel, content) {
155
+ const abs = path.join(projectDir, rel);
156
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
157
+ fs.writeFileSync(abs, content);
158
+ }
159
+
160
+ const S1_BLANK = 'Pain Points Register';
161
+
162
+ function s1Fixture(projectDir) {
163
+ write(projectDir, path.join(DISCOVERY_DIR, '_progress.md'), progressFile({
164
+ project: 'Harbour Flowers',
165
+ client: 'Harbour Flowers Ltd',
166
+ rows: { 1: 'Completed', 2: 'In Progress' },
167
+ }));
168
+ write(projectDir, path.join(DISCOVERY_DIR, 'session-01.md'), sessionNotes(1, { status: 'Completed' }));
169
+ write(projectDir, path.join(DISCOVERY_DIR, 'session-02.md'), sessionNotes(2, { status: 'In Progress', blank: [S1_BLANK] }));
170
+ }
171
+
172
+ /** Run the installed build's own session check on session 2 and say whether it fails for exactly the planted reason. */
173
+ function s1FixtureCheck(projectDir, { personHome }) {
174
+ const tools = path.join(projectDir, '.claude', 'pp-core', 'bin', 'pp-tools.cjs');
175
+ if (!fs.existsSync(tools)) return { ok: false, detail: `the installed pp-tools is missing at ${tools}` };
176
+ const env = installEnv(personHome);
177
+ env.PP_AUDIENCE = 'business';
178
+ const res = spawnSync(process.execPath, [tools, 'discovery', 'session-check', '2'], { cwd: projectDir, env, encoding: 'utf8', timeout: 60000 });
179
+ const out = `${res.stdout || ''}${res.stderr || ''}`;
180
+ const refs = out.match(/Ref: discovery\.session\.[a-z_]+/g) || [];
181
+ const failedOnce = out.split('Ref: discovery_session_check_failed').length - 1 === 1;
182
+ const ok = res.status !== 0 && failedOnce && refs.length === 1 && refs[0] === 'Ref: discovery.session.empty_section' && out.includes(S1_BLANK);
183
+ // The catalogued business text follows the JSON report (on the same line, as the CLI prints it).
184
+ const from = out.indexOf('Your discovery notes');
185
+ const head = out.slice(from < 0 ? 0 : from).trim().split('\n').slice(0, 12).join('\n');
186
+ return { ok, detail: ok ? head : `expected exactly one empty section (${S1_BLANK}); exit ${res.status}\n${head}` };
187
+ }
188
+
189
+ function s2Fixture(projectDir) {
190
+ write(projectDir, path.join('.planning', 'PROJECT.md'), [
191
+ '# Harbour Flowers website',
192
+ '',
193
+ '## What this is',
194
+ '',
195
+ 'A small website for Harbour Flowers, a flower shop. It tells customers when the shop is open and how to order.',
196
+ '',
197
+ '## Core value',
198
+ '',
199
+ 'A customer can find the opening hours and place an order without phoning the shop.',
200
+ '',
201
+ '## Requirements',
202
+ '',
203
+ '- Show the opening hours on their own page.',
204
+ '- Show how to order.',
205
+ '',
206
+ ].join('\n'));
207
+ write(projectDir, path.join('.planning', 'ROADMAP.md'), [
208
+ '# Roadmap: Harbour Flowers website',
209
+ '',
210
+ '## Overview',
211
+ '',
212
+ 'One small step: a page with the shop opening hours.',
213
+ '',
214
+ '## Phases',
215
+ '',
216
+ '- [ ] **Phase 1: Opening hours page** - Customers can see when the shop is open',
217
+ '',
218
+ '## Phase Details',
219
+ '',
220
+ '### Phase 1: Opening hours page',
221
+ '**Goal**: Customers can see when the shop is open.',
222
+ '**Depends on**: Nothing (first phase)',
223
+ '**Plans**: TBD',
224
+ '',
225
+ ].join('\n'));
226
+ write(projectDir, path.join('.planning', 'STATE.md'), [
227
+ '# Project State',
228
+ '',
229
+ '## Project Reference',
230
+ '',
231
+ 'See: .planning/PROJECT.md',
232
+ '',
233
+ '**Core value:** A customer can find the opening hours and place an order without phoning the shop.',
234
+ '**Current focus:** Opening hours page',
235
+ '',
236
+ '## Current Position',
237
+ '',
238
+ 'Phase: 1 of 1 (Opening hours page)',
239
+ 'Plan: 0 of 0 in current phase',
240
+ 'Status: Ready to plan',
241
+ `Last activity: ${FIXTURE_DAY} — Project set up`,
242
+ '',
243
+ 'Progress: [░░░░░░░░░░] 0%',
244
+ '',
245
+ '## Accumulated Context',
246
+ '',
247
+ '### Decisions',
248
+ '',
249
+ 'None yet.',
250
+ '',
251
+ '### Pending Todos',
252
+ '',
253
+ 'None yet.',
254
+ '',
255
+ '### Blockers/Concerns',
256
+ '',
257
+ 'None yet.',
258
+ '',
259
+ '## Session Continuity',
260
+ '',
261
+ `Last session: ${FIXTURE_DAY}`,
262
+ 'Stopped at: Project set up',
263
+ 'Resume file: None',
264
+ '',
265
+ ].join('\n'));
266
+ }
267
+
268
+ /** git in the scratch project, never steered at another repository by an inherited GIT_DIR. */
269
+ const gitOut = (cwd, args) => execFileSync('git', args, { cwd, env: withoutGitRepoEnv(process.env), encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
270
+
271
+ function s2FixtureCheck(projectDir) {
272
+ const problems = [];
273
+ try {
274
+ if (gitOut(projectDir, ['rev-list', '--count', 'HEAD']) !== '1') problems.push('the repo does not hold exactly one commit');
275
+ if (gitOut(projectDir, ['status', '--porcelain']) !== '') problems.push('the working tree is not clean');
276
+ } catch (err) {
277
+ problems.push(`git failed: ${String(err.message).split('\n')[0]}`);
278
+ }
279
+ if (!fs.existsSync(path.join(projectDir, '.planning', 'ROADMAP.md'))) problems.push('.planning/ROADMAP.md is missing');
280
+ return { ok: problems.length === 0, detail: problems.length === 0 ? 'one commit, clean tree, roadmap present' : problems.join('; ') };
281
+ }
282
+
283
+ function s3FixtureCheck(projectDir) {
284
+ const problems = [];
285
+ if (fs.existsSync(path.join(projectDir, '.planning'))) problems.push('a .planning directory exists');
286
+ const head = spawnSync('git', ['rev-parse', '--verify', '-q', 'HEAD'], { cwd: projectDir, env: withoutGitRepoEnv(process.env), encoding: 'utf8' });
287
+ if (head.status === 0) problems.push('the repo already has a commit');
288
+ return { ok: problems.length === 0, detail: problems.length === 0 ? 'empty repo, no planning folder, no commit' : problems.join('; ') };
289
+ }
290
+
291
+ const freeze = (s) => Object.freeze({ ...s, turns: Object.freeze([...s.turns]) });
292
+
293
+ const SCENARIOS = Object.freeze({
294
+ s1: freeze({
295
+ id: 's1',
296
+ title: 'A discovery session that hits a tool error',
297
+ command: 'pp-discovery',
298
+ audience: 'business',
299
+ initialCommit: false,
300
+ turns: ['/pp-discovery 2', 'That is everything for this session. Please finish it.'],
301
+ fixture: s1Fixture,
302
+ fixtureCheck: s1FixtureCheck,
303
+ }),
304
+ s2: freeze({
305
+ id: 's2',
306
+ title: 'A subagent returns',
307
+ command: 'pp-quick',
308
+ audience: 'business',
309
+ initialCommit: true,
310
+ turns: ['/pp-quick add a file called thanks.txt that says Thank you for your order'],
311
+ fixture: s2Fixture,
312
+ fixtureCheck: s2FixtureCheck,
313
+ }),
314
+ s3: freeze({
315
+ id: 's3',
316
+ title: 'The first question before any setup call',
317
+ command: 'pp-new-project',
318
+ audience: 'business',
319
+ initialCommit: false,
320
+ turns: ['/pp-new-project'],
321
+ fixture: () => {},
322
+ fixtureCheck: s3FixtureCheck,
323
+ }),
324
+ });
325
+
326
+ module.exports = { SCENARIOS, sessionNotes, progressFile };
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * scratch-project.cjs - Phase 44 throwaway project with this build installed locally (D-01).
5
+ *
6
+ * Shared by the live runs and the walk setup, so both exercise the same install.
7
+ * The global install is an older version, so only a project-local install exercises
8
+ * this milestone's code (a project-local install answers first). The build refuses to
9
+ * continue unless the installed audience reference is byte-equal to the repo copy and
10
+ * the scenario's command file is there: a run on a missing or global install proves
11
+ * nothing (Pitfalls 1 and 2).
12
+ *
13
+ * The scratch repo has the throwaway identity and no remote, ever.
14
+ */
15
+
16
+ const { execFileSync } = require('node:child_process');
17
+ const fs = require('node:fs');
18
+ const os = require('node:os');
19
+ const path = require('node:path');
20
+
21
+ const { CLAUDE_ENV_SCRUB_RE, GIT_IDENTITY, gitIdentityEnv, withoutGitRepoEnv } = require('./driver.cjs');
22
+
23
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
24
+ const LABEL_RE = /^[a-z0-9-]{1,32}$/;
25
+ const INSTALL_TIMEOUT_MS = 120000;
26
+ const AUDIENCE_REF = path.join('pp-core', 'references', 'audience.md');
27
+
28
+ /**
29
+ * The env the installer runs in: no audience, no config dir, no session markers and no
30
+ * GIT_DIR-style pointer at another repository; the person home is the temp one.
31
+ */
32
+ function installEnv(personHome, baseEnv = process.env) {
33
+ const env = withoutGitRepoEnv(baseEnv);
34
+ delete env.PP_AUDIENCE;
35
+ delete env.CLAUDE_CONFIG_DIR;
36
+ for (const key of Object.keys(env)) {
37
+ if (CLAUDE_ENV_SCRUB_RE.test(key)) delete env[key];
38
+ }
39
+ env.GSD_HOME = personHome;
40
+ return env;
41
+ }
42
+
43
+ /** Throw unless this build, not another one, is what the project has installed. */
44
+ function assertLocalInstall(projectDir, { repoRoot = REPO_ROOT, commands = [] } = {}) {
45
+ const installed = path.join(projectDir, '.claude', AUDIENCE_REF);
46
+ const original = path.join(repoRoot, AUDIENCE_REF);
47
+ if (!fs.existsSync(installed)) throw new Error(`local install check: ${installed} is missing (audience.md)`);
48
+ if (!fs.readFileSync(installed).equals(fs.readFileSync(original))) {
49
+ throw new Error(`local install check: the installed audience.md differs from ${original}; the project does not hold this build`);
50
+ }
51
+ for (const command of commands) {
52
+ const file = path.join(projectDir, '.claude', 'commands', `${command}.md`);
53
+ if (!fs.existsSync(file)) throw new Error(`local install check: command file ${command}.md is not installed under ${path.dirname(file)}`);
54
+ }
55
+ }
56
+
57
+ const git = (cwd, args, env) => execFileSync('git', args, { cwd, env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
58
+
59
+ function removeDir(dir) {
60
+ fs.rmSync(dir, { recursive: true, force: true });
61
+ }
62
+
63
+ /**
64
+ * Build a scratch project: a git repo with the throwaway identity and no remote,
65
+ * the scenario's fixture, this build installed locally (with the audience saved in
66
+ * the temp person home), the install verified, and an optional starting commit.
67
+ * `.claude/` is kept out of the repo through .git/info/exclude so the model's own
68
+ * `git status` and commits see only the project's files.
69
+ */
70
+ function createScratchProject({ label, audience, fixture, initialCommit = false, commands = [], repoRoot = REPO_ROOT, tmpRoot, dir } = {}) {
71
+ if (typeof label !== 'string' || !LABEL_RE.test(label)) {
72
+ throw new Error(`createScratchProject: label must be 1-32 lowercase letters, digits or hyphens, got ${JSON.stringify(label)}`);
73
+ }
74
+ const root = tmpRoot || os.tmpdir();
75
+ let projectDir;
76
+ let madeProject = false;
77
+ if (dir) {
78
+ if (fs.existsSync(dir) && fs.readdirSync(dir).length > 0) throw new Error(`createScratchProject: ${dir} must be empty`);
79
+ madeProject = !fs.existsSync(dir);
80
+ fs.mkdirSync(dir, { recursive: true });
81
+ projectDir = dir;
82
+ } else {
83
+ projectDir = fs.mkdtempSync(path.join(root, `pp-live-${label}-`));
84
+ madeProject = true;
85
+ }
86
+ const personHome = fs.mkdtempSync(path.join(root, 'pp-live-home-'));
87
+
88
+ const cleanup = () => {
89
+ if (madeProject) removeDir(projectDir);
90
+ else if (fs.existsSync(projectDir)) for (const name of fs.readdirSync(projectDir)) removeDir(path.join(projectDir, name));
91
+ removeDir(personHome);
92
+ };
93
+
94
+ try {
95
+ const identity = { ...withoutGitRepoEnv(process.env), ...gitIdentityEnv() };
96
+ git(projectDir, ['-c', 'init.defaultBranch=main', 'init', '-q'], identity);
97
+ git(projectDir, ['config', 'user.name', GIT_IDENTITY.name], identity);
98
+ git(projectDir, ['config', 'user.email', GIT_IDENTITY.email], identity);
99
+ git(projectDir, ['config', 'commit.gpgsign', 'false'], identity);
100
+ fs.appendFileSync(path.join(projectDir, '.git', 'info', 'exclude'), '.claude/\n');
101
+
102
+ if (fixture) fixture(projectDir, { repoRoot });
103
+
104
+ const installArgs = [path.join(repoRoot, 'bin', 'install.js'), '--claude', '--local'];
105
+ if (audience) installArgs.push('--audience', audience);
106
+ try {
107
+ execFileSync(process.execPath, installArgs, {
108
+ cwd: projectDir,
109
+ env: installEnv(personHome),
110
+ encoding: 'utf8',
111
+ timeout: INSTALL_TIMEOUT_MS,
112
+ stdio: ['ignore', 'pipe', 'pipe'],
113
+ });
114
+ } catch (err) {
115
+ const tail = String(err.stderr || err.stdout || err.message).trim().split('\n').slice(-8).join('\n');
116
+ throw new Error(`createScratchProject: the local install failed\n${tail}`);
117
+ }
118
+ assertLocalInstall(projectDir, { repoRoot, commands });
119
+
120
+ if (initialCommit) {
121
+ git(projectDir, ['add', '-A'], identity);
122
+ git(projectDir, ['commit', '-q', '-m', 'Starting point'], identity);
123
+ }
124
+ } catch (err) {
125
+ cleanup();
126
+ throw err;
127
+ }
128
+
129
+ return { projectDir, personHome, cleanup };
130
+ }
131
+
132
+ module.exports = { REPO_ROOT, createScratchProject, assertLocalInstall, installEnv };