@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.
- package/README.md +7 -4
- package/dist/config/config.d.ts +61 -18
- package/dist/config/config.js +50 -6
- package/dist/config/register.js +18 -5
- package/dist/shared/content-tokens.d.ts +16 -0
- package/dist/shared/content-tokens.js +137 -0
- package/dist/task/accept-debt.d.ts +15 -1
- package/dist/task/accept-debt.js +4 -0
- package/dist/task/artifact-closure.js +7 -1
- package/dist/task/auto-io.d.ts +59 -7
- package/dist/task/auto-io.js +140 -47
- package/dist/task/auto-orchestrator.d.ts +22 -19
- package/dist/task/auto-orchestrator.js +126 -112
- package/dist/task/child-runner.d.ts +18 -0
- package/dist/task/child-runner.js +7 -8
- package/dist/task/command-run.d.ts +4 -0
- package/dist/task/command-run.js +4 -2
- package/dist/task/constraint-policy.d.ts +52 -0
- package/dist/task/constraint-policy.js +66 -0
- package/dist/task/context-silence.d.ts +5 -4
- package/dist/task/context-silence.js +13 -5
- package/dist/task/coverage-loop.d.ts +7 -4
- package/dist/task/coverage-loop.js +16 -138
- package/dist/task/debug-log.d.ts +25 -2
- package/dist/task/debug-log.js +64 -10
- package/dist/task/decompose-fidelity.d.ts +17 -4
- package/dist/task/decompose-fidelity.js +95 -55
- package/dist/task/decompose-granularity.d.ts +7 -7
- package/dist/task/decompose-granularity.js +8 -8
- package/dist/task/env-notes.d.ts +65 -19
- package/dist/task/env-notes.js +152 -50
- package/dist/task/external-context.js +25 -5
- package/dist/task/fix-context.d.ts +39 -0
- package/dist/task/fix-context.js +58 -0
- package/dist/task/gate-deps.d.ts +49 -0
- package/dist/task/gate-deps.js +199 -38
- package/dist/task/gate-evidence.d.ts +51 -0
- package/dist/task/gate-evidence.js +90 -0
- package/dist/task/gate-resolution.d.ts +119 -0
- package/dist/task/gate-resolution.js +120 -0
- package/dist/task/git-state-guard.js +3 -26
- package/dist/task/handoff.d.ts +35 -0
- package/dist/task/handoff.js +100 -0
- package/dist/task/health-baseline.d.ts +87 -0
- package/dist/task/health-baseline.js +158 -0
- package/dist/task/ledger.d.ts +10 -0
- package/dist/task/ledger.js +22 -11
- package/dist/task/lint-fix.d.ts +11 -0
- package/dist/task/lint-fix.js +14 -8
- package/dist/task/loop-detector.d.ts +52 -3
- package/dist/task/loop-detector.js +81 -5
- package/dist/task/mentions.d.ts +17 -0
- package/dist/task/mentions.js +86 -0
- package/dist/task/orchestrator.d.ts +53 -9
- package/dist/task/orchestrator.js +104 -28
- package/dist/task/orientation.d.ts +135 -29
- package/dist/task/orientation.js +259 -67
- package/dist/task/owned-freeze-reassign.d.ts +7 -3
- package/dist/task/owned-freeze-reassign.js +13 -5
- package/dist/task/parsers.d.ts +6 -1
- package/dist/task/parsers.js +21 -4
- package/dist/task/phases.d.ts +1 -1
- package/dist/task/phases.js +125 -153
- package/dist/task/plan-orchestrator.js +5 -6
- package/dist/task/plan-rounds.d.ts +1 -0
- package/dist/task/plan-rounds.js +8 -1
- package/dist/task/prohibition-probe.d.ts +6 -1
- package/dist/task/prohibition-probe.js +26 -6
- package/dist/task/prompts.js +13 -2
- package/dist/task/qa-transcript.d.ts +17 -0
- package/dist/task/qa-transcript.js +28 -0
- package/dist/task/question-source.d.ts +16 -2
- package/dist/task/question-source.js +21 -8
- package/dist/task/repo-health-check.d.ts +27 -0
- package/dist/task/repo-health-check.js +39 -5
- package/dist/task/requirements.d.ts +67 -13
- package/dist/task/requirements.js +129 -67
- package/dist/task/research-worker.d.ts +43 -2
- package/dist/task/research-worker.js +103 -8
- package/dist/task/run-bracket.d.ts +9 -4
- package/dist/task/run-bracket.js +9 -0
- package/dist/task/run-context.d.ts +152 -0
- package/dist/task/run-context.js +277 -0
- package/dist/task/spec-doc.d.ts +78 -0
- package/dist/task/spec-doc.js +205 -0
- package/dist/task/spec-model.d.ts +70 -0
- package/dist/task/spec-model.js +139 -0
- package/dist/task/stall-detector.d.ts +1 -1
- package/dist/task/stall-detector.js +9 -5
- package/dist/task/state-dir.d.ts +29 -0
- package/dist/task/state-dir.js +80 -0
- package/dist/task/suppression-probe.d.ts +67 -0
- package/dist/task/suppression-probe.js +78 -0
- package/dist/task/task-gates.d.ts +39 -57
- package/dist/task/task-gates.js +115 -195
- package/dist/task/task-io.d.ts +10 -0
- package/dist/task/task-io.js +69 -18
- package/dist/task/task-parsers.js +6 -6
- package/dist/task/task-types.d.ts +7 -0
- package/dist/task/timings.d.ts +11 -0
- package/dist/task/timings.js +21 -0
- package/dist/task/tree-hash.d.ts +8 -0
- package/dist/task/tree-hash.js +55 -0
- package/dist/task/verify-reconcile.d.ts +4 -3
- package/dist/task/verify-reconcile.js +7 -8
- package/dist/task/verify-resolution.d.ts +28 -5
- package/dist/task/verify-resolution.js +70 -13
- package/dist/task/verify-work.d.ts +98 -35
- package/dist/task/verify-work.js +227 -60
- package/dist/task/yolo.d.ts +0 -13
- package/dist/task/yolo.js +9 -21
- package/dist/workers/pi-worker-core.d.ts +20 -0
- package/dist/workers/pi-worker-core.js +16 -11
- package/dist/workers/pi-worker-docs.d.ts +6 -5
- package/dist/workers/pi-worker-docs.js +8 -7
- package/dist/workers/pi-worker-fetch.d.ts +3 -2
- package/dist/workers/pi-worker-fetch.js +5 -4
- package/dist/workers/pi-worker-search.js +5 -4
- package/dist/workers/research-cache.d.ts +23 -1
- package/dist/workers/research-cache.js +63 -3
- package/dist/workers/shared.d.ts +18 -0
- package/dist/workers/shared.js +38 -22
- package/package.json +1 -1
package/dist/task/orientation.js
CHANGED
|
@@ -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" (
|
|
4
|
-
* schema, entrypoints, API surface). The four
|
|
5
|
-
* processes with no shared memory, so without
|
|
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
|
|
11
|
-
* the
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
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
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (
|
|
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
|
-
|
|
250
|
+
const rule = ORIENTATION_RULES.find(r => r.match(candidate));
|
|
251
|
+
if (!rule)
|
|
105
252
|
return null;
|
|
106
|
-
if (
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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 => ({
|
|
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.
|
|
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
|
|
153
|
-
* skip the rest. Returns an empty block (and empty set) when nothing
|
|
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
|
-
|
|
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
|
-
|
|
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') >
|
|
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 (
|
|
183
|
-
|
|
184
|
-
|
|
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; `
|
|
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
|
|
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
|
|
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; `
|
|
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 {
|
|
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 =
|
|
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
|
-
&&
|
|
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
|
package/dist/task/parsers.d.ts
CHANGED
|
@@ -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:
|
|
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;
|
package/dist/task/parsers.js
CHANGED
|
@@ -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
|
|
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 === '
|
|
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
|
}
|
package/dist/task/phases.d.ts
CHANGED
|
@@ -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
|
/**
|