@mjasnikovs/pi-task 0.40.50 → 0.41.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 (123) hide show
  1. package/README.md +7 -4
  2. package/dist/config/config.d.ts +61 -18
  3. package/dist/config/config.js +50 -6
  4. package/dist/config/register.js +18 -5
  5. package/dist/shared/content-tokens.d.ts +16 -0
  6. package/dist/shared/content-tokens.js +137 -0
  7. package/dist/task/accept-debt.d.ts +15 -1
  8. package/dist/task/accept-debt.js +4 -0
  9. package/dist/task/artifact-closure.js +7 -1
  10. package/dist/task/auto-io.d.ts +59 -7
  11. package/dist/task/auto-io.js +140 -47
  12. package/dist/task/auto-orchestrator.d.ts +22 -19
  13. package/dist/task/auto-orchestrator.js +126 -112
  14. package/dist/task/child-runner.d.ts +18 -0
  15. package/dist/task/child-runner.js +7 -8
  16. package/dist/task/command-run.d.ts +4 -0
  17. package/dist/task/command-run.js +4 -2
  18. package/dist/task/constraint-policy.d.ts +52 -0
  19. package/dist/task/constraint-policy.js +66 -0
  20. package/dist/task/context-silence.d.ts +5 -4
  21. package/dist/task/context-silence.js +13 -5
  22. package/dist/task/coverage-loop.d.ts +7 -4
  23. package/dist/task/coverage-loop.js +16 -138
  24. package/dist/task/debug-log.d.ts +25 -2
  25. package/dist/task/debug-log.js +64 -10
  26. package/dist/task/decompose-fidelity.d.ts +17 -4
  27. package/dist/task/decompose-fidelity.js +95 -55
  28. package/dist/task/decompose-granularity.d.ts +7 -7
  29. package/dist/task/decompose-granularity.js +8 -8
  30. package/dist/task/env-notes.d.ts +65 -19
  31. package/dist/task/env-notes.js +152 -50
  32. package/dist/task/external-context.js +25 -5
  33. package/dist/task/fix-context.d.ts +39 -0
  34. package/dist/task/fix-context.js +58 -0
  35. package/dist/task/gate-deps.d.ts +49 -0
  36. package/dist/task/gate-deps.js +199 -38
  37. package/dist/task/gate-evidence.d.ts +51 -0
  38. package/dist/task/gate-evidence.js +90 -0
  39. package/dist/task/gate-resolution.d.ts +119 -0
  40. package/dist/task/gate-resolution.js +120 -0
  41. package/dist/task/git-state-guard.js +3 -26
  42. package/dist/task/handoff.d.ts +35 -0
  43. package/dist/task/handoff.js +100 -0
  44. package/dist/task/health-baseline.d.ts +87 -0
  45. package/dist/task/health-baseline.js +158 -0
  46. package/dist/task/ledger.d.ts +10 -0
  47. package/dist/task/ledger.js +22 -11
  48. package/dist/task/lint-fix.d.ts +11 -0
  49. package/dist/task/lint-fix.js +14 -8
  50. package/dist/task/loop-detector.d.ts +52 -3
  51. package/dist/task/loop-detector.js +81 -5
  52. package/dist/task/mentions.d.ts +17 -0
  53. package/dist/task/mentions.js +86 -0
  54. package/dist/task/orchestrator.d.ts +53 -9
  55. package/dist/task/orchestrator.js +104 -28
  56. package/dist/task/orientation.d.ts +135 -29
  57. package/dist/task/orientation.js +259 -67
  58. package/dist/task/owned-freeze-reassign.d.ts +7 -3
  59. package/dist/task/owned-freeze-reassign.js +13 -5
  60. package/dist/task/parsers.d.ts +6 -1
  61. package/dist/task/parsers.js +21 -4
  62. package/dist/task/phases.d.ts +1 -1
  63. package/dist/task/phases.js +125 -153
  64. package/dist/task/plan-orchestrator.js +5 -6
  65. package/dist/task/plan-rounds.d.ts +1 -0
  66. package/dist/task/plan-rounds.js +8 -1
  67. package/dist/task/prohibition-probe.d.ts +6 -1
  68. package/dist/task/prohibition-probe.js +26 -6
  69. package/dist/task/prompts.js +13 -2
  70. package/dist/task/qa-transcript.d.ts +17 -0
  71. package/dist/task/qa-transcript.js +28 -0
  72. package/dist/task/question-source.d.ts +16 -2
  73. package/dist/task/question-source.js +21 -8
  74. package/dist/task/repo-health-check.d.ts +27 -0
  75. package/dist/task/repo-health-check.js +39 -5
  76. package/dist/task/requirements.d.ts +67 -13
  77. package/dist/task/requirements.js +129 -67
  78. package/dist/task/research-worker.d.ts +43 -2
  79. package/dist/task/research-worker.js +103 -8
  80. package/dist/task/run-bracket.d.ts +9 -4
  81. package/dist/task/run-bracket.js +9 -0
  82. package/dist/task/run-context.d.ts +152 -0
  83. package/dist/task/run-context.js +277 -0
  84. package/dist/task/spec-doc.d.ts +78 -0
  85. package/dist/task/spec-doc.js +205 -0
  86. package/dist/task/spec-model.d.ts +70 -0
  87. package/dist/task/spec-model.js +139 -0
  88. package/dist/task/stall-detector.d.ts +1 -1
  89. package/dist/task/stall-detector.js +9 -5
  90. package/dist/task/state-dir.d.ts +29 -0
  91. package/dist/task/state-dir.js +80 -0
  92. package/dist/task/suppression-probe.d.ts +67 -0
  93. package/dist/task/suppression-probe.js +78 -0
  94. package/dist/task/task-gates.d.ts +39 -57
  95. package/dist/task/task-gates.js +115 -195
  96. package/dist/task/task-io.d.ts +10 -0
  97. package/dist/task/task-io.js +69 -18
  98. package/dist/task/task-parsers.js +6 -6
  99. package/dist/task/task-types.d.ts +7 -0
  100. package/dist/task/timings.d.ts +11 -0
  101. package/dist/task/timings.js +21 -0
  102. package/dist/task/tree-hash.d.ts +8 -0
  103. package/dist/task/tree-hash.js +55 -0
  104. package/dist/task/verify-reconcile.d.ts +4 -3
  105. package/dist/task/verify-reconcile.js +7 -8
  106. package/dist/task/verify-resolution.d.ts +28 -5
  107. package/dist/task/verify-resolution.js +70 -13
  108. package/dist/task/verify-work.d.ts +98 -35
  109. package/dist/task/verify-work.js +227 -60
  110. package/dist/task/yolo.d.ts +0 -13
  111. package/dist/task/yolo.js +9 -21
  112. package/dist/workers/pi-worker-core.d.ts +20 -0
  113. package/dist/workers/pi-worker-core.js +16 -11
  114. package/dist/workers/pi-worker-docs.d.ts +6 -5
  115. package/dist/workers/pi-worker-docs.js +8 -7
  116. package/dist/workers/pi-worker-fetch.d.ts +3 -2
  117. package/dist/workers/pi-worker-fetch.js +5 -4
  118. package/dist/workers/pi-worker-search.js +5 -4
  119. package/dist/workers/research-cache.d.ts +23 -1
  120. package/dist/workers/research-cache.js +63 -3
  121. package/dist/workers/shared.d.ts +18 -0
  122. package/dist/workers/shared.js +38 -22
  123. package/package.json +1 -1
