@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/env-notes.js
CHANGED
|
@@ -42,42 +42,44 @@ const MAX_NOTES = 40;
|
|
|
42
42
|
/** A single fact is one line; anything longer is prose, not a fact. */
|
|
43
43
|
const MAX_NOTE_LENGTH = 240;
|
|
44
44
|
/**
|
|
45
|
-
* Field separator between a
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*/
|
|
49
|
-
const ORIGIN_SEP = '\t';
|
|
50
|
-
/**
|
|
51
|
-
* Parse the stored file into fact+origin records. A line with no separator parses
|
|
52
|
-
* with an empty origin rather than being dropped, so a cache written before
|
|
53
|
-
* provenance existed still reads — and `buildEnvNotesBlock` renders those as
|
|
54
|
-
* "origin unrecorded" instead of silently claiming an author.
|
|
45
|
+
* Field separator between a note's fields in the stored file. A tab never occurs
|
|
46
|
+
* in a one-line fact (facts are prose), so it round-trips cleanly and any stray
|
|
47
|
+
* tab in an emitted fact is normalised to a space before storage.
|
|
55
48
|
*/
|
|
49
|
+
const FIELD_SEP = '\t';
|
|
50
|
+
/** Stored line: `fact TAB origin TAB runId TAB subject TAB resolvedBy`, where every
|
|
51
|
+
* field after the fact may be absent — a one- or two-field line is what earlier
|
|
52
|
+
* versions wrote, and it still reads. */
|
|
56
53
|
export function parseEnvNotes(raw) {
|
|
57
54
|
const out = [];
|
|
58
55
|
for (const line of raw.split('\n')) {
|
|
59
56
|
const t = line.trim();
|
|
60
57
|
if (t.length === 0)
|
|
61
58
|
continue;
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
59
|
+
const [fact, origin, runId, subject, resolvedBy] = t.split(FIELD_SEP).map(f => f.trim());
|
|
60
|
+
out.push({
|
|
61
|
+
fact,
|
|
62
|
+
origin: origin ?? '',
|
|
63
|
+
runId: runId ?? '',
|
|
64
|
+
// A legacy line carries no subject, so it gets the one the host would
|
|
65
|
+
// derive for it today — which is what makes the old file dedupe too.
|
|
66
|
+
subject: subject || deriveSubject(fact),
|
|
67
|
+
...(resolvedBy ? { resolvedBy } : {})
|
|
68
|
+
});
|
|
67
69
|
}
|
|
68
70
|
return out;
|
|
69
71
|
}
|
|
70
72
|
function serializeNote(n) {
|
|
71
|
-
return n.origin
|
|
73
|
+
return [n.fact, n.origin, n.runId, n.subject, n.resolvedBy ?? ''].join(FIELD_SEP).trimEnd();
|
|
72
74
|
}
|
|
73
|
-
/** Keyed on the
|
|
74
|
-
* ORIGINAL origin — provenance traces to who first established it. */
|
|
75
|
+
/** Keyed on the SUBJECT (case-insensitive), latest statement wins. */
|
|
75
76
|
const ledger = makeLedger({
|
|
76
77
|
file: ENV_NOTES_FILE,
|
|
77
78
|
max: MAX_NOTES,
|
|
78
|
-
key: n => n.
|
|
79
|
+
key: n => n.subject.toLowerCase(),
|
|
79
80
|
serialize: serializeNote,
|
|
80
|
-
parse: parseEnvNotes
|
|
81
|
+
parse: parseEnvNotes,
|
|
82
|
+
conflict: 'replace'
|
|
81
83
|
});
|
|
82
84
|
export function envNotesFile(cwd) {
|
|
83
85
|
return ledger.path(cwd);
|
|
@@ -86,24 +88,91 @@ export function envNotesFile(cwd) {
|
|
|
86
88
|
export async function readEnvNotes(cwd) {
|
|
87
89
|
return ledger.readRaw(cwd);
|
|
88
90
|
}
|
|
91
|
+
/** Leading words a subject may be built from: the generic ones say nothing about
|
|
92
|
+
* which fact this is. */
|
|
93
|
+
const SUBJECT_STOPWORDS = new Set([
|
|
94
|
+
'the',
|
|
95
|
+
'this',
|
|
96
|
+
'that',
|
|
97
|
+
'these',
|
|
98
|
+
'those',
|
|
99
|
+
'and',
|
|
100
|
+
'but',
|
|
101
|
+
'for',
|
|
102
|
+
'from',
|
|
103
|
+
'with',
|
|
104
|
+
'current',
|
|
105
|
+
'still',
|
|
106
|
+
'now',
|
|
107
|
+
'not',
|
|
108
|
+
'are',
|
|
109
|
+
'was',
|
|
110
|
+
'were'
|
|
111
|
+
]);
|
|
112
|
+
function subjectWords(s) {
|
|
113
|
+
return s
|
|
114
|
+
.toLowerCase()
|
|
115
|
+
.split(/[^a-z0-9]+/)
|
|
116
|
+
.filter(t => t.length > 2 && !SUBJECT_STOPWORDS.has(t));
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The subject of a fact the child did not name one for.
|
|
120
|
+
*
|
|
121
|
+
* A fact identifies what it is ABOUT by naming it first, and three rungs cover
|
|
122
|
+
* what an environment fact names: the SERVICE it reached (a URL authority), the
|
|
123
|
+
* COMMAND it ran (the first code span, by its basename so a tool named by path is
|
|
124
|
+
* the same tool), or — for the rest — its leading words.
|
|
125
|
+
*
|
|
126
|
+
* Measured against the 40-note cache a real 21-task run left behind, where 34
|
|
127
|
+
* slots restate two facts: it resolves them to nine subjects, so the twelfth
|
|
128
|
+
* re-measurement of one database replaces the eleventh instead of joining it.
|
|
129
|
+
*/
|
|
130
|
+
export function deriveSubject(fact) {
|
|
131
|
+
const url = /\b[a-z][a-z0-9+.-]*:\/\/(?:[^\s/@`]*@)?([^\s/`,;)]+)/i.exec(fact);
|
|
132
|
+
if (url)
|
|
133
|
+
return url[1].toLowerCase();
|
|
134
|
+
const code = /`([^`]+)`/.exec(fact);
|
|
135
|
+
if (code) {
|
|
136
|
+
// A leading `VAR=value` is the environment of the command, not the command.
|
|
137
|
+
const word = code[1]
|
|
138
|
+
.trim()
|
|
139
|
+
.split(/\s+/)
|
|
140
|
+
.find(w => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(w));
|
|
141
|
+
if (word !== undefined && word.length > 0) {
|
|
142
|
+
const bare = /^\.{0,2}\//.test(word) ? (word.split('/').pop() ?? word) : word;
|
|
143
|
+
if (bare.length > 0)
|
|
144
|
+
return bare.toLowerCase();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return subjectWords(fact).slice(0, 2).join(' ');
|
|
148
|
+
}
|
|
89
149
|
/**
|
|
90
|
-
* Pull
|
|
91
|
-
*
|
|
92
|
-
*
|
|
150
|
+
* Pull the facts out of a child's answer text. Three forms are accepted:
|
|
151
|
+
* `ENV-NOTE[<subject>]: <fact>` (what the instruction asks for), the bare
|
|
152
|
+
* `ENV-NOTE: <fact>` whose subject the host derives, and
|
|
153
|
+
* `ENV-NOTE-RESOLVED[<subject>]: <what was observed>`, which retracts a recorded
|
|
154
|
+
* claim. Facts are trimmed, deduped case-insensitively, and dropped when empty or
|
|
155
|
+
* over MAX_NOTE_LENGTH. A `VERDICT:` line and even a near-miss `ENV-NOTES:` line
|
|
93
156
|
* match nothing, so a verdict cannot leak into the fact cache.
|
|
94
157
|
*/
|
|
95
158
|
export function extractEnvNotes(text) {
|
|
96
159
|
const notes = [];
|
|
97
160
|
const seen = new Set();
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
161
|
+
const line = /^[ \t]*ENV-NOTE(-RESOLVED)?(?:\[([^\]\n]*)\])?:[ \t]*(.+)$/gm;
|
|
162
|
+
for (const m of text.matchAll(line)) {
|
|
163
|
+
const fact = m[3].trim();
|
|
164
|
+
if (fact.length === 0 || fact.length > MAX_NOTE_LENGTH)
|
|
101
165
|
continue;
|
|
102
|
-
const key =
|
|
166
|
+
const key = fact.toLowerCase();
|
|
103
167
|
if (seen.has(key))
|
|
104
168
|
continue;
|
|
105
169
|
seen.add(key);
|
|
106
|
-
|
|
170
|
+
const named = (m[2] ?? '').trim();
|
|
171
|
+
notes.push({
|
|
172
|
+
subject: named.length > 0 ? named : deriveSubject(fact),
|
|
173
|
+
fact,
|
|
174
|
+
...(m[1] ? { resolved: true } : {})
|
|
175
|
+
});
|
|
107
176
|
}
|
|
108
177
|
return notes;
|
|
109
178
|
}
|
|
@@ -140,50 +209,77 @@ export function isExcuseNote(fact) {
|
|
|
140
209
|
return EXCUSE_PATTERNS.some(re => re.test(fact));
|
|
141
210
|
}
|
|
142
211
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
212
|
+
* Store what a child shared, keyed by SUBJECT: a fresh statement about a subject
|
|
213
|
+
* REPLACES the one the cache held, so the newest measurement is the one later
|
|
214
|
+
* children read and a stale fact cannot outlive its correction. Each note is
|
|
215
|
+
* stamped with the `origin` task and the `runId` that recorded it — the run is
|
|
216
|
+
* what the prompt block scopes on, the task is the provenance a reader is told to
|
|
217
|
+
* re-validate against.
|
|
218
|
+
*
|
|
219
|
+
* A retraction stores the observation that closed the subject and marks it
|
|
220
|
+
* RESOLVED, so it stays as an audit line without being carried into another
|
|
221
|
+
* child's prompt.
|
|
149
222
|
*
|
|
150
223
|
* Tabs in an emitted fact are normalised to spaces before storage, which is what
|
|
151
224
|
* keeps the separator unambiguous. The cap holds too — sixty further facts leave
|
|
152
225
|
* MAX_NOTES stored. Failures are swallowed: the cache is a sharpener, never a
|
|
153
226
|
* blocker.
|
|
154
227
|
*/
|
|
155
|
-
export async function appendEnvNotes(cwd, notes, origin = '') {
|
|
228
|
+
export async function appendEnvNotes(cwd, notes, origin = '', runId = '') {
|
|
156
229
|
const fresh = [];
|
|
157
230
|
for (const note of notes) {
|
|
158
|
-
const fact = note.trim().replace(/\t/g, ' ');
|
|
159
|
-
|
|
231
|
+
const fact = note.fact.trim().replace(/\t/g, ' ');
|
|
232
|
+
const subject = note.subject.trim().replace(/\t/g, ' ');
|
|
233
|
+
if (fact.length === 0 || subject.length === 0)
|
|
160
234
|
continue;
|
|
161
|
-
fresh.push({
|
|
235
|
+
fresh.push({
|
|
236
|
+
fact,
|
|
237
|
+
origin: origin.trim(),
|
|
238
|
+
runId: runId.trim(),
|
|
239
|
+
subject,
|
|
240
|
+
// The field is the resolved FLAG as well as its provenance, so an
|
|
241
|
+
// unattributed retraction still needs a value to survive the round-trip.
|
|
242
|
+
...(note.resolved ? { resolvedBy: origin.trim() || 'unrecorded' } : {})
|
|
243
|
+
});
|
|
162
244
|
}
|
|
163
245
|
await ledger.append(cwd, fresh);
|
|
164
246
|
}
|
|
247
|
+
function noteLine(n) {
|
|
248
|
+
const origin = n.origin ? ` — recorded by ${n.origin}` : ' — origin unrecorded';
|
|
249
|
+
const flag = isExcuseNote(n.fact) ?
|
|
250
|
+
' [EXCUSE-CLASS — re-validate in the CURRENT tree before citing this to wave off any failure]'
|
|
251
|
+
: '';
|
|
252
|
+
return `- ${n.fact}${origin}${flag}`;
|
|
253
|
+
}
|
|
165
254
|
/**
|
|
166
255
|
* The prompt block a gate child receives when notes exist. Two things are
|
|
167
256
|
* load-bearing: the no-waiver caveat (facts save re-discovery time but grant no
|
|
168
257
|
* license to prepare/repair) and the trust discipline (a note is second-hand
|
|
169
258
|
* until re-validated; an EXCUSE-CLASS note may not wave off a failure without a
|
|
170
259
|
* live re-check; a grep of a generated artifact is not evidence of absence).
|
|
260
|
+
*
|
|
261
|
+
* THIS RUN leads. A fact measured by a sibling task minutes ago describes the tree
|
|
262
|
+
* the child is standing in; one from a run last week describes a tree that has
|
|
263
|
+
* moved since, and reading the two as one list is how a stale fact gets cited as
|
|
264
|
+
* current. Resolved subjects are dropped from both lists: a retracted claim is
|
|
265
|
+
* exactly the thing that must not be carried forward. `runId` '' (a cache written
|
|
266
|
+
* before runs were stamped) reads as an earlier run.
|
|
171
267
|
*/
|
|
172
|
-
export function buildEnvNotesBlock(raw) {
|
|
173
|
-
const notes = parseEnvNotes(raw);
|
|
268
|
+
export function buildEnvNotesBlock(raw, runId = '') {
|
|
269
|
+
const notes = parseEnvNotes(raw).filter(n => n.resolvedBy === undefined);
|
|
174
270
|
if (notes.length === 0)
|
|
175
271
|
return '';
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
});
|
|
272
|
+
const inRun = (n) => runId.length > 0 && n.runId === runId;
|
|
273
|
+
const mine = notes.filter(inRun);
|
|
274
|
+
const earlier = notes.filter(n => !inRun(n));
|
|
275
|
+
const earlierBlock = earlier.length === 0 ? []
|
|
276
|
+
: mine.length === 0 ? earlier.map(noteLine)
|
|
277
|
+
: ['', 'From EARLIER runs (an older tree — trust these less):', ...earlier.map(noteLine)];
|
|
183
278
|
return [
|
|
184
279
|
'KNOWN ENVIRONMENT FACTS — recorded by earlier verification passes in this run',
|
|
185
280
|
'(second-hand, may be stale or WRONG):',
|
|
186
|
-
...
|
|
281
|
+
...mine.map(noteLine),
|
|
282
|
+
...earlierBlock,
|
|
187
283
|
'',
|
|
188
284
|
'These facts only save you re-discovery time (where credentials/config live, which',
|
|
189
285
|
'tools are installed, which services are reachable). They are NOT a license to',
|
|
@@ -216,8 +312,14 @@ export const ENV_NOTE_EMIT_INSTRUCTION = [
|
|
|
216
312
|
'ENVIRONMENT FACTS — share what you discover: when you establish a durable fact about',
|
|
217
313
|
'THIS MACHINE or the project environment (a service reachable/absent at an address, where',
|
|
218
314
|
'credentials/config live, a tool or runtime present/missing and its version), emit a line',
|
|
219
|
-
' ENV-NOTE: <one-line fact>',
|
|
220
|
-
'anywhere in your answer, one per fact.
|
|
315
|
+
' ENV-NOTE[<subject>]: <one-line fact>',
|
|
316
|
+
'anywhere in your answer, one per fact. <subject> names WHAT the fact is about in a word or',
|
|
317
|
+
'two — the service, the tool, the command (`postgres:5432`, `bun test`, `docker compose`).',
|
|
318
|
+
'It is the cache key: a fact you emit under a subject REPLACES the one recorded for it, so',
|
|
319
|
+
'a re-measurement corrects the record instead of stacking another copy beside it. If a fact',
|
|
320
|
+
'listed above turns out NOT to be true any more, retract it with',
|
|
321
|
+
' ENV-NOTE-RESOLVED[<subject>]: <what you observed instead>',
|
|
322
|
+
'A fact is something you OBSERVED to be true of the',
|
|
221
323
|
'machine or environment — never a task verdict, never spec content, never a judgment about',
|
|
222
324
|
'the code. In particular do NOT record an absence you inferred from grepping a built or',
|
|
223
325
|
'minified artifact (identifiers there are mangled — a zero-hit grep proves nothing), and',
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
* `### npm:` then `### docs:` then `### url:` then `### service:`.
|
|
24
24
|
*/
|
|
25
25
|
import { chooseEcosystem, declaredDepNames, defaultEcosystemIo } from '../workers/docs-ecosystems.js';
|
|
26
|
+
import { queryTokenKey } from '../workers/research-cache.js';
|
|
27
|
+
import { cachedWorkerCall, workerAnswer, workerUnavailable } from '../workers/shared.js';
|
|
26
28
|
import { docsRaw } from '../workers/docs-core.js';
|
|
27
29
|
import { fetchRaw } from '../workers/fetch-core.js';
|
|
28
30
|
import { formatNpmVersionSection, npmVersionLookup } from '../workers/npm-version.js';
|
|
@@ -121,6 +123,21 @@ export async function buildExternalContext(source, deps, lookups, policy = {}) {
|
|
|
121
123
|
return '';
|
|
122
124
|
return `EXTERNAL CONTEXT\n${sections.join('\n\n')}\n\n`;
|
|
123
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* One enrichment lookup, through the per-run research cache (workers/shared.ts).
|
|
128
|
+
*
|
|
129
|
+
* A null result is a lookup that produced no answer — a refused ecosystem, a dead
|
|
130
|
+
* registry, a page that would not fetch — and is never stored, so a transient
|
|
131
|
+
* failure costs one task's block rather than the whole run's. The cached value is
|
|
132
|
+
* the RESULT, not rendered text: the block assembly below formats it.
|
|
133
|
+
*/
|
|
134
|
+
async function cached(cwd, tool, key, run) {
|
|
135
|
+
const { details } = await cachedWorkerCall(cwd, tool, key, async () => {
|
|
136
|
+
const value = await run();
|
|
137
|
+
return value === null ? workerUnavailable('', value, 'no-answer') : workerAnswer('', value);
|
|
138
|
+
});
|
|
139
|
+
return details;
|
|
140
|
+
}
|
|
124
141
|
/**
|
|
125
142
|
* The RESEARCH-phase binding: raw workers, no caps, live versions for every
|
|
126
143
|
* named dep, truncated bodies, timed, and short-circuited when there is nothing
|
|
@@ -147,7 +164,7 @@ export async function gatherExternalContext(refined, deps) {
|
|
|
147
164
|
const info = await npmVersionFn(pkg, { signal: deps.signal });
|
|
148
165
|
return info ? { info, label: 'npm' } : null;
|
|
149
166
|
};
|
|
150
|
-
const
|
|
167
|
+
const liveVersion = async (pkg) => {
|
|
151
168
|
const choice = chooseEcosystem({
|
|
152
169
|
cwd: deps.cwd,
|
|
153
170
|
declaresPackage: p => p.declaredRange(p.parentPackage(pkg), deps.cwd) !== null,
|
|
@@ -171,8 +188,11 @@ export async function gatherExternalContext(refined, deps) {
|
|
|
171
188
|
const info = await choice.profile.latest(pkg, io);
|
|
172
189
|
return info ? { info, label: choice.profile.registryLabel } : null;
|
|
173
190
|
};
|
|
191
|
+
// A registry round-trip per declared dependency, per task. The answer does not
|
|
192
|
+
// move within a run, so the first task of the run pays for it.
|
|
193
|
+
const versionLookup = (pkg) => cached(deps.cwd, 'enrichment:version', pkg, () => liveVersion(pkg));
|
|
174
194
|
return buildExternalContext(refined, deps, {
|
|
175
|
-
docs:
|
|
195
|
+
docs: pkg => cached(deps.cwd, 'enrichment:docs', `${pkg}\u0000${queryTokenKey(docsQuery)}`, async () => {
|
|
176
196
|
const r = await docsRawFn({
|
|
177
197
|
pkg,
|
|
178
198
|
query: docsQuery,
|
|
@@ -189,11 +209,11 @@ export async function gatherExternalContext(refined, deps) {
|
|
|
189
209
|
.slice(0, RAW_BODY_LIMIT)
|
|
190
210
|
: undefined
|
|
191
211
|
};
|
|
192
|
-
},
|
|
193
|
-
url: async (
|
|
212
|
+
}),
|
|
213
|
+
url: url => cached(deps.cwd, 'enrichment:url', url, async () => {
|
|
194
214
|
const r = await fetchRawFn({ url, signal: deps.signal });
|
|
195
215
|
return { body: r.markdown.slice(0, RAW_BODY_LIMIT) };
|
|
196
|
-
},
|
|
216
|
+
}),
|
|
197
217
|
search: deps.searchFn
|
|
198
218
|
}, {
|
|
199
219
|
versionLookup,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fix-context — what an AUTOFIX re-run is told about the failure it exists to fix.
|
|
3
|
+
*
|
|
4
|
+
* It used to be one prose string: the FAIL reason, plus a diagnosis paragraph if
|
|
5
|
+
* a judge happened to produce one. Everything the gate had already computed
|
|
6
|
+
* stayed behind — which CLASS of failure it was, how many attempts had already
|
|
7
|
+
* been spent on it, what the deterministic probes found, and whether the spec
|
|
8
|
+
* contradicts itself. So the re-run re-derived what the gate knew, and in 0034 it
|
|
9
|
+
* re-derived it wrong: told only "lint fails", it widened suppressions until the
|
|
10
|
+
* check went quiet.
|
|
11
|
+
*
|
|
12
|
+
* A value, then, and one renderer. The banner keeps its RE-ATTEMPT opening — it
|
|
13
|
+
* is the sentence the implementer reads first and the one thing about the old
|
|
14
|
+
* string worth keeping.
|
|
15
|
+
*/
|
|
16
|
+
import type { Disposition, SpecContradiction } from './gate-resolution.js';
|
|
17
|
+
import type { ProbeFindings, VerifyOutcome } from './verify-work.js';
|
|
18
|
+
export interface FixContext {
|
|
19
|
+
outcome: VerifyOutcome;
|
|
20
|
+
disposition: Disposition;
|
|
21
|
+
/** The deterministic findings the verify pass computed for this verdict. */
|
|
22
|
+
probes: ProbeFindings;
|
|
23
|
+
/** Which unattended re-run this is, 1-based. */
|
|
24
|
+
attempt: number;
|
|
25
|
+
contradiction?: SpecContradiction | null;
|
|
26
|
+
/** The resolution judge's located cause, when it found one beyond the FAIL text. */
|
|
27
|
+
diagnosis?: string;
|
|
28
|
+
/** What the user typed instead of picking a card. */
|
|
29
|
+
guidance?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The RE-ATTEMPT banner prepended to the delivered spec.
|
|
33
|
+
*
|
|
34
|
+
* The fail CLASS leads because it decides what a fix may even look like: a
|
|
35
|
+
* `repo-health` failure is about the project's own statics, a `model-verdict` one
|
|
36
|
+
* is about behavior the spec demanded, and confusing the two is how a re-run
|
|
37
|
+
* "fixes" a verdict by silencing a linter.
|
|
38
|
+
*/
|
|
39
|
+
export declare function formatFixBanner(ctx: FixContext): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { VERIFY_FAIL_PREFIX } from './verify-work.js';
|
|
2
|
+
const PROBE_LABELS = {
|
|
3
|
+
repoHealth: 'inherited repo health (NOT yours to fix)',
|
|
4
|
+
substitution: 'tests this task itself wrote',
|
|
5
|
+
prohibition: 'spec-forbidden paths this task modified',
|
|
6
|
+
crossTaskDeletion: "another task's deliverables this task deletes",
|
|
7
|
+
probeGaming: 'lines written to quiet a check',
|
|
8
|
+
skipEscape: 'VERIFY commands that skip themselves',
|
|
9
|
+
foreignPath: 'absolute paths that resolve nowhere here',
|
|
10
|
+
scriptEscape: 'check scripts that cannot fail',
|
|
11
|
+
runnerGlob: 'test runners claiming the same files',
|
|
12
|
+
testAssembly: 'tests that rebuild production wiring',
|
|
13
|
+
suppressionWidening: 'suppressions this task added'
|
|
14
|
+
};
|
|
15
|
+
function probeBlock(probes) {
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const [key, findings] of Object.entries(probes)) {
|
|
18
|
+
if (!findings || findings.length === 0)
|
|
19
|
+
continue;
|
|
20
|
+
out.push(`- ${PROBE_LABELS[key] ?? key}:`);
|
|
21
|
+
for (const f of findings)
|
|
22
|
+
out.push(` ${f}`);
|
|
23
|
+
}
|
|
24
|
+
return out.length === 0 ? [] : ['', 'WHAT THE DETERMINISTIC PROBES ALREADY FOUND:', ...out];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The RE-ATTEMPT banner prepended to the delivered spec.
|
|
28
|
+
*
|
|
29
|
+
* The fail CLASS leads because it decides what a fix may even look like: a
|
|
30
|
+
* `repo-health` failure is about the project's own statics, a `model-verdict` one
|
|
31
|
+
* is about behavior the spec demanded, and confusing the two is how a re-run
|
|
32
|
+
* "fixes" a verdict by silencing a linter.
|
|
33
|
+
*/
|
|
34
|
+
export function formatFixBanner(ctx) {
|
|
35
|
+
const failClass = ctx.outcome.failClass;
|
|
36
|
+
const lines = [
|
|
37
|
+
'RE-ATTEMPT — your previous implementation of this task FAILED verification.',
|
|
38
|
+
"Fix the cause below, then make the spec's VERIFY block pass. Do NOT start over;",
|
|
39
|
+
'change only what is needed to resolve the failure.',
|
|
40
|
+
'',
|
|
41
|
+
`ATTEMPT ${ctx.attempt} of this task's fix budget.`
|
|
42
|
+
];
|
|
43
|
+
if (failClass) {
|
|
44
|
+
lines.push(`FAILURE CLASS: ${failClass} — ${VERIFY_FAIL_PREFIX[failClass]}. A fix that does not`, 'move THIS class has not fixed anything; suppressing, disabling, deleting or', 'skipping the check that reported it is a defect, not a fix.');
|
|
45
|
+
}
|
|
46
|
+
lines.push('', `VERIFICATION FAILURE:\n${(ctx.outcome.reason ?? '').trim()}`);
|
|
47
|
+
if (ctx.diagnosis && ctx.diagnosis.trim().length > 0) {
|
|
48
|
+
lines.push('', `DIAGNOSIS (a read-only investigation of this failure found):\n${ctx.diagnosis.trim()}`);
|
|
49
|
+
}
|
|
50
|
+
lines.push(...probeBlock(ctx.probes));
|
|
51
|
+
if (ctx.contradiction) {
|
|
52
|
+
lines.push('', `SPEC CONTRADICTION: \`${ctx.contradiction.frozenPath}\` is frozen by this task's own`, `spec, and ${ctx.contradiction.criterion || 'the failing check'} cannot be satisfied`, 'without editing it. Do NOT edit the frozen path. Report the contradiction in your', 'summary and fix whatever else you can reach.');
|
|
53
|
+
}
|
|
54
|
+
if (ctx.guidance && ctx.guidance.trim().length > 0) {
|
|
55
|
+
lines.push('', `User guidance: ${ctx.guidance.trim()}`);
|
|
56
|
+
}
|
|
57
|
+
return lines.join('\n');
|
|
58
|
+
}
|
package/dist/task/gate-deps.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import type { GateDeps } from './task-gates.js';
|
|
3
|
+
import { type HealthBaseline } from './health-baseline.js';
|
|
3
4
|
import { type VerifyProbes } from './verify-work.js';
|
|
5
|
+
import type { CommandRunner } from './command-run.js';
|
|
4
6
|
import { type FinalFixResult } from './final-gate-fix.js';
|
|
5
7
|
import { type AddedLine } from './probe-gaming.js';
|
|
6
8
|
import { type ChangedFile } from './substitution-probe.js';
|
|
7
9
|
import { type TreeChangeSummary, type IgnoredSnapshot } from './write-guard.js';
|
|
10
|
+
import { type DiffLine, type SuppressionHit } from './suppression-probe.js';
|
|
8
11
|
/** A function that re-runs a task's implementation turn (AUTOFIX). Injected by the
|
|
9
12
|
* command so this module stays free of the orchestrators (avoids an import cycle). */
|
|
10
13
|
export type RunTaskFn = GateDeps['runTask'];
|
|
@@ -152,6 +155,25 @@ export declare function collectTestAssemblyFindings(cwd: string, changed: Change
|
|
|
152
155
|
* would otherwise be the same four-line try/catch at four gate sites.
|
|
153
156
|
*/
|
|
154
157
|
export declare function readSpecForVerification(cwd: string, taskId: string): Promise<string | null>;
|
|
158
|
+
/**
|
|
159
|
+
* The task's rendered Q&A, which is what a constraint's `[from: Q<n>]` tag
|
|
160
|
+
* resolves against (see qa-transcript's `qaKindsFromRecord`). Null when the task
|
|
161
|
+
* asked nothing, or the file cannot be read — both mean every tagged constraint
|
|
162
|
+
* reads as `derived`, and `constraint-policy.ts` says what that is worth.
|
|
163
|
+
*/
|
|
164
|
+
export declare function readQaRecordForVerification(cwd: string, taskId: string): Promise<string | null>;
|
|
165
|
+
/**
|
|
166
|
+
* The health baseline the verify gate's differential needs: the one stored on the
|
|
167
|
+
* task file, or — for a task file that has none — one established lazily from a
|
|
168
|
+
* DETACHED WORKTREE at HEAD and written back, so the next resolution round does
|
|
169
|
+
* not pay for it again.
|
|
170
|
+
*
|
|
171
|
+
* Everything here is best-effort. A baseline that cannot be established is null,
|
|
172
|
+
* and `classifyHealthDelta` already states what an absent baseline means; failing
|
|
173
|
+
* the gate because the differential could not run would be the harness blaming
|
|
174
|
+
* the work for its own gap.
|
|
175
|
+
*/
|
|
176
|
+
export declare function healthBaselineFor(cwd: string, taskId: string, signal: AbortSignal): Promise<HealthBaseline | null>;
|
|
155
177
|
/**
|
|
156
178
|
* Bind the deterministic verify probes — THE one place the collectors above meet
|
|
157
179
|
* the probe table in verify-work.ts. One entry per `BoundProbeKey`; the table row
|
|
@@ -169,8 +191,35 @@ export declare function buildVerifyProbes(params: {
|
|
|
169
191
|
signal?: AbortSignal;
|
|
170
192
|
taskId: string;
|
|
171
193
|
spec: string | null;
|
|
194
|
+
/** The task's `## grill Q&A`, so a prohibition carries the WEIGHT of the
|
|
195
|
+
* constraint that states it rather than being unwaivable by default. */
|
|
196
|
+
qaRecord?: string | null;
|
|
172
197
|
log?: (msg: string) => void;
|
|
198
|
+
/** Names the gate-evidence command currently running, for the caller's live
|
|
199
|
+
* status line — the same channel `runRepoHealthCheck` takes. */
|
|
200
|
+
onCommand?: (cmd: string) => void;
|
|
201
|
+
/** The spawner for the gate-evidence commands, as `runRepoHealthCheck` takes
|
|
202
|
+
* one: injected so a session's command count is assertable without a shell. */
|
|
203
|
+
run?: CommandRunner;
|
|
173
204
|
}): VerifyProbes;
|
|
205
|
+
/**
|
|
206
|
+
* The task's net-new suppressions: the files its work touched (the same tree
|
|
207
|
+
* shape the write-guard reads), diffed against the checkpoint HEAD, counted per
|
|
208
|
+
* pattern.
|
|
209
|
+
*
|
|
210
|
+
* The pattern set follows the repo's detected ecosystems, so a Go project is not
|
|
211
|
+
* scanned for `@ts-expect-error` and a TypeScript one is not scanned for
|
|
212
|
+
* `//nolint`; `suppressionPatterns` in the config adds project-specific rows.
|
|
213
|
+
* Failures degrade to no findings — a sharpener, never a blocker.
|
|
214
|
+
*/
|
|
215
|
+
export declare function collectSuppressionHits(cwd: string, signal?: AbortSignal): Promise<SuppressionHit[]>;
|
|
216
|
+
/**
|
|
217
|
+
* A unified diff's content lines, tagged with their side and their file. The `+++`
|
|
218
|
+
* header names the file and the `---`/`+++` headers themselves are excluded, which
|
|
219
|
+
* is the one thing a naive `startsWith('+')` scan gets wrong — it counts every
|
|
220
|
+
* changed file's own header as an added line.
|
|
221
|
+
*/
|
|
222
|
+
export declare function parseDiffLines(diff: string): DiffLine[];
|
|
174
223
|
/**
|
|
175
224
|
* Build the gate deps for one command run. `runTask` is the orchestrator's
|
|
176
225
|
* implementation re-runner, injected by the caller. The returned object also drives
|