@mjasnikovs/pi-task 0.15.0 → 0.15.2
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/dist/config/register.js
CHANGED
|
@@ -18,15 +18,15 @@ const ITEMS = [
|
|
|
18
18
|
label: 'orientation',
|
|
19
19
|
description: 'Pre-supply the project core (manifest, types, schema…) to the read-heavy research workers'
|
|
20
20
|
},
|
|
21
|
-
{
|
|
22
|
-
id: 'enforceGuidelines',
|
|
23
|
-
label: 'enforce guidelines',
|
|
24
|
-
description: 'Before each /task-auto commit, re-check the work against AGENTS.md/CLAUDE.md and fix drift'
|
|
25
|
-
},
|
|
26
21
|
{
|
|
27
22
|
id: 'verifyWork',
|
|
28
23
|
label: 'verify work',
|
|
29
|
-
description: "After each /task-auto task, RUN its spec's VERIFY block in the workspace and report a PASS/FAIL verdict"
|
|
24
|
+
description: "After each /task-auto task, RUN its spec's VERIFY block in the workspace and report a PASS/FAIL verdict (also the signal that lets 'enforce guidelines' fix safely)"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'enforceGuidelines',
|
|
28
|
+
label: 'enforce guidelines',
|
|
29
|
+
description: "Check each /task-auto commit against AGENTS.md/CLAUDE.md. Needs 'verify work' to FIX drift (fixes are reverted if they regress verification); without it, only reports violations"
|
|
30
30
|
}
|
|
31
31
|
];
|
|
32
32
|
function makeTheme(theme) {
|
|
@@ -60,13 +60,34 @@ export type { PhaseDeps };
|
|
|
60
60
|
*/
|
|
61
61
|
export declare function runPhaseChild(deps: PhaseDeps, name: string, tools: string, prompt: string): Promise<string>;
|
|
62
62
|
export declare function formatLoopHint(hit: LoopHit): string;
|
|
63
|
+
/**
|
|
64
|
+
* Terminal hint for the degrade attempt: the model has thrashed through the whole
|
|
65
|
+
* strike budget re-reading files without converging, so we strip its tools and
|
|
66
|
+
* order it to emit the deliverable NOW from what it already has. Used only by
|
|
67
|
+
* read-only analysis phases (refine) whose output is a text rewrite that never
|
|
68
|
+
* strictly required a successful read — far better to ship a best-effort spec
|
|
69
|
+
* than to hard-fail the whole /task-auto run. See countRevisits / LoopExhausted.
|
|
70
|
+
*/
|
|
71
|
+
export declare function formatDegradeHint(hit: LoopHit): string;
|
|
63
72
|
export declare function prependHint(hint: string | null, prompt: string): string;
|
|
64
73
|
/**
|
|
65
74
|
* Run a phase child with loop detection. On a detected loop, kill and re-spawn
|
|
66
75
|
* with a hint that names the offending call. Cap at MAX_LOOP_RESTARTS restarts;
|
|
67
76
|
* the (MAX_LOOP_RESTARTS+1)th loop throws LoopExhaustedError.
|
|
68
77
|
*/
|
|
69
|
-
export
|
|
78
|
+
export interface LoopGuardOptions {
|
|
79
|
+
/**
|
|
80
|
+
* When the strike budget is exhausted by loops, do NOT fail the phase. Run
|
|
81
|
+
* ONE final attempt with NO tools and a terminal hint ordering the model to
|
|
82
|
+
* emit its output from what it already has. Only safe for phases whose
|
|
83
|
+
* deliverable is a pure text rewrite that never strictly required a read
|
|
84
|
+
* (refine) — a hard-fail there kills the whole /task-auto run for a model
|
|
85
|
+
* that simply over-explored. Research/location phases must NOT enable this:
|
|
86
|
+
* their output depends on real reads, so a no-tools fallback would fabricate.
|
|
87
|
+
*/
|
|
88
|
+
degradeOnExhaustion?: boolean;
|
|
89
|
+
}
|
|
90
|
+
export declare function runPhaseWithLoopGuard(deps: PhaseDeps, name: string, tools: string, buildPrompt: (loopHint: string | null) => string, opts?: LoopGuardOptions): Promise<string>;
|
|
70
91
|
/**
|
|
71
92
|
* Run a child up to twice; the second attempt gets `emphasized=true` to escalate
|
|
72
93
|
* the prompt. On success, return the validator's value; on two failures, throw
|
|
@@ -160,6 +160,22 @@ export function formatLoopHint(hit) {
|
|
|
160
160
|
+ `stuck in a loop. Avoid repeating that exact call; if you've already seen its result, `
|
|
161
161
|
+ `work from memory or pick a different angle.]`);
|
|
162
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Terminal hint for the degrade attempt: the model has thrashed through the whole
|
|
165
|
+
* strike budget re-reading files without converging, so we strip its tools and
|
|
166
|
+
* order it to emit the deliverable NOW from what it already has. Used only by
|
|
167
|
+
* read-only analysis phases (refine) whose output is a text rewrite that never
|
|
168
|
+
* strictly required a successful read — far better to ship a best-effort spec
|
|
169
|
+
* than to hard-fail the whole /task-auto run. See countRevisits / LoopExhausted.
|
|
170
|
+
*/
|
|
171
|
+
export function formatDegradeHint(hit) {
|
|
172
|
+
return (`[SYSTEM NOTE: You called ${hit.call.name}(${JSON.stringify(hit.call.args)}) `
|
|
173
|
+
+ `repeatedly and made no forward progress — you are stuck re-reading the same files. `
|
|
174
|
+
+ `You now have NO tools: you cannot read, grep, list, or open anything further. `
|
|
175
|
+
+ `STOP exploring. Produce the COMPLETE required output IMMEDIATELY, using only the task `
|
|
176
|
+
+ `description and what you have already seen. Emit the full structured result now — do not `
|
|
177
|
+
+ `ask to read more, do not explain, just output the final answer.]`);
|
|
178
|
+
}
|
|
163
179
|
export function prependHint(hint, prompt) {
|
|
164
180
|
return hint === null ? prompt : `${hint}\n\n${prompt}`;
|
|
165
181
|
}
|
|
@@ -172,12 +188,7 @@ async function appendLoopEvent(cwd, taskId, phase, hit, strike, outcome) {
|
|
|
172
188
|
const next = existing ? `${existing}\n${line}` : line;
|
|
173
189
|
await setTaskSection(cwd, taskId, 'loop events', next);
|
|
174
190
|
}
|
|
175
|
-
|
|
176
|
-
* Run a phase child with loop detection. On a detected loop, kill and re-spawn
|
|
177
|
-
* with a hint that names the offending call. Cap at MAX_LOOP_RESTARTS restarts;
|
|
178
|
-
* the (MAX_LOOP_RESTARTS+1)th loop throws LoopExhaustedError.
|
|
179
|
-
*/
|
|
180
|
-
export async function runPhaseWithLoopGuard(deps, name, tools, buildPrompt) {
|
|
191
|
+
export async function runPhaseWithLoopGuard(deps, name, tools, buildPrompt, opts = {}) {
|
|
181
192
|
const loopHistory = [];
|
|
182
193
|
// Carries the correction hint (loop OR leaked-tool-call) into the next strike.
|
|
183
194
|
let nextHint = null;
|
|
@@ -192,9 +203,14 @@ export async function runPhaseWithLoopGuard(deps, name, tools, buildPrompt) {
|
|
|
192
203
|
if (r.loopHit) {
|
|
193
204
|
const isLastStrike = strike === MAX_LOOP_RESTARTS;
|
|
194
205
|
loopHistory.push(r.loopHit);
|
|
195
|
-
|
|
196
|
-
|
|
206
|
+
const lastOutcome = opts.degradeOnExhaustion ? 'degraded — no-tools final attempt' : 'phase failed';
|
|
207
|
+
await appendLoopEvent(deps.cwd, deps.taskId, name, r.loopHit, strike + 1, isLastStrike ? lastOutcome : 'restarted with hint');
|
|
208
|
+
if (isLastStrike) {
|
|
209
|
+
if (opts.degradeOnExhaustion) {
|
|
210
|
+
return await runDegradedFinalAttempt(deps, name, buildPrompt, r.loopHit, loopHistory);
|
|
211
|
+
}
|
|
197
212
|
throw new LoopExhaustedError(name, loopHistory);
|
|
213
|
+
}
|
|
198
214
|
nextHint = formatLoopHint(r.loopHit);
|
|
199
215
|
continue;
|
|
200
216
|
}
|
|
@@ -236,6 +252,24 @@ export async function runPhaseWithLoopGuard(deps, name, tools, buildPrompt) {
|
|
|
236
252
|
}
|
|
237
253
|
throw new LoopExhaustedError(name, loopHistory);
|
|
238
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Final degrade attempt after the loop budget is spent: re-spawn the child with
|
|
257
|
+
* NO tools and a terminal hint, so a model that thrashed re-reading files is
|
|
258
|
+
* forced to emit its deliverable from what it already has. With no tools there
|
|
259
|
+
* are no tool calls, so no loop can recur — the only failure modes left are a
|
|
260
|
+
* dead turn (non-zero exit, model error) or empty output, any of which fall back
|
|
261
|
+
* to the original LoopExhaustedError so the phase still fails honestly when even
|
|
262
|
+
* the degrade produces nothing.
|
|
263
|
+
*/
|
|
264
|
+
async function runDegradedFinalAttempt(deps, name, buildPrompt, hit, loopHistory) {
|
|
265
|
+
deps.logDebug?.(`${name}: loop budget exhausted — degrading to a no-tools final attempt`);
|
|
266
|
+
const r = await runChild(deps.cwd, '', // --no-tools: the model cannot read/grep/list, only answer
|
|
267
|
+
buildPrompt(formatDegradeHint(hit)), deps.signal, deps.onChildOutput, deps.onContextUsage, undefined, deps.spawn);
|
|
268
|
+
if (r.exitCode !== 0 || r.modelError || r.text.trim().length === 0) {
|
|
269
|
+
throw new LoopExhaustedError(name, loopHistory);
|
|
270
|
+
}
|
|
271
|
+
return r.text;
|
|
272
|
+
}
|
|
239
273
|
/**
|
|
240
274
|
* Run a child up to twice; the second attempt gets `emphasized=true` to escalate
|
|
241
275
|
* the prompt. On success, return the validator's value; on two failures, throw
|
package/dist/task/phases.js
CHANGED
|
@@ -62,7 +62,15 @@ export function replaceToolingWithVerified(research, verifiedCommands) {
|
|
|
62
62
|
return replaced;
|
|
63
63
|
}
|
|
64
64
|
// ─── Phase functions ─────────────────────────────────────────────────────────
|
|
65
|
-
export const phaseRefine = (deps, raw, planContext) => runPhaseWithLoopGuard(deps, 'refine', 'read', hint => prependHint(hint, appendNoThink(REFINE_PROMPT(raw, planContext)))
|
|
65
|
+
export const phaseRefine = (deps, raw, planContext) => runPhaseWithLoopGuard(deps, 'refine', 'read', hint => prependHint(hint, appendNoThink(REFINE_PROMPT(raw, planContext))),
|
|
66
|
+
// refine's deliverable is a 4-section text rewrite that never strictly
|
|
67
|
+
// needs a successful read — on a test-writing task against a large
|
|
68
|
+
// existing codebase the model over-explores (re-reads source hunting for
|
|
69
|
+
// the impl) and burns the loop budget. Degrade to a no-tools final
|
|
70
|
+
// attempt instead of hard-failing the whole run. See TASK_0016 (mx5):
|
|
71
|
+
// refine looped 3×/resume forever; the deliverable was always producible
|
|
72
|
+
// from the title + design doc alone.
|
|
73
|
+
{ degradeOnExhaustion: true });
|
|
66
74
|
export async function phaseVerifyTooling(deps, research) {
|
|
67
75
|
const commands = extractToolingCommands(research);
|
|
68
76
|
if (!commands || commands.length === 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"description": "Deterministic spec-orchestration for local models, with a bundled real-time remote web view and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|