@@ -1,32 +1,48 @@
1
1
  /**
2
2
  * Project orientation core — a bounded snapshot of the few files a research worker
3
- * re-reads cold to learn "what is this project" (manifest, config, domain types,
4
- * schema, entrypoints, API surface). The four research workers are separate child
5
- * processes with no shared memory, so without this each one reads the same hot
6
- * files for itself.
3
+ * re-reads cold to learn "what is this project" (the doc the task cites, manifest,
4
+ * config, project rules, domain types, schema, entrypoints, API surface). The four
5
+ * research workers are separate child processes with no shared memory, so without
6
+ * this each one reads the same hot files for itself.
7
7
  *
8
8
  * This module picks that core from the file inventory (repo-agnostic, by path
9
9
  * convention) and reads it ONCE in the parent. The caller folds the block into a
10
- * prompt: refine gets a tier-0/1 subset, and phases.ts prepends the full block to
11
- * the two READ-HEAVY research workers (`worker:files`, `worker:apis`) — not to all
12
- * four. It is purely additive: nothing is blocked, so a worker can still read
13
- * anything it wants; orientation only removes the need to.
10
+ * prompt: refine gets the manifest/config tiers, and phases.ts prepends the full
11
+ * block to the three EXPLORING research workers — not to TOOLING. It is purely
12
+ * additive: nothing is blocked, so a worker can still read anything it wants;
13
+ * orientation only removes the need to.
14
+ *
15
+ * ELIGIBILITY IS A REGISTRY, not a ladder of regexes: {@link ORIENTATION_RULES} is
16
+ * an ordered list of `{id, tier, match}` rows over an {@link OrientationCandidate},
17
+ * so "does this file orient" and "how fundamental is it" are one row each, and the
18
+ * two facts a path cannot carry on its own — whether the task CITES it, and whether
19
+ * it is VENDORED — are fields on the candidate rather than more pattern-matching.
14
20
  *
15
21
  * Bounded by design — the snapshot can never overflow the prompt regardless of
16
- * repo size: a hard total byte budget, a per-file cap (one huge file can't eat
17
- * the budget), and a candidate cap. Files that don't fit are simply not
18
- * pre-supplied; the worker reads them as before. Selection is the pure, tested
19
- * core; reading takes an injectable reader so it can be exercised without a repo.
22
+ * repo size: a hard total byte budget, a per-file cap so one huge file can't eat
23
+ * it, a candidate cap, and a second budget of the same size that only cited
24
+ * documents may draw on. Files that don't fit are simply not pre-supplied; the worker reads
25
+ * them as before. Selection is the pure, tested core; reading takes an injectable
26
+ * reader so it can be exercised without a repo.
20
27
  */
