@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/lint-fix.js
CHANGED
|
@@ -261,6 +261,7 @@ export async function runBoundedLintFix(deps) {
|
|
|
261
261
|
deps.log?.(`lint-fix REVERT-GUARD — discarded ${violations.length} work file(s)`);
|
|
262
262
|
return {
|
|
263
263
|
ok: false,
|
|
264
|
+
class: 'not-applied',
|
|
264
265
|
reason: `revert-guard: fix pass discarded work (${violations.slice(0, 3).join(', ')}`
|
|
265
266
|
+ `${violations.length > 3 ? ', …' : ''}) — fix ${snapshot ? 'rolled back' : 'REJECTED but no snapshot to restore'}`
|
|
266
267
|
};
|
|
@@ -288,6 +289,7 @@ export async function runBoundedLintFix(deps) {
|
|
|
288
289
|
deps.log?.(`lint-fix CROSS-TASK-DELETION GUARD — ${named[0]}`);
|
|
289
290
|
return {
|
|
290
291
|
ok: false,
|
|
292
|
+
class: 'not-applied',
|
|
291
293
|
reason: `cross-task-deletion: fix child DELETED sibling task deliverable(s) `
|
|
292
294
|
+ `(${named.slice(0, 3).join('; ')}${crossDeletions.length > 3 ? '; …' : ''}) `
|
|
293
295
|
+ `— restored from HEAD; deleting another task's committed work is not a fix`
|
|
@@ -311,6 +313,8 @@ export async function runBoundedLintFix(deps) {
|
|
|
311
313
|
deps.log?.(`lint-fix FROZEN-PATH GUARD — ${frozenViolations.slice(0, 3).join(', ')}`);
|
|
312
314
|
return {
|
|
313
315
|
ok: false,
|
|
316
|
+
class: 'frozen-path',
|
|
317
|
+
contradiction: { criterion: deps.failReason, frozenPath: frozenViolations[0] },
|
|
314
318
|
reason: `frozen-path: fix child modified spec-frozen path(s) `
|
|
315
319
|
+ `(${frozenViolations.slice(0, 3).join(', ')}`
|
|
316
320
|
+ `${frozenViolations.length > 3 ? ', …' : ''}) — `
|
|
@@ -322,7 +326,7 @@ export async function runBoundedLintFix(deps) {
|
|
|
322
326
|
}
|
|
323
327
|
if (end.kind === 'error') {
|
|
324
328
|
deps.log?.(`lint-fix child failed — ${end.msg}`);
|
|
325
|
-
return { ok: false, reason: `fix child failed: ${end.msg}` };
|
|
329
|
+
return { ok: false, class: 'not-applied', reason: `fix child failed: ${end.msg}` };
|
|
326
330
|
}
|
|
327
331
|
if (end.kind === 'blocked')
|
|
328
332
|
deps.log?.(`lint-fix BLOCKED — ${end.note}`);
|
|
@@ -344,28 +348,30 @@ export async function runBoundedLintFix(deps) {
|
|
|
344
348
|
const health = await deps.repoHealth();
|
|
345
349
|
if (!health.ok) {
|
|
346
350
|
if (end.kind === 'blocked') {
|
|
347
|
-
return { ok: false, reason: `fix child blocked: ${end.note}` };
|
|
351
|
+
return { ok: false, class: 'not-applied', reason: `fix child blocked: ${end.note}` };
|
|
348
352
|
}
|
|
349
353
|
// FROZEN-PATH TRACE on non-convergence: when the child was honest — it did
|
|
350
354
|
// NOT touch the frozen path, so the guard above never tripped — but the
|
|
351
355
|
// check is still red and its own output NAMES a frozen path (the typed
|
|
352
356
|
// ESLint message quoted in this file's header), the findings can only be
|
|
353
|
-
// fixed by an edit this task's spec forbids. Report
|
|
354
|
-
// `frozen-path
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
+
// fixed by an edit this task's spec forbids. Report the same
|
|
358
|
+
// `frozen-path` class as the guard trip, so the gate's decision table
|
|
359
|
+
// exits on round one instead of burning unattended AUTOFIX rounds an impl
|
|
360
|
+
// re-run under the same freeze cannot converge out of.
|
|
357
361
|
const implicated = frozen.filter(p => pathNamedIn(`${health.reason}\n${health.output ?? ''}`, p));
|
|
358
362
|
if (implicated.length > 0) {
|
|
359
363
|
deps.log?.(`lint-fix FROZEN-PATH TRACE — ${implicated.slice(0, 3).join(', ')}`);
|
|
360
364
|
return {
|
|
361
365
|
ok: false,
|
|
366
|
+
class: 'frozen-path',
|
|
367
|
+
contradiction: { criterion: health.reason, frozenPath: implicated[0] },
|
|
362
368
|
reason: `frozen-path: static findings implicate spec-frozen path(s) `
|
|
363
369
|
+ `(${implicated.slice(0, 3).join(', ')}`
|
|
364
370
|
+ `${implicated.length > 3 ? ', …' : ''}) — did not converge `
|
|
365
371
|
+ `(${health.reason}); a fix under this task's constraints cannot converge`
|
|
366
372
|
};
|
|
367
373
|
}
|
|
368
|
-
return { ok: false, reason: `did not converge: ${health.reason}` };
|
|
374
|
+
return { ok: false, class: 'not-applied', reason: `did not converge: ${health.reason}` };
|
|
369
375
|
}
|
|
370
|
-
return { ok: true, reason: guardNote };
|
|
376
|
+
return { ok: true, class: 'converged', ...(guardNote === undefined ? {} : { reason: guardNote }) };
|
|
371
377
|
}
|
|
@@ -30,7 +30,15 @@ export interface ToolCall {
|
|
|
30
30
|
export interface LoopHit {
|
|
31
31
|
call: ToolCall;
|
|
32
32
|
count: number;
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* How many recent calls `count` was counted over.
|
|
35
|
+
*
|
|
36
|
+
* OPTIONAL because a stall hit counts a streak or a byte total, not a window
|
|
37
|
+
* — it used to carry 0 there, which every renderer printed as "in the last 0
|
|
38
|
+
* calls". Absent means "this hit has no window"; a renderer must say what the
|
|
39
|
+
* hit actually measured instead.
|
|
40
|
+
*/
|
|
41
|
+
windowSize?: number;
|
|
34
42
|
/**
|
|
35
43
|
* Set when the kill came from the whole-run StallDetector rather than this
|
|
36
44
|
* short-window detector, naming which of its two rules tripped
|
|
@@ -63,11 +71,25 @@ export declare class LoopDetector {
|
|
|
63
71
|
/** Revisits of one path needed to trip; defaults to the exact threshold. */
|
|
64
72
|
private readonly pathThreshold;
|
|
65
73
|
private readonly buf;
|
|
74
|
+
/**
|
|
75
|
+
* Every path the attempt has targeted, with the span it covered — the WHOLE
|
|
76
|
+
* attempt, not the window `buf` holds. It is the only record of what a killed
|
|
77
|
+
* attempt actually read: the kill fires from the tool-call hook, before a byte
|
|
78
|
+
* of answer text streams, so text carry-forward is empty by construction and
|
|
79
|
+
* the re-spawn would otherwise start blind.
|
|
80
|
+
*/
|
|
81
|
+
private readonly visits;
|
|
66
82
|
constructor(window?: number, threshold?: number,
|
|
67
83
|
/** Revisits of one path needed to trip; defaults to the exact threshold. */
|
|
68
84
|
pathThreshold?: number);
|
|
69
85
|
/** Record a tool call. Returns LoopHit if either threshold is breached, else null. */
|
|
70
86
|
record(call: ToolCall): LoopHit | null;
|
|
87
|
+
/**
|
|
88
|
+
* The attempt's READ-SET, in first-seen order: every path it opened, with the
|
|
89
|
+
* line span when the call named one. This is what a restart carries instead of
|
|
90
|
+
* answer text — see {@link visits}.
|
|
91
|
+
*/
|
|
92
|
+
visited(): string[];
|
|
71
93
|
/**
|
|
72
94
|
* Count same-path calls in the window that are "revisits" — accesses that end
|
|
73
95
|
* no further into the file than the furthest line already covered for that
|
|
@@ -84,5 +106,32 @@ export declare class LoopDetector {
|
|
|
84
106
|
*/
|
|
85
107
|
private countRevisits;
|
|
86
108
|
}
|
|
87
|
-
/**
|
|
88
|
-
|
|
109
|
+
/**
|
|
110
|
+
* What killed the attempt, as prose a human reads in a degraded section or a
|
|
111
|
+
* `loop events` line.
|
|
112
|
+
*
|
|
113
|
+
* ONE renderer because each rule measures something different — calls in a
|
|
114
|
+
* window, a consecutive streak, bytes against a context window — and a shared
|
|
115
|
+
* "×N in the last M calls" template printed the streak's absent window as
|
|
116
|
+
* "in the last 0 calls".
|
|
117
|
+
*/
|
|
118
|
+
export declare function describeLoopHit(hit: LoopHit): string;
|
|
119
|
+
/**
|
|
120
|
+
* The read-set clause a restart hint carries, or '' when the killed attempt
|
|
121
|
+
* opened nothing.
|
|
122
|
+
*
|
|
123
|
+
* It is the only thing a loop restart CAN carry: the kill fires from the
|
|
124
|
+
* tool-call hook, so there is no partial answer to hand forward (which is why
|
|
125
|
+
* `loop` is absent from CARRY_FORWARD_REASONS in pi-worker-core.ts). Without it
|
|
126
|
+
* "do not re-read what you have already read" names no files and the re-spawn
|
|
127
|
+
* re-reads the same ones.
|
|
128
|
+
*
|
|
129
|
+
* Bounded so a rotation over hundreds of files cannot outgrow the prompt it is
|
|
130
|
+
* prepended to; the count keeps the elision honest.
|
|
131
|
+
*/
|
|
132
|
+
export declare function formatReadSet(visited: readonly string[]): string;
|
|
133
|
+
/**
|
|
134
|
+
* The restart hint a re-spawned child gets after a loop kill: names the call, and
|
|
135
|
+
* the read-set the killed attempt built up.
|
|
136
|
+
*/
|
|
137
|
+
export declare function formatLoopHint(hit: LoopHit, visited?: readonly string[]): string;
|
|
@@ -96,6 +96,14 @@ export class LoopDetector {
|
|
|
96
96
|
threshold;
|
|
97
97
|
pathThreshold;
|
|
98
98
|
buf = [];
|
|
99
|
+
/**
|
|
100
|
+
* Every path the attempt has targeted, with the span it covered — the WHOLE
|
|
101
|
+
* attempt, not the window `buf` holds. It is the only record of what a killed
|
|
102
|
+
* attempt actually read: the kill fires from the tool-call hook, before a byte
|
|
103
|
+
* of answer text streams, so text carry-forward is empty by construction and
|
|
104
|
+
* the re-spawn would otherwise start blind.
|
|
105
|
+
*/
|
|
106
|
+
visits = new Map();
|
|
99
107
|
constructor(window = LOOP_WINDOW, threshold = LOOP_THRESHOLD,
|
|
100
108
|
/** Revisits of one path needed to trip; defaults to the exact threshold. */
|
|
101
109
|
pathThreshold = threshold) {
|
|
@@ -107,9 +115,17 @@ export class LoopDetector {
|
|
|
107
115
|
record(call) {
|
|
108
116
|
const key = loopKey(call);
|
|
109
117
|
const offset = readOffset(call.args);
|
|
110
|
-
|
|
118
|
+
const path = primaryPath(call.args);
|
|
119
|
+
const end = readEnd(call.args, offset);
|
|
120
|
+
this.buf.push({ key, path, offset, end });
|
|
111
121
|
if (this.buf.length > this.window)
|
|
112
122
|
this.buf.shift();
|
|
123
|
+
if (path !== null) {
|
|
124
|
+
const seen = this.visits.get(path);
|
|
125
|
+
this.visits.set(path, seen ?
|
|
126
|
+
{ from: Math.min(seen.from, offset), to: Math.max(seen.to, end) }
|
|
127
|
+
: { from: offset, to: end });
|
|
128
|
+
}
|
|
113
129
|
// 1. Exact-match loop: identical (name, args) repeated past threshold.
|
|
114
130
|
let exact = 0;
|
|
115
131
|
for (const e of this.buf)
|
|
@@ -120,7 +136,6 @@ export class LoopDetector {
|
|
|
120
136
|
// 2. Path-aware loop: the same file re-targeted without forward progress.
|
|
121
137
|
// Caught here precisely because varied offset/limit change the exact key on
|
|
122
138
|
// every call, so pattern 1 above can never see it.
|
|
123
|
-
const path = this.buf[this.buf.length - 1].path;
|
|
124
139
|
if (path !== null) {
|
|
125
140
|
const revisits = this.countRevisits(path);
|
|
126
141
|
if (revisits >= this.pathThreshold) {
|
|
@@ -129,6 +144,14 @@ export class LoopDetector {
|
|
|
129
144
|
}
|
|
130
145
|
return null;
|
|
131
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* The attempt's READ-SET, in first-seen order: every path it opened, with the
|
|
149
|
+
* line span when the call named one. This is what a restart carries instead of
|
|
150
|
+
* answer text — see {@link visits}.
|
|
151
|
+
*/
|
|
152
|
+
visited() {
|
|
153
|
+
return [...this.visits].map(([path, span]) => span.to === Infinity ? path : `${path} (lines ${span.from}-${span.to})`);
|
|
154
|
+
}
|
|
132
155
|
/**
|
|
133
156
|
* Count same-path calls in the window that are "revisits" — accesses that end
|
|
134
157
|
* no further into the file than the furthest line already covered for that
|
|
@@ -157,11 +180,64 @@ export class LoopDetector {
|
|
|
157
180
|
return revisits;
|
|
158
181
|
}
|
|
159
182
|
}
|
|
160
|
-
/**
|
|
161
|
-
|
|
183
|
+
/**
|
|
184
|
+
* What killed the attempt, as prose a human reads in a degraded section or a
|
|
185
|
+
* `loop events` line.
|
|
186
|
+
*
|
|
187
|
+
* ONE renderer because each rule measures something different — calls in a
|
|
188
|
+
* window, a consecutive streak, bytes against a context window — and a shared
|
|
189
|
+
* "×N in the last M calls" template printed the streak's absent window as
|
|
190
|
+
* "in the last 0 calls".
|
|
191
|
+
*/
|
|
192
|
+
export function describeLoopHit(hit) {
|
|
193
|
+
const call = `${hit.call.name}(${JSON.stringify(hit.call.args)})`;
|
|
194
|
+
if (hit.stall === 'context-churn') {
|
|
195
|
+
return (`pulled in about ${hit.count} tokens of tool output, more than its `
|
|
196
|
+
+ `${hit.windowSize}-token context window holds (last call ${call})`);
|
|
197
|
+
}
|
|
198
|
+
if (hit.stall === 'no-new-ground') {
|
|
199
|
+
return (`stopped covering new ground — ${hit.count} consecutive tool calls returned `
|
|
200
|
+
+ `nothing it had not already seen (last call ${call})`);
|
|
201
|
+
}
|
|
202
|
+
return `stuck in a loop — called ${call} ×${hit.count} in the last ${hit.windowSize} calls`;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* The read-set clause a restart hint carries, or '' when the killed attempt
|
|
206
|
+
* opened nothing.
|
|
207
|
+
*
|
|
208
|
+
* It is the only thing a loop restart CAN carry: the kill fires from the
|
|
209
|
+
* tool-call hook, so there is no partial answer to hand forward (which is why
|
|
210
|
+
* `loop` is absent from CARRY_FORWARD_REASONS in pi-worker-core.ts). Without it
|
|
211
|
+
* "do not re-read what you have already read" names no files and the re-spawn
|
|
212
|
+
* re-reads the same ones.
|
|
213
|
+
*
|
|
214
|
+
* Bounded so a rotation over hundreds of files cannot outgrow the prompt it is
|
|
215
|
+
* prepended to; the count keeps the elision honest.
|
|
216
|
+
*/
|
|
217
|
+
export function formatReadSet(visited) {
|
|
218
|
+
if (visited.length === 0)
|
|
219
|
+
return '';
|
|
220
|
+
const shown = visited.slice(0, MAX_READ_SET_ENTRIES);
|
|
221
|
+
const rest = visited.length - shown.length;
|
|
222
|
+
return (` You have already read, do not re-open: ${shown.join(', ')}`
|
|
223
|
+
+ (rest > 0 ? `, and ${rest} more` : '')
|
|
224
|
+
+ '.');
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* How many paths a restart hint lists. The read-set is a reminder, not an
|
|
228
|
+
* inventory: past a screenful the tail stops being read and only costs prefill,
|
|
229
|
+
* and the elision line still tells the child there was more.
|
|
230
|
+
*/
|
|
231
|
+
const MAX_READ_SET_ENTRIES = 40;
|
|
232
|
+
/**
|
|
233
|
+
* The restart hint a re-spawned child gets after a loop kill: names the call, and
|
|
234
|
+
* the read-set the killed attempt built up.
|
|
235
|
+
*/
|
|
236
|
+
export function formatLoopHint(hit, visited = []) {
|
|
162
237
|
const argsStr = JSON.stringify(hit.call.args);
|
|
163
238
|
return (`[SYSTEM NOTE: Your prior attempt called ${hit.call.name}(${argsStr}) `
|
|
164
239
|
+ `${hit.count} times in the last ${hit.windowSize} tool calls — you appeared to be `
|
|
165
240
|
+ `stuck in a loop. Avoid repeating that exact call; if you've already seen its result, `
|
|
166
|
-
+ `work from memory or pick a different angle
|
|
241
|
+
+ `work from memory or pick a different angle.`
|
|
242
|
+
+ `${formatReadSet(visited)}]`);
|
|
167
243
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expand any @file references in the feature text by appending each referenced
|
|
3
|
+
* file's contents, so the planning children (clarify, decompose) always see the
|
|
4
|
+
* real spec inline instead of relying on the model to open the file itself.
|
|
5
|
+
* Without this, clarify on a one-line "Implement @spec.md" tends to bail with
|
|
6
|
+
* NONE because, to the model, the request looks small and unambiguous.
|
|
7
|
+
* Unreadable mentions (typos, non-file @tokens) are left untouched; the feature
|
|
8
|
+
* is returned verbatim when nothing readable is referenced.
|
|
9
|
+
*/
|
|
10
|
+
export declare function expandFeatureMentions(cwd: string, feature: string): Promise<string>;
|
|
11
|
+
/**
|
|
12
|
+
* The @file references in the feature that point at a readable file on disk —
|
|
13
|
+
* the bare path tokens, deduped, in first-seen order. Unreadable mentions
|
|
14
|
+
* (typos, non-file @tokens) are dropped so we never advertise a missing file as
|
|
15
|
+
* an authoritative spec.
|
|
16
|
+
*/
|
|
17
|
+
export declare function readableMentions(cwd: string, feature: string): Promise<string[]>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@file` references in a prompt, and the two things done with them: inlining the
|
|
3
|
+
* file's contents for the planning children, and naming the files themselves.
|
|
4
|
+
*
|
|
5
|
+
* Its own module because both the PLANNING side (auto-orchestrator, which threads
|
|
6
|
+
* the spec doc into every task title) and the PHASE side (research, which lets the
|
|
7
|
+
* orientation core select a cited doc) need the same answer, and the phase side
|
|
8
|
+
* cannot import the orchestrator that imports it.
|
|
9
|
+
*/
|
|
10
|
+
import * as fsp from 'node:fs/promises';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import { readTextFile } from '../shared/fs-text.js';
|
|
13
|
+
const MENTION_RE = /(?:^|\s)@([^\s]+)/g;
|
|
14
|
+
// Trailing punctuation a user naturally types AFTER an @-mention when it sits in
|
|
15
|
+
// prose — "Implement @design.md, reuse…" or "see @spec.md." — which the greedy
|
|
16
|
+
// [^\s]+ above would otherwise swallow into the path. Left unstripped, the
|
|
17
|
+
// resulting "design.md," resolves to no file, expansion is silently skipped, and
|
|
18
|
+
// the planner reasons over a one-line "Implement @design.md" with NO spec inline
|
|
19
|
+
// → it fabricates generic questions and tasks the spec never called for.
|
|
20
|
+
//
|
|
21
|
+
// Measured against a real file: the greedy token from "Implement @design.md,
|
|
22
|
+
// reuse the parser" is `design.md,`, which does not exist; stripped, `design.md`
|
|
23
|
+
// does. None of these chars are legitimate trailing characters of a doc path.
|
|
24
|
+
const MENTION_TRAILING_PUNCT = /[.,;:!?)\]}>"']+$/;
|
|
25
|
+
/** The cleaned path token of an @-mention: greedy match minus trailing prose punctuation. */
|
|
26
|
+
function mentionPath(token) {
|
|
27
|
+
return token.replace(MENTION_TRAILING_PUNCT, '');
|
|
28
|
+
}
|
|
29
|
+
/** Every distinct @-mention path in `text`, in first-seen order. */
|
|
30
|
+
function mentionPaths(text) {
|
|
31
|
+
const out = [];
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
for (const m of text.matchAll(MENTION_RE)) {
|
|
34
|
+
const rel = mentionPath(m[1]);
|
|
35
|
+
if (rel === '' || seen.has(rel))
|
|
36
|
+
continue;
|
|
37
|
+
seen.add(rel);
|
|
38
|
+
out.push(rel);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Expand any @file references in the feature text by appending each referenced
|
|
44
|
+
* file's contents, so the planning children (clarify, decompose) always see the
|
|
45
|
+
* real spec inline instead of relying on the model to open the file itself.
|
|
46
|
+
* Without this, clarify on a one-line "Implement @spec.md" tends to bail with
|
|
47
|
+
* NONE because, to the model, the request looks small and unambiguous.
|
|
48
|
+
* Unreadable mentions (typos, non-file @tokens) are left untouched; the feature
|
|
49
|
+
* is returned verbatim when nothing readable is referenced.
|
|
50
|
+
*/
|
|
51
|
+
export async function expandFeatureMentions(cwd, feature) {
|
|
52
|
+
const blocks = [];
|
|
53
|
+
for (const rel of mentionPaths(feature)) {
|
|
54
|
+
try {
|
|
55
|
+
// Normalize CRLF/CR so an @-mentioned design doc saved on Windows
|
|
56
|
+
// inlines with LF endings the downstream phase parsers expect.
|
|
57
|
+
const body = await readTextFile(path.resolve(cwd, rel));
|
|
58
|
+
if (body.trim().length > 0) {
|
|
59
|
+
blocks.push(`--- contents of ${rel} ---\n${body.trim()}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// not a readable file — leave the @token in place, skip expansion
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return blocks.length === 0 ? feature : `${feature.trim()}\n\n${blocks.join('\n\n')}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The @file references in the feature that point at a readable file on disk —
|
|
70
|
+
* the bare path tokens, deduped, in first-seen order. Unreadable mentions
|
|
71
|
+
* (typos, non-file @tokens) are dropped so we never advertise a missing file as
|
|
72
|
+
* an authoritative spec.
|
|
73
|
+
*/
|
|
74
|
+
export async function readableMentions(cwd, feature) {
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const rel of mentionPaths(feature)) {
|
|
77
|
+
try {
|
|
78
|
+
await fsp.access(path.resolve(cwd, rel));
|
|
79
|
+
out.push(rel);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// not a readable file — don't thread it into task titles
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
import type { ExtensionAPI, ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
20
20
|
import { type WidgetState } from './widget.js';
|
|
21
21
|
import { type RunTaskFn } from './gate-deps.js';
|
|
22
|
+
import { type HealthBaseline } from './health-baseline.js';
|
|
23
|
+
import { type FixContext } from './fix-context.js';
|
|
22
24
|
import { type GateDeps } from './task-gates.js';
|
|
23
25
|
import { type PhaseSeams } from './child-runner.js';
|
|
24
26
|
import type { PiModel } from '../shared/model-resolve.js';
|
|
@@ -68,14 +70,34 @@ export interface TaskRunnerOptions {
|
|
|
68
70
|
*/
|
|
69
71
|
planContext?: string;
|
|
70
72
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* implementer fixes the specific failure and re-satisfies the VERIFY block
|
|
76
|
-
* rather than blindly redoing the task. Empty/undefined on a first attempt.
|
|
73
|
+
* The /task-auto plan entry this run implements (`TaskEntry.key`). Recorded in
|
|
74
|
+
* the task file's front matter, which is where every phase reads it back —
|
|
75
|
+
* this option only exists to carry it in on a fresh start. Undefined for a
|
|
76
|
+
* bare /task, which belongs to no plan.
|
|
77
77
|
*/
|
|
78
|
-
|
|
78
|
+
planKey?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Marks this run as a verify-FAIL re-attempt (set only by the gate loop's
|
|
81
|
+
* autofix path, with `resumeId` pointing at the already-composed task):
|
|
82
|
+
* everything the gate knows about the failure — its class, the attempt
|
|
83
|
+
* number, what the deterministic probes found, any spec contradiction —
|
|
84
|
+
* rendered ahead of the delivered spec by `formatFixBanner`. Undefined on a
|
|
85
|
+
* first attempt.
|
|
86
|
+
*/
|
|
87
|
+
fixContext?: FixContext;
|
|
88
|
+
/**
|
|
89
|
+
* The repo-health baseline for the tree this task starts from
|
|
90
|
+
* (health-baseline.ts), written to `## health baseline` once the task file
|
|
91
|
+
* exists. A thunk because only the runner knows whether the file already
|
|
92
|
+
* carries one — a resumed or auto-fixed task must keep the ORIGINAL baseline,
|
|
93
|
+
* and re-running the project's statics to discard the result is the cost this
|
|
94
|
+
* saves. Absent → no section, and the verify gate establishes one lazily.
|
|
95
|
+
*
|
|
96
|
+
* It is handed the RUNNER'S ctx, not the caller's: `runSingleTask` replaces the
|
|
97
|
+
* session before the runner starts, so the ctx a call site closed over is torn
|
|
98
|
+
* down by the time this fires and its status loader would throw on it.
|
|
99
|
+
*/
|
|
100
|
+
healthBaseline?: (ctx: ExtensionCommandContext) => Promise<HealthBaseline | null>;
|
|
79
101
|
/** True when the caller awaits the implementation turn (waitForImplementation):
|
|
80
102
|
* the impl widget stays armed across the whole impl phase (incl. compaction /
|
|
81
103
|
* steer turns) and is disarmed here. False for fire-and-forget /task, where the
|
|
@@ -91,7 +113,9 @@ export declare class TaskRunner {
|
|
|
91
113
|
private readonly _sendSpec;
|
|
92
114
|
private readonly _onStart;
|
|
93
115
|
private readonly _planContext;
|
|
94
|
-
private readonly
|
|
116
|
+
private readonly _planKey;
|
|
117
|
+
private readonly _fixContext;
|
|
118
|
+
private readonly _healthBaseline;
|
|
95
119
|
/** See {@link TaskRunnerOptions.implAwaited}. */
|
|
96
120
|
private readonly _implAwaited;
|
|
97
121
|
private readonly _abort;
|
|
@@ -109,6 +133,12 @@ export declare class TaskRunner {
|
|
|
109
133
|
*/
|
|
110
134
|
private readonly _timings;
|
|
111
135
|
private _currentPhaseChildren;
|
|
136
|
+
/**
|
|
137
|
+
* Whether this run has already appended its timings block. The success path
|
|
138
|
+
* writes, and a cancel raised AFTER it still lands in the catch — which would
|
|
139
|
+
* otherwise append a second block for the same attempt.
|
|
140
|
+
*/
|
|
141
|
+
private _timingsWritten;
|
|
112
142
|
constructor(opts: TaskRunnerOptions);
|
|
113
143
|
get taskId(): string;
|
|
114
144
|
get signal(): AbortSignal;
|
|
@@ -127,12 +157,26 @@ export declare class TaskRunner {
|
|
|
127
157
|
* `runSingleTask` path on its own.
|
|
128
158
|
*/
|
|
129
159
|
run(): Promise<RunEnd>;
|
|
160
|
+
/**
|
|
161
|
+
* Store the task's repo-health baseline, ONCE. A re-entry — a resume, an
|
|
162
|
+
* autofix re-run — must keep the original: the baseline answers "what was
|
|
163
|
+
* already broken when this task started", and re-taking it on a tree the task
|
|
164
|
+
* has since edited answers a different question with the same name.
|
|
165
|
+
*/
|
|
166
|
+
private _writeHealthBaseline;
|
|
130
167
|
private _run;
|
|
131
168
|
/** Stop the phase widget — clearing both the terminal and remote surfaces —
|
|
132
169
|
* exactly once. Nulling the disposer makes repeat calls no-ops, so the
|
|
133
170
|
* failure flash that handleFailure sets after the catch isn't wiped by the
|
|
134
171
|
* finally block's call. */
|
|
135
172
|
private _disposeWidget;
|
|
173
|
+
private _writeTimings;
|
|
174
|
+
/**
|
|
175
|
+
* Append this delivery to the `## handoff` ledger. The attempt number is the
|
|
176
|
+
* count of deliveries already recorded rather than anything this process
|
|
177
|
+
* carries: a re-entry is a FRESH runner and knows nothing of the one before it.
|
|
178
|
+
*/
|
|
179
|
+
private _recordHandoff;
|
|
136
180
|
private _deliverSpec;
|
|
137
181
|
/**
|
|
138
182
|
* The spec as the implementer should receive it (Layer B). Layer A strips phantom
|
|
@@ -145,7 +189,7 @@ export declare class TaskRunner {
|
|
|
145
189
|
*/
|
|
146
190
|
private _specForDelivery;
|
|
147
191
|
}
|
|
148
|
-
export interface RunSingleTaskOptions extends Pick<TaskRunnerOptions, 'resumeId' | 'seams' | 'onStart' | 'planContext' | '
|
|
192
|
+
export interface RunSingleTaskOptions extends Pick<TaskRunnerOptions, 'resumeId' | 'seams' | 'onStart' | 'planContext' | 'planKey' | 'fixContext' | 'healthBaseline'> {
|
|
149
193
|
/** Await the session going idle after the spec is delivered, so the caller
|
|
150
194
|
* blocks until the agent has implemented it. Default false. */
|
|
151
195
|
waitForImplementation?: boolean;
|