@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,346 @@
1
+ "use strict";
2
+ /**
3
+ * Plan Sizing: the per-plan budget and the plan-split check (Phase 36).
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/plan-sizing.cjs`; do NOT
7
+ * hand-edit the compiled output.
8
+ *
9
+ * Two consumers share these numbers so the planner and the plan checker cannot
10
+ * disagree about them:
11
+ * - `init plan-phase` spreads `planBudget(config.context_window)` into its JSON
12
+ * (D-03): the planner reads the window and the per-plan budget there.
13
+ * - `check plan-sizing <phase>` reads the phase's PLAN.md files and reports
14
+ * plans over the 6-task ceiling, plans over the budget, and serial splits
15
+ * that have neither a parallelism nor a budget reason (D-05, D-14). Executed
16
+ * plans are reported but never block.
17
+ *
18
+ * A larger window raises only the per-plan budget (50% of the window). The task
19
+ * band (20000-60000 tokens, the 10-30% of the 200k calibration) and the 6-task
20
+ * ceiling stay fixed (D-04). The check is read-only: it never writes.
21
+ */
22
+ var __importDefault = (this && this.__importDefault) || function (mod) {
23
+ return (mod && mod.__esModule) ? mod : { "default": mod };
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.MIN_TOKENS_PER_FILE = exports.TASK_TOKENS_MAX = exports.TASK_TOKENS_MIN = exports.MAX_TASKS_PER_PLAN = exports.DEFAULT_CONTEXT_WINDOW = void 0;
27
+ exports.resolveContextWindow = resolveContextWindow;
28
+ exports.planBudget = planBudget;
29
+ exports.parseEstimate = parseEstimate;
30
+ exports.analyzePlanSizing = analyzePlanSizing;
31
+ exports.readPhasePlans = readPhasePlans;
32
+ exports.computePlanSizing = computePlanSizing;
33
+ const node_fs_1 = __importDefault(require("node:fs"));
34
+ const node_path_1 = __importDefault(require("node:path"));
35
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
36
+ const phaseMod = require("./phase.cjs");
37
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
38
+ const coreUtilsMod = require("./core-utils.cjs");
39
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
40
+ const frontmatterMod = require("./frontmatter.cjs");
41
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
42
+ const configLoaderMod = require("./config-loader.cjs");
43
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
44
+ const phaseScopeMod = require("./phase-scope.cjs");
45
+ const { computeDependencyLevels, countPlanTasks } = phaseMod;
46
+ const { extractCanonicalPlanId } = coreUtilsMod;
47
+ const { extractFrontmatter } = frontmatterMod;
48
+ const { loadConfig } = configLoaderMod;
49
+ const { resolvePhaseForRead } = phaseScopeMod;
50
+ exports.DEFAULT_CONTEXT_WINDOW = 200000;
51
+ exports.MAX_TASKS_PER_PLAN = 6;
52
+ /** 10% of the 200k calibration (D-04); stated in tokens so a 1M window does not inflate tasks. */
53
+ exports.TASK_TOKENS_MIN = 20000;
54
+ /** 30% of the 200k calibration (D-04). */
55
+ exports.TASK_TOKENS_MAX = 60000;
56
+ /** Cross-check floor for the estimate against the number of files a plan touches (D-10). */
57
+ exports.MIN_TOKENS_PER_FILE = 5000;
58
+ /** A positive integer window; anything else (a bad config value) falls back to the default. */
59
+ function resolveContextWindow(raw) {
60
+ const n = typeof raw === 'string' && raw.trim() !== '' ? Number(raw) : raw;
61
+ return typeof n === 'number' && Number.isInteger(n) && n > 0 ? n : exports.DEFAULT_CONTEXT_WINDOW;
62
+ }
63
+ /** The flat budget object: one place for the numbers (D-03, D-04). */
64
+ function planBudget(raw) {
65
+ const context_window = resolveContextWindow(raw);
66
+ return {
67
+ context_window,
68
+ plan_budget_tokens: Math.floor(context_window / 2),
69
+ task_tokens_min: exports.TASK_TOKENS_MIN,
70
+ task_tokens_max: exports.TASK_TOKENS_MAX,
71
+ max_tasks_per_plan: exports.MAX_TASKS_PER_PLAN,
72
+ };
73
+ }
74
+ /**
75
+ * Read `context_estimate`. The frontmatter parser keeps an inline comment
76
+ * (`90000 # tokens` parses to that whole string), so a trailing `# comment` is
77
+ * allowed. Accepted: digits (optionally grouped in threes by `,` or `_`) and an
78
+ * optional `k` multiplier (`60k` is 60000; a decimal only with `k`, `1.5k`).
79
+ * Anything else - a range, an exponent, a `~` prefix, a unit - is null (treated
80
+ * as missing) rather than its leading digits; this never throws. readPhasePlans
81
+ * keeps the rejected text so the report can name it (`unparseable_estimate`).
82
+ * The comment is cut at the first `#` and the text trimmed before the anchored
83
+ * match, so one whitespace quantifier is left (36-W-1a: adjacent whitespace
84
+ * quantifiers were quadratic on a long run of spaces).
85
+ */
86
+ function parseEstimate(raw) {
87
+ if (typeof raw === 'number') {
88
+ return Number.isSafeInteger(raw) && raw >= 0 ? raw : null;
89
+ }
90
+ if (typeof raw !== 'string')
91
+ return null;
92
+ // The old comment group was `#` then `.*` to the end, and nothing else in the pattern
93
+ // matches `#`, so a comment always starts at the first `#`. `.` refuses a line
94
+ // terminator, so a comment that runs across one made the whole value invalid.
95
+ let text = raw;
96
+ const hash = raw.indexOf('#');
97
+ if (hash >= 0) {
98
+ if (/[\n\r\u2028\u2029]/.test(raw.slice(hash)))
99
+ return null;
100
+ text = raw.slice(0, hash);
101
+ }
102
+ const m = /^(\d+|\d{1,3}(?:[_,]\d{3})+)(?:\.(\d+))?\s*([kK])?$/.exec(text.trim());
103
+ if (!m)
104
+ return null;
105
+ const [, whole, fraction, kilo] = m;
106
+ if (fraction !== undefined && !kilo)
107
+ return null;
108
+ const base = Number(`${whole.replace(/[_,]/g, '')}.${fraction ?? '0'}`);
109
+ const n = kilo ? Math.round(base * 1000) : base;
110
+ return Number.isSafeInteger(n) ? n : null;
111
+ }
112
+ /** Analyse already-parsed plans against a budget. Pure: tests feed literals. */
113
+ function analyzePlanSizing(plans, budget) {
114
+ const { plan_budget_tokens: limit, max_tasks_per_plan: ceiling, context_window } = budget;
115
+ const rawPlans = plans.map((p) => ({
116
+ id: p.id,
117
+ declaredWave: null,
118
+ dependsOn: p.dependsOn,
119
+ autonomous: true,
120
+ objective: null,
121
+ filesModified: p.files,
122
+ taskCount: p.tasks,
123
+ hasSummary: p.hasSummary,
124
+ }));
125
+ const planMap = new Map(rawPlans.map((p) => [p.id.toLowerCase(), p]));
126
+ const canonicalToId = new Map(rawPlans.map((p) => [extractCanonicalPlanId(p.id).toLowerCase(), p.id]));
127
+ const { level, visited } = computeDependencyLevels(rawPlans, planMap, canonicalToId);
128
+ const cycle = visited < plans.length;
129
+ const waveOf = (id) => (level.get(id) ?? 0) + 1;
130
+ const entries = plans.map((p) => ({
131
+ id: p.id,
132
+ wave: cycle ? 0 : waveOf(p.id),
133
+ tasks: p.tasks,
134
+ files: p.files.length,
135
+ context_estimate: p.estimate,
136
+ percent_of_window: p.estimate === null ? null : Math.floor((p.estimate * 100) / context_window),
137
+ gap_closure: p.gapClosure,
138
+ has_summary: p.hasSummary,
139
+ }));
140
+ const over_ceiling = plans.filter((p) => p.tasks > ceiling).map((p) => p.id);
141
+ const over_budget = plans.filter((p) => p.estimate !== null && p.estimate > limit).map((p) => p.id);
142
+ // An executed plan (it has a SUMMARY) is reported but never blocks: the planner
143
+ // cannot split it, and a lowered context_window would otherwise block forever.
144
+ const over_blocking = plans.some((p) => !p.hasSummary && (over_ceiling.includes(p.id) || over_budget.includes(p.id)));
145
+ const unestimated = plans.filter((p) => p.estimate === null).map((p) => p.id);
146
+ // Present but rejected (`~35000`, `35000 tokens`): named with its value so the
147
+ // planner can tell it from a missing key; it still counts as unestimated.
148
+ const unparseable_estimate = plans.flatMap((p) => p.estimate === null && p.rawEstimate !== undefined ? [{ id: p.id, value: p.rawEstimate }] : []);
149
+ const estimate_files_mismatch = plans
150
+ .filter((p) => p.estimate !== null && p.estimate < p.files.length * exports.MIN_TOKENS_PER_FILE)
151
+ .map((p) => p.id);
152
+ const unjustified_splits = [];
153
+ const unestimated_serial = [];
154
+ let min_plans = plans.length;
155
+ if (!cycle) {
156
+ const byWave = new Map();
157
+ for (const p of plans) {
158
+ const w = waveOf(p.id);
159
+ const list = byWave.get(w);
160
+ if (list)
161
+ list.push(p);
162
+ else
163
+ byWave.set(w, [p]);
164
+ }
165
+ const resolveDeps = (p) => {
166
+ const out = new Set();
167
+ for (const dep of p.dependsOn) {
168
+ const lower = dep.toLowerCase();
169
+ const hit = planMap.get(lower)?.id ?? canonicalToId.get(lower);
170
+ if (hit)
171
+ out.add(hit);
172
+ }
173
+ return out;
174
+ };
175
+ // Serial link (a, b): a alone in wave w, b alone in wave w+1, b depends on a,
176
+ // neither a gap-closure plan, a `type: tdd` plan nor already executed.
177
+ const links = [];
178
+ for (const [w, list] of [...byWave.entries()].sort((x, y) => x[0] - y[0])) {
179
+ const next = byWave.get(w + 1);
180
+ if (list.length !== 1 || !next || next.length !== 1)
181
+ continue;
182
+ const a = list[0];
183
+ const b = next[0];
184
+ if (a.gapClosure || b.gapClosure || a.hasSummary || b.hasSummary || a.tdd || b.tdd)
185
+ continue;
186
+ if (!resolveDeps(b).has(a.id))
187
+ continue;
188
+ links.push({ a, b });
189
+ }
190
+ // A missing estimate is judged on task count only: a serial pair whose merge fits
191
+ // the ceiling has no visible budget reason, so it is reported (and blocks) rather
192
+ // than passing silently.
193
+ const chainNext = new Map();
194
+ const hasChainPrev = new Set();
195
+ for (const { a, b } of links) {
196
+ chainNext.set(a.id, b);
197
+ hasChainPrev.add(b.id);
198
+ const mergedTasks = a.tasks + b.tasks;
199
+ if (a.estimate === null || b.estimate === null) {
200
+ if (mergedTasks <= ceiling)
201
+ unestimated_serial.push({ plans: [a.id, b.id], merged_tasks: mergedTasks });
202
+ continue;
203
+ }
204
+ const mergedEstimate = a.estimate + b.estimate;
205
+ if (mergedTasks <= ceiling && mergedEstimate <= limit) {
206
+ unjustified_splits.push({ plans: [a.id, b.id], merged_tasks: mergedTasks, merged_estimate: mergedEstimate });
207
+ }
208
+ }
209
+ // min_plans: pack each maximal serial chain greedily in wave order (optimal for
210
+ // contiguous grouping; a missing estimate counts its tasks only); a plan in no
211
+ // chain is one plan.
212
+ let groups = 0;
213
+ const inChain = new Set();
214
+ for (const p of plans) {
215
+ if (hasChainPrev.has(p.id) || !chainNext.has(p.id))
216
+ continue;
217
+ let cur = p;
218
+ let tasks = 0;
219
+ let est = 0;
220
+ let open = false;
221
+ while (cur) {
222
+ inChain.add(cur.id);
223
+ const curEst = cur.estimate ?? 0;
224
+ if (open && (tasks + cur.tasks > ceiling || est + curEst > limit)) {
225
+ groups++;
226
+ tasks = 0;
227
+ est = 0;
228
+ }
229
+ tasks += cur.tasks;
230
+ est += curEst;
231
+ open = true;
232
+ cur = chainNext.get(cur.id);
233
+ }
234
+ if (open)
235
+ groups++;
236
+ }
237
+ min_plans = groups + plans.filter((p) => !inChain.has(p.id)).length;
238
+ }
239
+ return {
240
+ ...budget,
241
+ plans: entries,
242
+ over_ceiling,
243
+ over_budget,
244
+ unjustified_splits,
245
+ unestimated,
246
+ unparseable_estimate,
247
+ unestimated_serial,
248
+ estimate_files_mismatch,
249
+ dependency_cycle: cycle,
250
+ min_plans,
251
+ // Over the ceiling or the budget is the planner's split threshold (D-05), so both
252
+ // block, for plans that are not executed yet.
253
+ block: over_blocking || unjustified_splits.length > 0 || unestimated_serial.length > 0,
254
+ };
255
+ }
256
+ /**
257
+ * Tasks as `phase-plan-index` counts them (the shared `countPlanTasks`: `<task>`
258
+ * elements, else `## Task N` headings). A `type: tdd` plan holds one `<feature>`
259
+ * block instead (tdd.md), counted as one task.
260
+ */
261
+ const FEATURE_RE = /<feature[\s>]/gi;
262
+ function planTaskCount(content) {
263
+ return countPlanTasks(content) || (content.match(FEATURE_RE) || []).length;
264
+ }
265
+ /** The raw value of a present, non-empty frontmatter key, as text; else undefined. */
266
+ function rawValue(v) {
267
+ if (typeof v === 'number')
268
+ return String(v);
269
+ if (typeof v === 'string')
270
+ return v.trim() === '' ? undefined : v.trim();
271
+ if (Array.isArray(v))
272
+ return v.length === 0 ? undefined : JSON.stringify(v);
273
+ if (v !== null && typeof v === 'object')
274
+ return Object.keys(v).length === 0 ? undefined : JSON.stringify(v);
275
+ return undefined;
276
+ }
277
+ function asStringList(v) {
278
+ if (Array.isArray(v))
279
+ return v.map(String);
280
+ if (typeof v === 'string' && v.trim() !== '')
281
+ return [v];
282
+ return [];
283
+ }
284
+ /** Parse every `*-PLAN.md` in a phase directory. An unreadable directory yields no plans. */
285
+ function readPhasePlans(phaseDirAbs) {
286
+ let names;
287
+ try {
288
+ names = node_fs_1.default.readdirSync(phaseDirAbs);
289
+ }
290
+ catch {
291
+ return [];
292
+ }
293
+ const planFiles = names.filter((f) => f.endsWith('-PLAN.md') || f === 'PLAN.md').sort();
294
+ const completed = new Set();
295
+ for (const s of names) {
296
+ if (!(s.endsWith('-SUMMARY.md') || s === 'SUMMARY.md'))
297
+ continue;
298
+ const exact = s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
299
+ completed.add(exact);
300
+ completed.add(extractCanonicalPlanId(s));
301
+ }
302
+ const out = [];
303
+ for (const file of planFiles) {
304
+ let content;
305
+ try {
306
+ content = node_fs_1.default.readFileSync(node_path_1.default.join(phaseDirAbs, file), 'utf8');
307
+ }
308
+ catch {
309
+ continue;
310
+ }
311
+ const fm = extractFrontmatter(content);
312
+ const id = file.replace('-PLAN.md', '').replace('PLAN.md', '');
313
+ const estimate = parseEstimate(fm['context_estimate']);
314
+ const rawEstimate = estimate === null ? rawValue(fm['context_estimate']) : undefined;
315
+ out.push({
316
+ id,
317
+ dependsOn: asStringList(fm['depends_on']),
318
+ tasks: planTaskCount(content),
319
+ files: asStringList(fm['files_modified'] ?? fm['files-modified']),
320
+ estimate,
321
+ ...(rawEstimate === undefined ? {} : { rawEstimate }),
322
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string -- FrontmatterValue scalar compare
323
+ gapClosure: fm['gap_closure'] === 'true' || String(fm['gap_closure']) === 'true',
324
+ hasSummary: completed.has(id) || completed.has(extractCanonicalPlanId(file)),
325
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string -- FrontmatterValue scalar compare
326
+ tdd: String(fm['type'] ?? '').trim() === 'tdd',
327
+ });
328
+ }
329
+ return out;
330
+ }
331
+ /**
332
+ * The phase-scoped check: budget from config, plans from the resolved phase directory.
333
+ * An unknown phase blocks (`phase_found: false`, `block: true`) so a wrong phase
334
+ * argument cannot pass the gate; a resolver error propagates instead of reading as
335
+ * "not found".
336
+ */
337
+ function computePlanSizing(projectDir, phase) {
338
+ const budget = planBudget(loadConfig(projectDir).context_window);
339
+ const result = resolvePhaseForRead(projectDir, phase);
340
+ const relDir = result && typeof result['directory'] === 'string' ? result['directory'] : '';
341
+ if (!relDir) {
342
+ return { phase, phase_found: false, ...analyzePlanSizing([], budget), block: true };
343
+ }
344
+ const phaseDir = node_path_1.default.resolve(projectDir, relDir);
345
+ return { phase, phase_found: true, ...analyzePlanSizing(readPhasePlans(phaseDir), budget) };
346
+ }