21
28
  /**
22
29
  * Total bytes the emitted orientation block may occupy. This is the real overflow
23
30
  * guard: it bounds the block regardless of repo size, and because the snapshot is
24
- * prepended to each read-heavy worker it also caps the prefill those workers pay.
31
+ * prepended to each exploring worker it also caps the prefill those workers pay.
25
32
  * Selection stops as soon as adding a file would exceed it.
26
33
  */
27
34
  export const ORIENTATION_BYTE_BUDGET = 40 * 1024;
28
35
  /** A single file larger than this is skipped (read by the worker as before). */
29
36
  export const ORIENTATION_PER_FILE_MAX = 12 * 1024;
37
+ /**
38
+ * A SECOND purse, spent only on cited documents.
39
+ *
40
+ * A design doc is routinely most of the core budget on its own — the recorded mx5
41
+ * run's is 29 KB of 40 KB — so charging it to the shared purse buys the one file
42
+ * the task cites by dropping five files every task needs. Equal to the core
43
+ * budget, so the block can at most double and the bound stays a stated one.
44
+ */
45
+ export const ORIENTATION_CITED_BYTE_BUDGET = ORIENTATION_BYTE_BUDGET;
30
46
  /**
31
47
  * Backstop file-count cap — a guard against a pathological repo with hundreds of
32
48
  * tiny core files packing the byte budget into noise, NOT a normal-case limit. Set
@@ -34,6 +50,28 @@ export const ORIENTATION_PER_FILE_MAX = 12 * 1024;
34
50
  * selection, budget is left unspent and core files are dropped for no reason.
35
51
  */
36
52
  export const ORIENTATION_MAX_FILES = 40;
53
+ /**
54
+ * The priority ladder, lowest first. The tiers mirror the questions a worker
55
+ * re-derives on every task: what am I being asked about (the cited doc) → what is
56
+ * this project (manifest) → how is it built (config) → what rules does it set for
57
+ * itself (guidelines) → what is its domain model (types/schema) → where does it
58
+ * start (entrypoints) → what is its surface (api) → what does it say about itself
59
+ * (docs).
60
+ *
61
+ * Named rather than numbered at the call sites: refine asks for "manifest and
62
+ * config", not for "0 and 1", and a tier inserted in the middle must not silently
63
+ * change what refine gets.
64
+ */
65
+ export const ORIENTATION_TIERS = {
66
+ cited: 0,
67
+ manifest: 1,
68
+ config: 2,
69
+ guidelines: 3,
70
+ domain: 4,
71
+ entrypoint: 5,
72
+ api: 6,
73
+ docs: 7
74
+ };
37
75
  /** Code/config/doc extensions worth pre-reading; everything else is ignored. */
