@pareto4/pp-core 1.19.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/pp-nyquist-auditor.md +19 -14
  3. package/agents/pp-phase-researcher.md +10 -0
  4. package/agents/pp-plan-checker.md +59 -36
  5. package/agents/pp-planner.md +49 -53
  6. package/agents/pp-vault-reviewer.md +2 -0
  7. package/agents/pp-verifier.md +15 -60
  8. package/commands/pp-autonomous.md +2 -1
  9. package/commands/pp-discovery.md +3 -2
  10. package/commands/pp-plan-phase.md +2 -1
  11. package/commands/pp-plan-review-convergence.md +2 -1
  12. package/gemini-extension.json +1 -1
  13. package/package.json +6 -3
  14. package/pareto-skills/_shared/pp-claude-md-protocol.md +1 -1
  15. package/pareto-skills/pp-project-setup/SKILL.md +18 -343
  16. package/pp-core/bin/lib/capability-activation.cjs +77 -0
  17. package/pp-core/bin/lib/capability-registry.cjs +15 -15
  18. package/pp-core/bin/lib/capability-state.cjs +8 -18
  19. package/pp-core/bin/lib/ceremony.cjs +1342 -0
  20. package/pp-core/bin/lib/check-command-router.cjs +62 -1
  21. package/pp-core/bin/lib/commands.cjs +29 -3
  22. package/pp-core/bin/lib/config.cjs +5 -0
  23. package/pp-core/bin/lib/discovery-access.cjs +276 -0
  24. package/pp-core/bin/lib/discovery-charter.cjs +664 -0
  25. package/pp-core/bin/lib/discovery-deviations.cjs +264 -0
  26. package/pp-core/bin/lib/discovery-entities.cjs +323 -0
  27. package/pp-core/bin/lib/discovery-for-phase.cjs +628 -0
  28. package/pp-core/bin/lib/discovery-integrations.cjs +286 -0
  29. package/pp-core/bin/lib/discovery-migration.cjs +246 -0
  30. package/pp-core/bin/lib/discovery-phases.cjs +233 -0
  31. package/pp-core/bin/lib/discovery-processes.cjs +355 -0
  32. package/pp-core/bin/lib/discovery-research-track.cjs +5 -4
  33. package/pp-core/bin/lib/discovery-rollup.cjs +8 -7
  34. package/pp-core/bin/lib/discovery-rules.cjs +216 -0
  35. package/pp-core/bin/lib/discovery-screens.cjs +1 -1
  36. package/pp-core/bin/lib/discovery-session-check.cjs +264 -0
  37. package/pp-core/bin/lib/discovery-stories.cjs +273 -0
  38. package/pp-core/bin/lib/discovery-sweep.cjs +1 -1
  39. package/pp-core/bin/lib/discovery-table.cjs +55 -0
  40. package/pp-core/bin/lib/init.cjs +50 -1
  41. package/pp-core/bin/lib/io.cjs +24 -0
  42. package/pp-core/bin/lib/loop-resolver.cjs +26 -12
  43. package/pp-core/bin/lib/phase.cjs +39 -3
  44. package/pp-core/bin/lib/plan-sizing.cjs +346 -0
  45. package/pp-core/bin/lib/test-substance.cjs +3190 -0
  46. package/pp-core/bin/lib/timeline.cjs +605 -0
  47. package/pp-core/bin/lib/vault-standards-review.cjs +50 -2
  48. package/pp-core/bin/pp-tools.cjs +214 -5
  49. package/pp-core/bin/shared/ceremony-gates.json +17 -0
  50. package/pp-core/bin/shared/config-schema.manifest.json +1 -0
  51. package/pp-core/references/ceremony.md +81 -0
  52. package/pp-core/references/discovery-access.md +112 -0
  53. package/pp-core/references/discovery-build-consumption.md +256 -0
  54. package/pp-core/references/discovery-charter.md +127 -0
  55. package/pp-core/references/discovery-data-model.md +145 -0
  56. package/pp-core/references/discovery-id-conventions.md +50 -14
  57. package/pp-core/references/discovery-integrations.md +151 -0
  58. package/pp-core/references/discovery-processes.md +190 -0
  59. package/pp-core/references/discovery-rebaseline.md +2 -2
  60. package/pp-core/references/discovery-research-track.md +5 -6
  61. package/pp-core/references/discovery-session-output.md +70 -0
  62. package/pp-core/references/discovery-standards.md +107 -0
  63. package/pp-core/references/discovery-traceability.md +2 -2
  64. package/pp-core/references/few-shot-examples/plan-checker.md +8 -8
  65. package/pp-core/references/phase-timeline.md +107 -0
  66. package/pp-core/references/planner-antipatterns.md +32 -0
  67. package/pp-core/references/planner-discovery-extract.md +35 -0
  68. package/pp-core/references/planner-guidance.md +17 -14
  69. package/pp-core/references/planner-revision.md +1 -1
  70. package/pp-core/references/planning-config.md +3 -2
  71. package/pp-core/references/test-substance.md +324 -0
  72. package/pp-core/references/verifier-discovery-conformance.md +36 -0
  73. package/pp-core/references/verifier-stub-patterns.md +51 -0
  74. package/pp-core/templates/VALIDATION.md +1 -0
  75. package/pp-core/templates/phase-prompt.md +19 -7
  76. package/pp-core/templates/project-charter.md +161 -100
  77. package/pp-core/templates/project.md +3 -3
  78. package/pp-core/templates/requirements.md +18 -0
  79. package/pp-core/templates/roadmap.md +1 -1
  80. package/pp-core/templates/verification-report.md +17 -1
  81. package/pp-core/workflows/audit-milestone.md +4 -1
  82. package/pp-core/workflows/autonomous.md +11 -8
  83. package/pp-core/workflows/code-review.md +15 -6
  84. package/pp-core/workflows/discovery-sprint/charter.md +114 -0
  85. package/pp-core/workflows/discovery-sprint/prefill.md +1 -1
  86. package/pp-core/workflows/discovery-sprint/prototype.md +4 -4
  87. package/pp-core/workflows/discovery-sprint/session-13-gates.md +10 -0
  88. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +5 -2
  89. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +27 -5
  90. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +56 -31
  91. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +1 -1
  92. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +42 -19
  93. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +27 -12
  94. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +41 -23
  95. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +50 -23
  96. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +26 -11
  97. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +28 -9
  98. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +21 -0
  99. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +24 -9
  100. package/pp-core/workflows/discovery-sprint.md +12 -15
  101. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +80 -29
  102. package/pp-core/workflows/discovery-to-build/steps/{screen-inventory-gate.md → handoff-gates.md} +111 -3
  103. package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +12 -9
  104. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +27 -26
  105. package/pp-core/workflows/discovery-to-build.md +70 -34
  106. package/pp-core/workflows/discuss-phase/discovery.md +83 -0
  107. package/pp-core/workflows/discuss-phase/templates/context.md +4 -0
  108. package/pp-core/workflows/discuss-phase.md +10 -7
  109. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +7 -7
  110. package/pp-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -2
  111. package/pp-core/workflows/execute-phase.md +14 -3
  112. package/pp-core/workflows/new-project.md +3 -3
  113. package/pp-core/workflows/plan-phase/steps/chunked-planning.md +7 -3
  114. package/pp-core/workflows/plan-phase/steps/discovery-extract.md +41 -0
  115. package/pp-core/workflows/plan-phase/steps/prd-express.md +104 -0
  116. package/pp-core/workflows/plan-phase.md +62 -124
  117. package/pp-core/workflows/plan-review-convergence.md +6 -3
  118. package/pp-core/workflows/ship.md +2 -2
  119. package/pp-core/workflows/stats.md +19 -4
  120. package/pp-core/workflows/validate-phase.md +19 -4
  121. package/pp-core/workflows/vault-review.md +5 -0
  122. package/pp-core/workflows/verify-phase.md +1 -1
  123. package/pp-core/workflows/verify-work.md +1 -1
  124. package/scripts/gen-capability-registry.cjs +43 -45
  125. package/scripts/run-tests.cjs +54 -30
  126. package/pp-core/workflows/discovery-sprint/templates/context-template.md +0 -272