38
76
  const ORIENTATION_EXTENSIONS = new Set([
39
77
  'ts',
@@ -69,6 +107,28 @@ const MANIFEST_BASENAMES = new Set([
69
107
  'composer.json',
70
108
  'deno.json'
71
109
  ]);
110
+ /** The files a project states its own rules in. */
111
+ const GUIDELINE_BASENAMES = new Set(['agents.md', 'claude.md']);
112
+ /**
113
+ * Directories whose contents are never this project's own source: vendored
114
+ * dependencies, build output, and the agent's own scaffolding. A dependency's
115
+ * `package.json` is a manifest by basename and `.pi/skills/**` is a wall of
116
+ * markdown — either would outrank the project's real files and eat the budget.
117
+ *
118
+ * A registry rather than one regex so a project can be given more through
119
+ * `.gitignore` and `orientationExclude` (config.ts) without editing a pattern.
120
+ */
121
+ export const VENDORED_DIRS = [
122
+ '.git',
123
+ 'node_modules',
124
+ 'dist',
125
+ 'build',
126
+ 'out',
127
+ 'vendor',
128
+ 'coverage',
129
+ '.pi',
130
+ '.pi-tasks'
131
+ ];
72
132
  function basename(p) {
73
133
  const i = p.lastIndexOf('/');
74
134
  return i === -1 ? p : p.slice(i + 1);
@@ -86,51 +146,162 @@ function depth(p) {
86
146
  return n;
87
147
  }
88
148
  /**
89
- * Priority tier for an orientation candidate lower is more fundamental. The
90
- * tiers mirror the questions a worker re-derives on every task: what is this
91
- * project (manifest) → how is it built (config) → what is its domain model
92
- * (types/schema) where does it start (entrypoints) what is its surface (api)
93
- * what does it say about itself (docs). Returns null for paths that aren't
94
- * orientation material, so they're dropped entirely.
149
+ * Eligibility, in priority order FIRST MATCH WINS, and a candidate no row
150
+ * matches is not orientation material and is dropped entirely.
151
+ *
152
+ * The three rejecting rows come first and carry no tier of their own: a vendored
153
+ * file, a test and an absolute path are excluded whatever else they look like.
154
+ * They are rows rather than early returns so the whole policy is one list.
155
+ */
156
+ export const ORIENTATION_RULES = [
157
+ {
158
+ // The task's own spec document. Highest tier because it is the one file
159
+ // the worker is guaranteed to need and the one it cannot guess the path
160
+ // of — the mx5 run read it 50 times because orientation could not select
161
+ // a `DESIGN/*.md` by convention.
162
+ id: 'cited-spec-doc',
163
+ tier: ORIENTATION_TIERS.cited,
164
+ match: c => c.cited,
165
+ byName: true
166
+ },
167
+ {
168
+ id: 'manifest',
169
+ tier: ORIENTATION_TIERS.manifest,
170
+ match: c => MANIFEST_BASENAMES.has(basename(c.path).toLowerCase()),
171
+ byName: true
172
+ },
173
+ {
174
+ id: 'config',
175
+ tier: ORIENTATION_TIERS.config,
176
+ match: c => {
177
+ const base = basename(c.path).toLowerCase();
178
+ return (base === 'tsconfig.json'
179
+ || /\.config\.(ts|js|mjs|cjs|json)$/.test(base)
180
+ || extension(c.path) === 'toml');
181
+ }
182
+ },
183
+ {
184
+ id: 'guidelines',
185
+ tier: ORIENTATION_TIERS.guidelines,
186
+ match: c => GUIDELINE_BASENAMES.has(basename(c.path).toLowerCase()),
187
+ byName: true
188
+ },
189
+ {
190
+ id: 'domain',
191
+ tier: ORIENTATION_TIERS.domain,
192
+ match: c => {
193
+ const lower = c.path.toLowerCase();
194
+ const ext = extension(lower);
195
+ const base = basename(lower);
196
+ return (ext === 'sql'
197
+ || ext === 'prisma'
198
+ || ext === 'graphql'
199
+ || ext === 'gql'
200
+ || base.endsWith('.d.ts')
201
+ || lower.split('/').includes('types')
202
+ || base.replace(/\.[^.]+$/, '') === 'types'
203
+ || /schema|zod/.test(lower));
204
+ }
205
+ },
206
+ {
207
+ id: 'entrypoint',
208
+ tier: ORIENTATION_TIERS.entrypoint,
209
+ match: c => ['index', 'main', 'app', 'server', 'mod'].includes(basename(c.path)
210
+ .toLowerCase()
211
+ .replace(/\.[^.]+$/, ''))
212
+ },
213
+ {
214
+ id: 'api',
215
+ tier: ORIENTATION_TIERS.api,
216
+ match: c => {
217
+ const lower = c.path.toLowerCase();
218
+ return /(^|\/)api(\/|s?\.|$)/.test(lower) || lower.split('/').includes('api');
219
+ }
220
+ },
221
+ {
222
+ id: 'readme',
223
+ tier: ORIENTATION_TIERS.docs,
224
+ match: c => basename(c.path)
225
+ .toLowerCase()
226
+ .replace(/\.[^.]+$/, '') === 'readme'
227
+ }
228
+ ];
229
+ /** A path that no rule may promote, whatever it looks like. */
230
+ function excluded(c) {
231
+ if (c.vendored)
232
+ return true;
233
+ if (c.path.startsWith('/'))
234
+ return true; // absolute path escaped the repo root
235
+ const lower = c.path.toLowerCase();
236
+ return /\.(test|spec)\.[a-z]+$/.test(lower) || /(^|\/)(__tests__|e2e)\//.test(lower);
237
+ }
238
+ /**
239
+ * Priority tier for an orientation candidate — lower is more fundamental, null
240
+ * for a path that is not orientation material.
95
241
  */
96
- export function orientationTier(path) {
97
- const lower = path.toLowerCase();
98
- // Never orient on vendored/build output or tests: a dependency's package.json
99
- // is tier 0 by basename and a `*.test.ts` under `src/types/` is tier 2, so
100
- // either would outrank the project's own files and eat the budget. A
101
- // git-ls-files inventory won't list these anyway; this is belt-and-braces.
102
- if (/(^|\/)(node_modules|dist|build|out|vendor|\.git|coverage)\//.test(lower))
242
+ export function orientationTier(path, opts = {}) {
243
+ const candidate = {
244
+ path,
245
+ cited: opts.cited ?? false,
246
+ vendored: opts.vendored ?? isVendored(path)
247
+ };
248
+ if (excluded(candidate))
103
249
  return null;
104
- if (/\.(test|spec)\.[a-z]+$/.test(lower) || /(^|\/)(__tests__|e2e)\//.test(lower))
250
+ const rule = ORIENTATION_RULES.find(r => r.match(candidate));
251
+ if (!rule)
105
252
  return null;
106
- if (path.startsWith('/'))
107
- return null; // absolute path escaped the repo root
108
- const base = basename(lower);
109
- const ext = extension(lower);
110
- if (MANIFEST_BASENAMES.has(base))
111
- return 0;
112
- if (!ORIENTATION_EXTENSIONS.has(ext))
253
+ if (!rule.byName && !ORIENTATION_EXTENSIONS.has(extension(path)))
113
254
  return null;
114
- if (base === 'tsconfig.json' || /\.config\.(ts|js|mjs|cjs|json)$/.test(base))
115
- return 1;
116
- const segs = lower.split('/');
117
- const stem = base.replace(/\.[^.]+$/, '');
118
- if (ext === 'sql'
119
- || ext === 'prisma'
120
- || ext === 'graphql'
121
- || ext === 'gql'
122
- || base.endsWith('.d.ts')
123
- || segs.includes('types')
124
- || stem === 'types'
125
- || /schema|zod/.test(lower))
126
- return 2;
127
- if (['index', 'main', 'app', 'server', 'mod'].includes(stem))
128
- return 3;
129
- if (/(^|\/)api(\/|s?\.|$)/.test(lower) || segs.includes('api'))
130
- return 4;
131
- if (stem === 'readme')
132
- return 5;
133
- return null;
255
+ return rule.tier;
256
+ }
257
+ /**
258
+ * Is this path outside the project's own source? `VENDORED_DIRS` plus whatever
259
+ * `.gitignore` and `orientationExclude` add (see {@link parseIgnorePatterns}).
260
+ */
261
+ export function isVendored(path, patterns = []) {
262
+ const segments = path.split('/');
263
+ if (VENDORED_DIRS.some(d => segments.includes(d)))
264
+ return true;
265
+ return patterns.some(p => ignoreMatcher(p)(path));
266
+ }
267
+ /**
268
+ * The usable patterns in a `.gitignore`.
269
+ *
270
+ * Negations (`!kept.md`) are DROPPED rather than honoured: un-ignoring is a
271
+ * whole-file-order semantics this does not implement, and the inventory a real
272
+ * run orients over is `git ls-files`, which has already applied the real rules.
273
+ * What is left here only has to keep bulk out.
274
+ */
275
+ export function parseIgnorePatterns(gitignore) {
276
+ return gitignore
277
+ .split('\n')
278
+ .map(l => l.trim())
279
+ .filter(l => l.length > 0 && !l.startsWith('#') && !l.startsWith('!'));
280
+ }
281
+ const matchers = new Map();
282
+ /** A `.gitignore`-style pattern as a predicate, compiled once per pattern. */
283
+ function ignoreMatcher(pattern) {
284
+ const cached = matchers.get(pattern);
285
+ if (cached)
286
+ return cached;
287
+ const anchored = pattern.startsWith('/');
288
+ const body = pattern.replace(/^\//, '').replace(/\/$/, '');
289
+ const source = body
290
+ .split('/')
291
+ .map(seg => seg
292
+ .split('*')
293
+ .map(part => part.replace(/[.+^${}()|[\]\\?]/g, '\\$&'))
294
+ .join('[^/]*')
295
+ // `**` survives the split above as two `[^/]*` around an empty
296
+ // part; a directory wildcard has to cross separators.
297
+ .replace(/\[\^\/\]\*\[\^\/\]\*/g, '.*'))
298
+ .join('/');
299
+ // A pattern matches the path itself or anything under it, and — unless it is
300
+ // anchored to the repo root — at any depth, which is gitignore's own rule.
301
+ const re = new RegExp(`^${anchored ? '' : '(?:.*/)?'}${source}(?:/.*)?$`);
302
+ const fn = (path) => re.test(path);
303
+ matchers.set(pattern, fn);
304
+ return fn;
134
305
  }
135
306
  /**
136
307
  * Rank inventory paths into orientation priority order: by tier, then shallower
@@ -138,26 +309,39 @@ export function orientationTier(path) {
138
309
  * for a fully deterministic order. Non-orientation paths are dropped. The result
139
310
  * is the *candidate* order; the byte budget is applied later when reading.
140
311
  */
141
- export function selectOrientationFiles(inventoryPaths) {
312
+ export function selectOrientationFiles(inventoryPaths, opts = {}) {
313
+ const cited = new Set(opts.cited ?? []);
142
314
  return inventoryPaths
143
- .map(p => ({ p, tier: orientationTier(p) }))
315
+ .map(p => ({
316
+ path: p,
317
+ tier: orientationTier(p, {
318
+ cited: cited.has(p),
319
+ vendored: isVendored(p, opts.excludePatterns ?? [])
320
+ })
321
+ }))
144
322
  .filter((x) => x.tier !== null)
145
- .sort((a, b) => a.tier - b.tier || depth(a.p) - depth(b.p) || (a.p < b.p ? -1 : 1))
146
- .map(x => x.p);
323
+ .sort((a, b) => a.tier - b.tier || depth(a.path) - depth(b.path) || (a.path < b.path ? -1 : 1));
147
324
  }
148
325
  /**
149
326
  * Read the orientation core within the byte budget and format it as a header
150
327
  * block. `readFile` returns a file's text or null (missing/unreadable/binary) —
151
328
  * a null or over-cap file is skipped, not fatal. Greedy in priority order: take
152
- * each file whose content fits the per-file cap and the remaining total budget;
153
- * skip the rest. Returns an empty block (and empty set) when nothing qualifies,
154
- * so the caller falls back to today's behavior.
329
+ * each file whose content fits its tier's per-file cap and the remaining total
330
+ * budget; skip the rest. Returns an empty block (and empty set) when nothing
331
+ * qualifies, so the caller falls back to today's behavior.
155
332
  */
156
333
  export async function buildOrientation(inventoryPaths, readFile, opts = {}) {
157
334
  const byteBudget = opts.byteBudget ?? ORIENTATION_BYTE_BUDGET;
335
+ const citedBudget = opts.citedByteBudget ?? ORIENTATION_CITED_BYTE_BUDGET;
158
336
  const perFileMax = opts.perFileMax ?? ORIENTATION_PER_FILE_MAX;
159
337
  const maxFiles = opts.maxFiles ?? ORIENTATION_MAX_FILES;
160
- const candidates = selectOrientationFiles(inventoryPaths).slice(0, maxFiles * 4);
338
+ // The cited doc is the file the task is ABOUT, and a design document is
339
+ // routinely larger than the cap that keeps one incidental file from eating
340
+ // the budget. Skipping it is the failure this tier exists to fix, so it is
341
+ // bounded by its own purse instead.
342
+ const cited = (tier) => tier === ORIENTATION_TIERS.cited;
343
+ const perFileMaxFor = (tier) => (cited(tier) ? citedBudget : perFileMax);
344
+ const candidates = selectOrientationFiles(inventoryPaths, opts).slice(0, maxFiles * 4);
161
345
  const supplied = new Set();
162
346
  const parts = [];
163
347
  // Budget the *emitted* block, not just the raw content: the per-file fence
@@ -168,20 +352,28 @@ export async function buildOrientation(inventoryPaths, readFile, opts = {}) {
168
352
  + `already provided, do not re-read these)\n`;
169
353
  const TRAILER = '\n\n';
170
354
  let used = Buffer.byteLength(HEADER + TRAILER, 'utf8');
171
- for (const path of candidates) {
355
+ let usedCited = 0;
356
+ for (const { path, tier } of candidates) {
172
357
  if (supplied.size >= maxFiles)
173
358
  break;
174
359
  const text = await readFile(path);
175
360
  if (text === null)
176
361
  continue;
177
- if (Buffer.byteLength(text, 'utf8') > perFileMax)
362
+ if (Buffer.byteLength(text, 'utf8') > perFileMaxFor(tier))
178
363
  continue;
179
364
  const piece = `--- ${path} ---\n${text.replace(/\n+$/, '')}`;
180
365
  // Each piece after the first is preceded by the `\n\n` join separator.
181
366
  const pieceBytes = Buffer.byteLength(piece, 'utf8') + (parts.length > 0 ? 2 : 0);
182
- if (used + pieceBytes > byteBudget)
183
- continue;
184
- used += pieceBytes;
367
+ if (cited(tier)) {
368
+ if (usedCited + pieceBytes > citedBudget)
369
+ continue;
370
+ usedCited += pieceBytes;
371
+ }
372
+ else {
373
+ if (used + pieceBytes > byteBudget)
374
+ continue;
375
+ used += pieceBytes;
376
+ }
185
377
  supplied.add(path);
186
378
  parts.push(piece);
187
379
  }
@@ -15,7 +15,7 @@
15
15
  * `appendOwnedConstraints` stamps the bullet, which is the first moment
16
16
  * the pair exists): the entry is marked `pending: [frozen paths]` in the
17
17
  * ledger and its stamped bullet is dropped from this spec. It is now
18
- * owned by nobody; `ownedForTitle` skips it; the quote is still there,
18
+ * owned by nobody; `ownedForTask` skips it; the quote is still there,
19
19
  * byte for byte.
20
20
  * CLAIM at every LATER task's compose, before the belt block is built: if that
21
21
  * task's REFINED PROMPT shows a write intent on one of the pending
@@ -89,7 +89,9 @@ export declare function writeIntent(text: string, p: string): boolean;
89
89
  */
90
90
  export declare function detachUnsatisfiableRequirements(args: {
91
91
  spec: string;
92
- /** The executing task's plan title — the ledger's join key. */
92
+ /** The executing task's plan key — the ledger's join. */
93
+ key?: string;
94
+ /** The executing task's plan title — display, and the legacy join. */
93
95
  title: string;
94
96
  ledger: OwnedRequirement[];
95
97
  isSource?: OwnedFreezeOptions['isSource'];
@@ -110,7 +112,9 @@ export interface ClaimResult {
110
112
  export declare function claimPendingRequirements(args: {
111
113
  /** The claiming task's refined prompt — the text that says what it will write. */
112
114
  intent: string;
113
- /** The claiming task's plan title (the ledger's join key). */
115
+ /** The claiming task's plan key — what the ledger joins on afterwards. */
116
+ key?: string;
117
+ /** The claiming task's plan title. */
114
118
  title: string;
115
119
  ledger: OwnedRequirement[];
116
120
  }): ClaimResult;
@@ -15,7 +15,7 @@
15
15
  * `appendOwnedConstraints` stamps the bullet, which is the first moment
16
16
  * the pair exists): the entry is marked `pending: [frozen paths]` in the
17
17
  * ledger and its stamped bullet is dropped from this spec. It is now
18
- * owned by nobody; `ownedForTitle` skips it; the quote is still there,
18
+ * owned by nobody; `ownedForTask` skips it; the quote is still there,
19
19
  * byte for byte.
20
20
  * CLAIM at every LATER task's compose, before the belt block is built: if that
21
21
  * task's REFINED PROMPT shows a write intent on one of the pending
@@ -46,7 +46,7 @@
46
46
  * see rather than an obligation that quietly evaporated.
47
47
  */
48
48
  import { findOwnedFreezeConflicts } from './owned-freeze-conflict.js';
49
- import { ownedForTitle } from './requirements.js';
49
+ import { ownedForTask } from './requirements.js';
50
50
  import { PROHIBITION_RE } from './prohibition-probe.js';
51
51
  /**
52
52
  * Does this text CLAIM a write on `p` — a create/modify verb reaching a mention
@@ -101,7 +101,7 @@ function dropLine(spec, line) {
101
101
  * no quote leaves the ledger, no prose is touched.
102
102
  */
103
103
  export function detachUnsatisfiableRequirements(args) {
104
- const mine = ownedForTitle(args.ledger, args.title);
104
+ const mine = ownedForTask(args.ledger, args);
105
105
  const conflicts = findOwnedFreezeConflicts(args.spec, {
106
106
  owned: mine,
107
107
  isSource: args.isSource
@@ -111,10 +111,13 @@ export function detachUnsatisfiableRequirements(args) {
111
111
  spec: args.spec,
112
112
  actions: []
113
113
  };
114
+ // `mine` holds the pre-copy entries, so the copy is re-found by quote — the
115
+ // ledger's own key.
116
+ const ownedQuotes = new Set(mine.map(o => o.quote));
114
117
  for (const c of conflicts) {
115
118
  const entry = out.ledger.find(o => !(o.pending && o.pending.length > 0)
116
119
  && c.requirement.includes(o.quote)
117
- && normalise(o.title) === normalise(args.title));
120
+ && ownedQuotes.has(o.quote));
118
121
  if (!entry) {
119
122
  // The conflicting line is not one of THIS task's ledger quotes (a
120
123
  // stamp from an earlier plan round, or a quote the ledger no longer
@@ -166,6 +169,12 @@ export function claimPendingRequirements(args) {
166
169
  if (claimed.length === 0)
167
170
  continue;
168
171
  delete entry.pending;
172
+ // Both halves of the join move together — a claim that retitled without
173
+ // rekeying would leave the entry owned by the task that gave it up.
174
+ if (args.key === undefined)
175
+ delete entry.key;
176
+ else
177
+ entry.key = args.key;
169
178
  entry.title = args.title;
170
179
  out.actions.push({ kind: 'claim', quote: entry.quote, by: args.title, paths: claimed });
171
180
  }
@@ -177,7 +186,6 @@ export function claimPendingRequirements(args) {
177
186
  export function unclaimedPendingRequirements(ledger) {
178
187
  return ledger.filter(o => (o.pending ?? []).length > 0);
179
188
  }
180
- const normalise = (s) => s.trim().toLowerCase().replace(/\s+/g, ' ');
181
189
  /** One line per action, for the run's debug log. */
182
190
  export function formatReassignActions(actions) {
183
191
  return actions
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Pure functions that parse raw model output into structured data.
5
5
  */
6
+ import type { ToolingClass } from './run-context.js';
6
7
  /**
7
8
  * WHY an auto-answer came back `unknown` — the producers are otherwise
8
9
  * indistinguishable at the call site, and they are NOT equivalent: only
@@ -48,7 +49,11 @@ export declare function parseClarifyList(raw: string): ClarifyQuestion[];
48
49
  export declare function autoAnswerHasTag(raw: string): boolean;
49
50
  export declare function parseAutoAnswer(raw: string): AutoAnswer;
50
51
  export declare function parseVerifyToolingOutput(output: string): {
51
- verified: string[];
52
+ verified: Array<{
53
+ cmd: string;
54
+ class: ToolingClass;
55
+ evidence: string;
56
+ }>;
52
57
  rejected: Array<{
53
58
  cmd: string;
54
59
  reason: string;
@@ -164,6 +164,12 @@ export function parseAutoAnswer(raw) {
164
164
  return { kind: 'unknown', raw, reason: 'model-unknown' };
165
165
  }
166
166
  // ─── Verify tooling output parser ────────────────────────────────────────────
167
+ const TOOLING_CLASSES = ['check', 'build', 'serve'];
168
+ /** The class column of a VERIFIED line, or null when the child wrote something else. */
169
+ function toolingClass(field) {
170
+ const t = field.trim().toLowerCase();
171
+ return TOOLING_CLASSES.find(c => c === t) ?? null;
172
+ }
167
173
  export function parseVerifyToolingOutput(output) {
168
174
  const verified = [];
169
175
  const rejected = [];
@@ -180,15 +186,26 @@ export function parseVerifyToolingOutput(output) {
180
186
  }
181
187
  if (!line)
182
188
  continue;
183
- // Lines look like: " <cmd> <evidence/reason>"
189
+ // Lines look like: " <cmd> <class> <evidence>" (REJECTED has no class).
184
190
  const match = line.match(/^(\S.*?)\s{2,}(.+)$/);
185
191
  if (!match)
186
192
  continue;
187
193
  const [, cmd, detail] = match;
188
- if (section === 'verified')
189
- verified.push(cmd.trim());
190
- else if (section === 'rejected')
194
+ if (section === 'rejected') {
191
195
  rejected.push({ cmd: cmd.trim(), reason: detail.trim() });
196
+ continue;
197
+ }
198
+ if (section !== 'verified')
199
+ continue;
200
+ const columns = detail.match(/^(\S+)\s{2,}(.+)$/);
201
+ const cls = columns ? toolingClass(columns[1]) : null;
202
+ // A line with no class column is a check: the TOOLING list is verification
203
+ // commands, and the prompt requires a long-running one to say `serve`.
204
+ verified.push({
205
+ cmd: cmd.trim(),
206
+ class: cls ?? 'check',
207
+ evidence: (cls && columns ? columns[2] : detail).trim()
208
+ });
192
209
  }
193
210
  return { verified, rejected };
194
211
  }
@@ -170,7 +170,7 @@ export declare const SINGLE_READ_EXTENSION_PATH: string;
170
170
  * …) or end of text.
171
171
  */
172
172
  export declare function scopedToolingGoal(refined: string): string;
173
- export declare function phaseResearch(deps: PhaseDeps, refined: string): Promise<string>;
173
+ export declare function phaseResearch(deps: PhaseDeps, refined: string, rawPrompt?: string): Promise<string>;
174
174
  export declare function phaseAutoAnswer(deps: PhaseDeps, refined: string, research: string, question: string): Promise<AutoAnswer>;
175
175
  export declare function phaseGrill(deps: PhaseDeps, ctx: ExtensionCommandContext, widgetState: WidgetState, refined: string, research: string): Promise<string>;
176
176
  /**