@@ -0,0 +1,273 @@
1
+ "use strict";
2
+ /**
3
+ * Discovery user-stories check (Discovery Sprint improvement plan, Batch 4).
4
+ *
5
+ * `.planning/discovery/_stories.md` is the register of user stories Session 3
6
+ * Step 5E mints from the step matrices: `US-nn`, the EPIC it belongs to, the
7
+ * requirements it realises, the story itself, its acceptance criteria and the
8
+ * step it was derived from. Before Batch 4 the stories lived only in Session
9
+ * 3's Notes Template, where no build agent read them (review F-4); the
10
+ * verifier now takes its UAT from this register through `for-phase`. This
11
+ * module checks the structure:
12
+ *
13
+ * - the `## Stories` table exists with the template's columns;
14
+ * - `US-` IDs are well-formed, unique, contiguous;
15
+ * - `EPIC` is one `E-nn`;
16
+ * - every story cites at least one requirement, each existing in
17
+ * `_requirements.md` when it is on file and parseable;
18
+ * - `Story` and `Acceptance criteria` are non-empty;
19
+ * - `Acceptance criteria` is a criterion, not a pointer (`As in F-01`);
20
+ * - `Source step` is `—` or a comma list of `MP-nn #<cell>`, the main process
21
+ * and the step row existing in `_processes.md` when it is on file;
22
+ * - every MVP-disposition requirement that a persona-actor step cites has at
23
+ * least one story (the `N-`/`T-` prefixes never need one; a requirement
24
+ * only `System` steps realise, or no step at all, has no user to tell it).
25
+ *
26
+ * Posture (D-43/D-53): reports at Sessions 8 and 13; hard-blocks at
27
+ * `/pp-discovery-to-build`. Absent artifact → exit 0.
28
+ */
29
+ var __importDefault = (this && this.__importDefault) || function (mod) {
30
+ return (mod && mod.__esModule) ? mod : { "default": mod };
31
+ };
32
+ const node_fs_1 = __importDefault(require("node:fs"));
33
+ const node_path_1 = __importDefault(require("node:path"));
34
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
35
+ const io = require("./io.cjs");
36
+ const { output, error, ERROR_REASON } = io;
37
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
38
+ const table = require("./discovery-table.cjs");
39
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
40
+ const rollup = require("./discovery-rollup.cjs");
41
+ // ─── Constants ────────────────────────────────────────────────────────────────
42
+ const DISCOVERY_DIR = '.planning/discovery';
43
+ const STORIES_FILE = `${DISCOVERY_DIR}/_stories.md`;
44
+ const REQUIREMENTS_FILE = `${DISCOVERY_DIR}/_requirements.md`;
45
+ const PROCESSES_FILE = `${DISCOVERY_DIR}/_processes.md`;
46
+ const STORIES_SECTION = 'Stories';
47
+ const STORIES_HEADER = Object.freeze(['ID', 'EPIC', 'Requirements', 'Story', 'Acceptance criteria', 'Source step']);
48
+ const US_RE = /^US-(\d{2})$/;
49
+ const EPIC_RE = /^E-\d{2}$/;
50
+ const REQUIREMENT_ID_RE = /^[A-Z][A-Z0-9]*-\d+$/;
51
+ /** `MP-nn #<cell>` — the main process and the step matrix's `#` cell, copied verbatim (`01.3`, `5`). */
52
+ const SOURCE_STEP_RE = /^(MP-\d{2}) #(\S+)$/;
53
+ const MP_HEADING_RE = /^(MP-\d{2})\s+—\s+\S/;
54
+ const SYSTEM_ACTOR = 'System';
55
+ /** An acceptance-criteria cell that only points elsewhere is no criterion the verifier can run. */
56
+ const POINTER_AC_RE = /^(as in|see|zie|idem)\b/i;
57
+ const PENDING = '—';
58
+ /** Requirement prefixes that need no user story: non-functional and technical rows have no actor. */
59
+ const STORYLESS_PREFIXES = Object.freeze(['N-', 'T-']);
60
+ const USAGE = 'Usage: pp-tools discovery stories-check';
61
+ function readIfPresent(abs) {
62
+ if (!node_fs_1.default.existsSync(abs))
63
+ return { kind: 'absent' };
64
+ try {
65
+ return { kind: 'ok', content: node_fs_1.default.readFileSync(abs, 'utf-8') };
66
+ }
67
+ catch {
68
+ return { kind: 'unreadable' };
69
+ }
70
+ }
71
+ function sameHeader(actual, expected) {
72
+ return actual.length === expected.length && actual.every((c, i) => c.trim().toLowerCase() === expected[i].toLowerCase());
73
+ }
74
+ function splitList(cell) {
75
+ return cell.split(',').map((t) => t.trim()).filter(Boolean);
76
+ }
77
+ /** First-column IDs of the first table under a `## <heading>` of another artifact. */
78
+ function firstColumnOf(content, heading) {
79
+ const section = table.splitSections(content.split(/\r?\n/), 2).find((s) => s.heading === heading);
80
+ if (!section)
81
+ return new Set();
82
+ const tables = table.parseTables(section.body);
83
+ if (tables.length === 0)
84
+ return new Set();
85
+ return new Set(tables[0].rows.map((r) => (r.cells[0] ?? '').trim()).filter(Boolean));
86
+ }
87
+ /**
88
+ * Every step row of `_processes.md` § Steps: its key (`MP-nn #<cell>`), its
89
+ * actors and the requirements it cites. `null` when the section is absent —
90
+ * the step rule then cannot run and only the prefix exemption applies.
91
+ */
92
+ function stepsOf(content) {
93
+ const steps = table.splitSections(content.split(/\r?\n/), 2).find((s) => s.heading === 'Steps');
94
+ if (!steps)
95
+ return null;
96
+ const out = [];
97
+ for (const sec of table.splitSections(steps.body.map((l) => l.text), 3)) {
98
+ const m = MP_HEADING_RE.exec(sec.heading);
99
+ const tbl = table.parseTables(sec.body)[0];
100
+ if (!m || !tbl)
101
+ continue;
102
+ for (const r of tbl.rows) {
103
+ out.push({ key: `${m[1]} #${(r.cells[0] ?? '').trim()}`, actors: splitList(r.cells[2] ?? ''), requirements: splitList(r.cells[7] ?? '') });
104
+ }
105
+ }
106
+ return out;
107
+ }
108
+ /**
109
+ * Does this MVP requirement need a user story? Step 5E derives stories from the
110
+ * step rows where the actor is a persona, so the check mirrors that: a
111
+ * requirement needs one when at least one step citing it has an actor other
112
+ * than `System`. Without a step matrix the prefix exemption is all there is.
113
+ */
114
+ function needsStory(r, steps) {
115
+ if (STORYLESS_PREFIXES.includes(r.prefix))
116
+ return false;
117
+ if (steps === null)
118
+ return true;
119
+ return steps.some((s) => s.requirements.includes(r.id) && s.actors.some((a) => a !== SYSTEM_ACTOR));
120
+ }
121
+ // ─── Check ────────────────────────────────────────────────────────────────────
122
+ function checkStories(content, sources) {
123
+ const violations = [];
124
+ const sections = table.splitSections(content.split(/\r?\n/), 2);
125
+ const section = sections.find((s) => s.heading === STORIES_SECTION);
126
+ if (!section) {
127
+ return { ok: false, stories: 0, violations: [{ artifact: STORIES_FILE, reason: 'missing_section', id: STORIES_SECTION }] };
128
+ }
129
+ const tables = table.parseTables(section.body);
130
+ if (tables.length === 0) {
131
+ return { ok: false, stories: 0, violations: [{ artifact: STORIES_FILE, reason: 'missing_section', line: section.line, id: STORIES_SECTION }] };
132
+ }
133
+ if (!sameHeader(tables[0].header, STORIES_HEADER)) {
134
+ return { ok: false, stories: 0, violations: [{ artifact: STORIES_FILE, reason: 'bad_table_shape', line: tables[0].line, id: STORIES_SECTION }] };
135
+ }
136
+ const knownIds = sources.requirements instanceof Array ? new Set(sources.requirements.map((r) => r.id)) : null;
137
+ const ids = new Set();
138
+ const seqs = [];
139
+ const storied = new Set();
140
+ for (const row of tables[0].rows) {
141
+ const [id, epic, requirements, story, acceptance, sourceStep] = row.cells.map((c) => c.trim());
142
+ const m = US_RE.exec(id ?? '');
143
+ if (!m) {
144
+ violations.push({ artifact: STORIES_FILE, reason: 'bad_id', line: row.line });
145
+ }
146
+ else if (ids.has(id)) {
147
+ violations.push({ artifact: STORIES_FILE, reason: 'duplicate_id', line: row.line, id });
148
+ }
149
+ else {
150
+ ids.add(id);
151
+ seqs.push(Number(m[1]));
152
+ }
153
+ if (!EPIC_RE.test(epic ?? ''))
154
+ violations.push({ artifact: STORIES_FILE, reason: 'bad_epic', line: row.line });
155
+ if ((story ?? '') === '')
156
+ violations.push({ artifact: STORIES_FILE, reason: 'empty_cell', line: row.line, id: 'Story' });
157
+ if ((acceptance ?? '') === '')
158
+ violations.push({ artifact: STORIES_FILE, reason: 'empty_cell', line: row.line, id: 'Acceptance criteria' });
159
+ else if (POINTER_AC_RE.test(acceptance))
160
+ violations.push({ artifact: STORIES_FILE, reason: 'acceptance_is_pointer', line: row.line, id });
161
+ const reqs = splitList(requirements ?? '');
162
+ if (reqs.length === 0)
163
+ violations.push({ artifact: STORIES_FILE, reason: 'no_requirement', line: row.line });
164
+ for (const r of reqs) {
165
+ if (!REQUIREMENT_ID_RE.test(r))
166
+ violations.push({ artifact: STORIES_FILE, reason: 'unknown_requirement_id', line: row.line, id: r.slice(0, 40) });
167
+ else if (knownIds !== null && !knownIds.has(r))
168
+ violations.push({ artifact: STORIES_FILE, reason: 'unknown_requirement_id', line: row.line, id: r });
169
+ else
170
+ storied.add(r);
171
+ }
172
+ const src = sourceStep ?? '';
173
+ if (src !== PENDING) {
174
+ const refs = splitList(src);
175
+ if (refs.length === 0)
176
+ violations.push({ artifact: STORIES_FILE, reason: 'bad_source_step', line: row.line });
177
+ for (const ref of refs) {
178
+ const sm = SOURCE_STEP_RE.exec(ref);
179
+ if (!sm)
180
+ violations.push({ artifact: STORIES_FILE, reason: 'bad_source_step', line: row.line });
181
+ else if (sources.processes !== null && !sources.processes.has(sm[1]))
182
+ violations.push({ artifact: STORIES_FILE, reason: 'unknown_process_id', line: row.line, id: sm[1] });
183
+ else if (sources.steps !== null && !sources.steps.some((st) => st.key === ref))
184
+ violations.push({ artifact: STORIES_FILE, reason: 'unknown_process_step', line: row.line, id: ref.slice(0, 40) });
185
+ }
186
+ }
187
+ }
188
+ const max = table.boundedSequenceMax(seqs);
189
+ if (max !== null) {
190
+ const have = new Set(seqs);
191
+ for (let n = 1; n <= max; n += 1) {
192
+ if (!have.has(n))
193
+ violations.push({ artifact: STORIES_FILE, reason: 'sequence_gap', id: `US-${String(n).padStart(2, '0')}` });
194
+ }
195
+ }
196
+ if (sources.requirements instanceof Array) {
197
+ for (const r of sources.requirements) {
198
+ if (r.disposition !== rollup.DISPOSITIONS.MVP)
199
+ continue;
200
+ if (!needsStory(r, sources.steps))
201
+ continue;
202
+ if (!storied.has(r.id))
203
+ violations.push({ artifact: REQUIREMENTS_FILE, reason: 'mvp_requirement_without_story', id: r.id });
204
+ }
205
+ }
206
+ else if (sources.requirements === 'unparseable') {
207
+ violations.push({ artifact: REQUIREMENTS_FILE, reason: 'requirements_unparseable' });
208
+ }
209
+ return { ok: violations.length === 0, stories: ids.size, violations };
210
+ }
211
+ function checkProjectStories(cwd) {
212
+ const read = readIfPresent(node_path_1.default.join(cwd, STORIES_FILE));
213
+ if (read.kind === 'absent')
214
+ return null;
215
+ if (read.kind === 'unreadable')
216
+ return { ok: false, file: STORIES_FILE, stories: 0, violations: [{ artifact: STORIES_FILE, reason: 'artifact_unreadable' }] };
217
+ const extra = [];
218
+ const sources = { requirements: null, processes: null, steps: null };
219
+ const req = readIfPresent(node_path_1.default.join(cwd, REQUIREMENTS_FILE));
220
+ if (req.kind === 'unreadable')
221
+ extra.push({ artifact: REQUIREMENTS_FILE, reason: 'artifact_unreadable' });
222
+ if (req.kind === 'ok') {
223
+ const parsed = rollup.parseRequirementsArtifact(req.content);
224
+ sources.requirements = parsed.ok ? parsed.rows.map((r) => ({ id: r.id, prefix: r.prefix, disposition: r.disposition })) : 'unparseable';
225
+ }
226
+ const proc = readIfPresent(node_path_1.default.join(cwd, PROCESSES_FILE));
227
+ if (proc.kind === 'unreadable')
228
+ extra.push({ artifact: PROCESSES_FILE, reason: 'artifact_unreadable' });
229
+ if (proc.kind === 'ok') {
230
+ sources.processes = firstColumnOf(proc.content, 'To-be');
231
+ sources.steps = stepsOf(proc.content);
232
+ }
233
+ const result = checkStories(read.content, sources);
234
+ result.violations.push(...extra);
235
+ result.ok = result.violations.length === 0;
236
+ return { file: STORIES_FILE, ...result };
237
+ }
238
+ function formatViolation(v) {
239
+ const where = v.line !== undefined ? `:${v.line}` : '';
240
+ const what = v.id !== undefined ? ` (${v.id})` : '';
241
+ return ` - ${v.artifact}${where}: ${v.reason}${what}`;
242
+ }
243
+ // ─── CLI adapter ──────────────────────────────────────────────────────────────
244
+ /** `pp-tools discovery stories-check` (dotted `query discovery.stories-check`). Absent artifact → exit 0, `present: false`. */
245
+ function cmdDiscoveryStoriesCheck(cwd, args, raw) {
246
+ if (args.length > 2) {
247
+ error(USAGE, ERROR_REASON.USAGE);
248
+ return; // unreachable
249
+ }
250
+ const result = checkProjectStories(cwd);
251
+ if (result === null) {
252
+ output({ ok: true, present: false, file: STORIES_FILE }, raw);
253
+ return;
254
+ }
255
+ const payload = { ok: result.ok, present: true, file: result.file, stories: result.stories };
256
+ if (!result.ok) {
257
+ output({ ...payload, violations: result.violations.length }, raw);
258
+ error(`discovery stories-check: ${result.violations.length} violation(s):\n` + result.violations.map(formatViolation).join('\n'), ERROR_REASON.DISCOVERY_STORIES_CHECK_FAILED);
259
+ return; // unreachable
260
+ }
261
+ output(payload, raw);
262
+ }
263
+ module.exports = {
264
+ STORIES_FILE,
265
+ STORIES_HEADER,
266
+ STORYLESS_PREFIXES,
267
+ SYSTEM_ACTOR,
268
+ needsStory,
269
+ STORIES_CHECK_FAILED: ERROR_REASON.DISCOVERY_STORIES_CHECK_FAILED,
270
+ checkStories,
271
+ checkProjectStories,
272
+ cmdDiscoveryStoriesCheck,
273
+ };
@@ -40,7 +40,7 @@ const VOID_TAGS = new Set([
40
40
  const RAW_TEXT_TAGS = new Set(['script', 'style', 'noscript']);
41
41
  const ID_RE = /^[A-Za-z][A-Za-z0-9_:.-]{0,79}$/;
42
42
  const REQ_ID_RE = /^[A-Z][A-Z0-9]*-\d+$/;
43
- const DECISION_RE = /^D-\d{2}-\d+$/;
43
+ const DECISION_RE = /^D-(?:\d{2}|P\d{2})-\d+$/;
44
44
  const ELEMENT_RE = /^(\*|[A-Za-z0-9_.-]+\.html?)#([A-Za-z][A-Za-z0-9_:.-]{0,79})$/;
45
45
  const ROUND_RE = /^## Sweep (\d{1,2}) — \d{4}-\d{2}-\d{2} — .+$/;
46
46
  const TABLE_HEADER = ['Element', 'Outcome', 'Reference'];
@@ -90,9 +90,64 @@ function boundedSequenceMax(seqs) {
90
90
  const max = seqs.reduce((a, b) => (b > a ? b : a), 0);
91
91
  return max > MAX_SEQUENCE_SPAN ? null : max;
92
92
  }
93
+ /**
94
+ * Every pipe table in a run of numbered lines (Discovery Sprint improvement
95
+ * plan, Batch 2). The header is the row before the first separator; data rows
96
+ * follow until the first line that is not a pipe row; a second separator
97
+ * starts the next table. Cells go through `splitTableCells`, so an escaped pipe
98
+ * inside a free-text cell never shifts a column.
99
+ */
100
+ function parseTables(body) {
101
+ const tables = [];
102
+ for (let i = 1; i < body.length; i += 1) {
103
+ if (!SEPARATOR_ROW_RE.test(body[i].text) || !body[i - 1].text.trim().startsWith('|'))
104
+ continue;
105
+ const table = { line: body[i - 1].n, header: splitTableCells(body[i - 1].text), rows: [] };
106
+ let j = i + 1;
107
+ for (; j < body.length; j += 1) {
108
+ const text = body[j].text.trim();
109
+ if (!text.startsWith('|') || SEPARATOR_ROW_RE.test(text))
110
+ break;
111
+ table.rows.push({ line: body[j].n, cells: splitTableCells(text) });
112
+ }
113
+ tables.push(table);
114
+ i = j - 1;
115
+ }
116
+ return tables;
117
+ }
118
+ /**
119
+ * Split a file into the sections opened by headings of exactly `level` hashes
120
+ * (`## ` for 2). A section's body runs to the next heading of the same or a
121
+ * higher level; lower-level headings stay inside the body, so a caller can
122
+ * split a body again at the next level. Lines before the first heading belong
123
+ * to no section.
124
+ */
125
+ function splitSections(lines, level) {
126
+ const open = new RegExp(`^#{${level}}\\s+(.*)$`);
127
+ const close = new RegExp(`^#{1,${level}}\\s`);
128
+ const out = [];
129
+ let current = null;
130
+ for (let i = 0; i < lines.length; i += 1) {
131
+ const m = open.exec(lines[i]);
132
+ if (m) {
133
+ current = { heading: m[1].trim(), line: i + 1, body: [] };
134
+ out.push(current);
135
+ continue;
136
+ }
137
+ if (close.test(lines[i])) {
138
+ current = null;
139
+ continue;
140
+ }
141
+ if (current)
142
+ current.body.push({ n: i + 1, text: lines[i] });
143
+ }
144
+ return out;
145
+ }
93
146
  module.exports = {
94
147
  splitTableCells,
95
148
  SEPARATOR_ROW_RE,
96
149
  MAX_SEQUENCE_SPAN,
97
150
  boundedSequenceMax,
151
+ parseTables,
152
+ splitSections,
98
153
  };
@@ -32,6 +32,7 @@ const worktreeSafety = require("./worktree-safety.cjs");
32
32
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
33
33
  const planningWorkspace = require("./planning-workspace.cjs");
34
34
  const secrets_cjs_1 = require("./secrets.cjs");
35
+ const plan_sizing_cjs_1 = require("./plan-sizing.cjs");
35
36
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-scan.cjs is an export= CommonJS module
36
37
  const scanPhasePlans = require("./plan-scan.cjs");
37
38
  const state_document_cjs_1 = require("./state-document.cjs");
@@ -222,6 +223,49 @@ function resolvePhaseForInit(cwd, phase) {
222
223
  }
223
224
  return { phaseInfo, roadmapPhase };
224
225
  }
226
+ /**
227
+ * Phase 37 (D-15): the recorded verdict as init keys, plus the research flag folded
228
+ * through it. Only plan-phase ever computes a verdict; this reads the recorded one,
229
+ * else full. Any throw returns the full keys and the configured research value, so a
230
+ * broken read never lowers ceremony (T-37-08). The fold can only turn research off when
231
+ * config has it on, the verdict says there is no codebase to match and nothing forced
232
+ * the researcher; it never turns a config-off gate on (T-37-10).
233
+ */
234
+ function ceremonyInitKeys(cwd, phase, researchConfigured) {
235
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
236
+ const loadCeremony = () => require('./ceremony.cjs');
237
+ try {
238
+ const { effectiveVerdict, resolveGateConfig, deriveCeremonyKeys } = loadCeremony();
239
+ const { verdict } = effectiveVerdict(cwd, phase);
240
+ const gateConfig = resolveGateConfig(cwd);
241
+ const keys = deriveCeremonyKeys(verdict, gateConfig.code_review_depth);
242
+ const keepResearch = verdict.mode === 'full' ||
243
+ verdict.answers.codebase_to_match.value === true ||
244
+ verdict.gates['research']?.decision === 'run';
245
+ return {
246
+ keys,
247
+ research_enabled: researchConfigured === undefined ? researchConfigured : Boolean(researchConfigured) && keepResearch,
248
+ };
249
+ }
250
+ catch {
251
+ let depth = 'standard';
252
+ try {
253
+ depth = loadCeremony().resolveGateConfig(cwd).code_review_depth;
254
+ }
255
+ catch { /* keep standard */ }
256
+ return {
257
+ keys: {
258
+ ceremony_tier: 'heavy',
259
+ ceremony_mode: 'full',
260
+ plan_check_rounds: 3,
261
+ plan_check_mode: 'standard',
262
+ verifier_depth: 'full',
263
+ code_review_depth: depth === 'quick' || depth === 'deep' ? depth : 'standard',
264
+ },
265
+ research_enabled: researchConfigured,
266
+ };
267
+ }
268
+ }
225
269
  function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
226
270
  if (!phase) {
227
271
  error('phase required for init execute-phase');
@@ -235,6 +279,7 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
235
279
  : null;
236
280
  const phase_req_ids = reqExtracted && reqExtracted !== 'TBD' ? reqExtracted : null;
237
281
  const wf = (config.workflow ?? {});
282
+ const ceremony = ceremonyInitKeys(cwd, phase, undefined);
238
283
  const result = {
239
284
  executor_model: resolveModelInternal(cwd, 'pp-executor'),
240
285
  verifier_model: resolveModelInternal(cwd, 'pp-verifier'),
@@ -247,6 +292,7 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
247
292
  phase_branch_template: config.phase_branch_template,
248
293
  milestone_branch_template: config.milestone_branch_template,
249
294
  verifier_enabled: config.verifier,
295
+ ...ceremony.keys,
250
296
  phase_found: !!phaseInfo,
251
297
  phase_dir: phaseInfo?.['directory'] || null,
252
298
  phase_number: phaseInfo?.['phase_number'] || null,
@@ -331,13 +377,16 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
331
377
  assertValidGranularityOverride(granularityOverride, error);
332
378
  const granularity = resolveGranularityInternal(cwd, 'planning', granularityOverride || undefined);
333
379
  const wf = (config.workflow ?? {});
380
+ const ceremony = ceremonyInitKeys(cwd, phase, wf['research']);
334
381
  const result = {
335
382
  researcher_model: resolveModelInternal(cwd, 'pp-phase-researcher'),
336
383
  planner_model: resolveModelInternal(cwd, 'pp-planner'),
337
384
  checker_model: resolveModelInternal(cwd, 'pp-plan-checker'),
338
385
  tdd_mode: options['tdd'] || Boolean(wf['tdd_mode']) || false,
339
386
  granularity,
340
- research_enabled: wf['research'],
387
+ ...(0, plan_sizing_cjs_1.planBudget)(config.context_window),
388
+ ...ceremony.keys,
389
+ research_enabled: ceremony.research_enabled,
341
390
  plan_checker_enabled: config.plan_checker,
342
391
  nyquist_validation_enabled: wf['nyquist_validation'],
343
392
  commit_docs: config.commit_docs,
@@ -203,6 +203,30 @@ const ERROR_REASON = Object.freeze({
203
203
  DISCOVERY_REBASELINE_CHECK_FAILED: 'discovery_rebaseline_check_failed',
204
204
  // discovery vault context record (PBI 75336)
205
205
  DISCOVERY_VAULT_CHECK_FAILED: 'discovery_vault_check_failed',
206
+ // discovery project charter (Discovery Sprint improvement plan, Batch 1)
207
+ DISCOVERY_CHARTER_CHECK_FAILED: 'discovery_charter_check_failed',
208
+ DISCOVERY_CHARTER_UNSIGNED: 'discovery_charter_unsigned',
209
+ // discovery data model, processes, business rules (improvement plan, Batch 2)
210
+ DISCOVERY_ENTITY_CHECK_FAILED: 'discovery_entity_check_failed',
211
+ DISCOVERY_ENTITY_CHECK_INCOMPLETE: 'discovery_entity_check_incomplete',
212
+ DISCOVERY_PROCESS_CHECK_FAILED: 'discovery_process_check_failed',
213
+ DISCOVERY_PROCESS_STEPS_UNCITED: 'discovery_process_steps_uncited',
214
+ DISCOVERY_RULES_CHECK_FAILED: 'discovery_rules_check_failed',
215
+ DISCOVERY_STORIES_CHECK_FAILED: 'discovery_stories_check_failed',
216
+ DISCOVERY_PHASES_CHECK_FAILED: 'discovery_phases_check_failed',
217
+ DISCOVERY_FOR_PHASE_FAILED: 'discovery_for_phase_failed',
218
+ // discovery roles, integrations, migration, deviations (improvement plan, Batch 3)
219
+ DISCOVERY_ROLES_CHECK_FAILED: 'discovery_roles_check_failed',
220
+ DISCOVERY_INTEGRATIONS_CHECK_FAILED: 'discovery_integrations_check_failed',
221
+ DISCOVERY_INTEGRATIONS_UNCITED: 'discovery_integrations_uncited',
222
+ DISCOVERY_MIGRATION_CHECK_FAILED: 'discovery_migration_check_failed',
223
+ DISCOVERY_MIGRATION_UNCITED: 'discovery_migration_uncited',
224
+ // discovery session output gate (improvement plan, Batch 5)
225
+ DISCOVERY_SESSION_CHECK_FAILED: 'discovery_session_check_failed',
226
+ DISCOVERY_DEVIATIONS_CHECK_FAILED: 'discovery_deviations_check_failed',
227
+ DISCOVERY_DEVIATIONS_UNAPPROVED: 'discovery_deviations_unapproved',
228
+ // timeline (Phase 35: end with no matching open run)
229
+ TIMELINE_NO_OPEN_RUN: 'timeline_no_open_run',
206
230
  // generic
207
231
  USAGE: 'usage',
208
232
  UNKNOWN: 'unknown',
@@ -40,7 +40,7 @@ const { resolveCapabilityRuntimeState, SEAL_FIELD, sealRenderVerdict, resolveInj
40
40
  // ─── Capability-activation engine (single owner for config-key precedence) ────
41
41
  // eslint-disable-next-line @typescript-eslint/no-require-imports
42
42
  const capabilityActivationModule = require("./capability-activation.cjs");
43
- const { _getNestedConfigValue, _readRawConfigKey, _resolveActivationValue, resolveConfigKey } = capabilityActivationModule;
43
+ const { _getNestedConfigValue, _readRawConfigKey, _resolveActivationValue, resolveConfigKey, evaluateWhen } = capabilityActivationModule;
44
44
  // ─── Canonical points (derived from LOOP_HOST_CONTRACT — authoritative 12) ───
45
45
  // FIX 2: Derive the authoritative canonical set from LOOP_HOST_CONTRACT so it
46
46
  // cannot drift from the host contract. CANONICAL_POINTS_FALLBACK is kept as an
@@ -99,11 +99,14 @@ function _getCanonicalPoints(_registry) {
99
99
  * Ordering: steps first, then contributions, then gates. Within each array,
100
100
  * the materialized registry order is preserved.
101
101
  *
102
- * Activation: a hook with no `when` is always active. With `when` (dotted key),
103
- * resolved against `config`; active iff truthy. Inactive hooks are filtered out.
102
+ * Activation: a hook with no `when` is always active. With `when` (a dotted key,
103
+ * or dotted keys joined by `&&`), resolved against `config` by evaluateWhen in
104
+ * capability-activation; active iff every conjunct holds. A `ceremony.<gate>`
105
+ * conjunct reads the phase verdict through the `ceremony` thunk and is true
106
+ * without one. Inactive hooks are filtered out.
104
107
  */
105
108
  function resolveLoopHooks(input) {
106
- const { point, registry, config, cwd, capabilityStatesById, recordedArtifacts, platformBindingsPresent, } = input;
109
+ const { point, registry, config, cwd, capabilityStatesById, recordedArtifacts, platformBindingsPresent, ceremony, } = input;
107
110
  // Validate point
108
111
  const canonicalPoints = _getCanonicalPoints(registry);
109
112
  if (!canonicalPoints.includes(point)) {
@@ -124,14 +127,10 @@ function resolveLoopHooks(input) {
124
127
  const activeHooks = [];
125
128
  // Helper: check activation using single-key precedence resolver (FIX 1 + FIX 3)
126
129
  function isActive(hook) {
127
- const when = hook['when'];
128
- // No `when` → unconditional hook, always active
129
- if (when === undefined || when === null)
130
- return true;
131
- // FIX 3: `when` present but not a non-empty string → malformed registry data → INACTIVE
132
- if (typeof when !== 'string' || when.length === 0)
133
- return false;
134
- return _resolveActivationValue(when, config, cwd, registry);
130
+ // One evaluator owns the `when` rules (no `when` -> active; malformed -> inactive;
131
+ // `&&` conjuncts; `ceremony.<gate>` through the injected thunk), shared with
132
+ // capability-state so the two cannot drift (Phase 37 D-14).
133
+ return evaluateWhen(hook['when'], { config, cwd, registry, ceremony });
135
134
  }
136
135
  function isCapabilityActive(capId) {
137
136
  if (!capabilityStatesById)
@@ -487,6 +486,11 @@ function renderLoopHooks(resolved) {
487
486
  * seal gate render (Phase 16 D-19). `--active-cap <cap>` also requires the
488
487
  * capability's own state to be active (D-27).
489
488
  *
489
+ * options.phase + options.resolveCeremonyGate: an injected
490
+ * `(phase) => (gate) => boolean`; with a non-empty `--phase` a `ceremony.<gate>`
491
+ * conjunct of a hook's `when` answers from that phase's recorded verdict. Without
492
+ * both, or under `--active-cap`, every such conjunct is true (Phase 37 D-14).
493
+ *
490
494
  * options.resolvePlatformBindingsPresent: an injected `() => boolean` (pp-tools
491
495
  * builds it on the vault leaf's hasPlatformBindings); only literal `false`
492
496
  * drops a `requiresPlatformBindings` capability's hooks (QD-13). `--into` and
@@ -544,6 +548,15 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
544
548
  // (pp-tools passes one built on the vault leaf's hasPlatformBindings).
545
549
  const resolvePlatformBindingsPresent = options['resolvePlatformBindingsPresent'];
546
550
  const platformBindingsPresent = () => resolveInjectedPlatformBindingsPresent(resolvePlatformBindingsPresent);
551
+ // Phase 37 D-14: the ceremony verdict arrives the same way. Only a non-empty
552
+ // `--phase` plus an injected resolver builds the thunk (once, so the verdict
553
+ // is read once per run); anything else passes none and every `ceremony.*`
554
+ // conjunct stays true. `--active-cap` reads the unfiltered resolution.
555
+ const ceremonyPhase = typeof options['phase'] === 'string' ? options['phase'] : '';
556
+ const resolveCeremonyGate = options['resolveCeremonyGate'];
557
+ const ceremonyThunk = activeCapId === undefined && ceremonyPhase.length > 0 && typeof resolveCeremonyGate === 'function'
558
+ ? resolveCeremonyGate(ceremonyPhase)
559
+ : undefined;
547
560
  let resolved;
548
561
  try {
549
562
  // --active-cap reads the UNFILTERED resolution (16-16 D-81, see above):
@@ -551,6 +564,7 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
551
564
  resolved = resolveLoopHooks({
552
565
  point, registry, config, cwd, capabilityStatesById, recordedArtifacts,
553
566
  ...(activeCapId === undefined ? { platformBindingsPresent } : {}),
567
+ ...(ceremonyThunk !== undefined ? { ceremony: ceremonyThunk } : {}),
554
568
  });
555
569
  }
556
570
  catch (err) {
@@ -347,6 +347,43 @@ function extractObjective(content) {
347
347
  const m = content.match(/<objective>\s*\n?\s*(.+)/);
348
348
  return m ? m[1].trim() : null;
349
349
  }
350
+ // Tasks in a PLAN.md body: `<task>` elements (case-insensitive; the `<tasks>` wrapper
351
+ // does not match), else `## Task N` headings for markdown-format plans. A task counts
352
+ // only where it really is one: the opening tag (or heading) starts a line and the tag
353
+ // has a closing `</task>`, so a `<task>` mentioned in prose or inline code is not a
354
+ // task. Shared with `check plan-sizing`, where the count feeds `over_ceiling`.
355
+ //
356
+ // One pass (36-W-1b): the earlier single regex `^[ \t]*<task(?=[\s>])[^>]*>[\s\S]*?</task>`
357
+ // rescanned to the end of the file for every unclosed open line, which is quadratic. Here
358
+ // the open tag is found, its `>` is the first one after it (what `[^>]*>` always ended
359
+ // at), and the close is searched from there (what the lazy `[\s\S]*?` ended at). When
360
+ // either search fails the loop stops: any later line-start open tag begins after this
361
+ // one, so its first `>` and its close search start no earlier and fail too. After a
362
+ // match the open search resumes where the old global match resumed, at the end of the
363
+ // close tag. The content is not lowercased: `toLowerCase` maps U+212A to `k` and
364
+ // changes the length of U+0130, which the regex `i` flag (non-unicode) does not do.
365
+ function countPlanTasks(content) {
366
+ const open = /^[ \t]*<task(?=[\s>])/gim;
367
+ const close = /<\/task>/gi;
368
+ let xmlTasks = 0;
369
+ for (;;) {
370
+ const found = open.exec(content);
371
+ if (found === null)
372
+ break;
373
+ const gt = content.indexOf('>', open.lastIndex);
374
+ if (gt < 0)
375
+ break;
376
+ close.lastIndex = gt + 1;
377
+ const closed = close.exec(content);
378
+ if (closed === null)
379
+ break;
380
+ xmlTasks++;
381
+ open.lastIndex = close.lastIndex;
382
+ }
383
+ if (xmlTasks > 0)
384
+ return xmlTasks;
385
+ return (content.match(/^[ \t]*#{2,}\s*Task\s*\d+/gim) || []).length;
386
+ }
350
387
  // O(V + E). Assigns each in-phase plan its longest-path topological level over the
351
388
  // in-phase dependsOn DAG (Kahn's algorithm). Returns { level: Map<id,number>, visited: number }.
352
389
  // visited < rawPlans.length signals a dependency cycle.
@@ -444,9 +481,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
444
481
  const planPath = node_path_1.default.join(phaseDir, planFile);
445
482
  const content = node_fs_1.default.readFileSync(planPath, 'utf-8');
446
483
  const fm = extractFrontmatter(content);
447
- const xmlTasks = content.match(/<task[\s>]/gi) || [];
448
- const mdTasks = content.match(/##\s*Task\s*\d+/gi) || [];
449
- const taskCount = xmlTasks.length || mdTasks.length;
484
+ const taskCount = countPlanTasks(content);
450
485
  const parsedWave = parseInt(fm['wave'], 10);
451
486
  const declaredWave = Number.isNaN(parsedWave) ? null : parsedWave;
452
487
  let dependsOn = [];
@@ -1477,4 +1512,5 @@ module.exports = {
1477
1512
  cmdPhaseComplete,
1478
1513
  cmdPhaseUatPassed,
1479
1514
  computeDependencyLevels,
1515
+ countPlanTasks,
1480
1516
  };