@mjasnikovs/pi-task 0.14.15 → 0.14.17
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/task/auto-orchestrator.d.ts +13 -0
- package/dist/task/auto-orchestrator.js +158 -65
- package/dist/task/orchestrator.d.ts +11 -1
- package/dist/task/orchestrator.js +23 -7
- package/dist/task/task-io.js +9 -1
- package/dist/task/verify-resolution.d.ts +75 -0
- package/dist/task/verify-resolution.js +190 -0
- package/dist/task/widget.d.ts +3 -2
- package/dist/task/widget.js +2 -1
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import type { RunSingleTaskResult } from './orchestrator.js';
|
|
|
3
3
|
import { type CommitResult } from './auto-commit.js';
|
|
4
4
|
import { type EnforceOutcome } from './enforce-guidelines.js';
|
|
5
5
|
import { type VerifyOutcome } from './verify-work.js';
|
|
6
|
+
import { type ResolutionOutcome } from './verify-resolution.js';
|
|
6
7
|
/**
|
|
7
8
|
* Injectable seams so the planner and loop are testable without spawning pi.
|
|
8
9
|
* `runChild` is used by planAuto; `runTask` is used by runAutoLoop.
|
|
@@ -16,6 +17,9 @@ export interface AutoDeps {
|
|
|
16
17
|
onStart?: (taskId: string) => void | Promise<void>;
|
|
17
18
|
/** Scope fence naming the sibling steps, forwarded into refine. */
|
|
18
19
|
planContext?: string;
|
|
20
|
+
/** Verify-FAIL autofix re-attempt: prepended to the delivered spec as
|
|
21
|
+
* a RE-ATTEMPT banner so the re-run fixes that specific failure. */
|
|
22
|
+
fixInstruction?: string;
|
|
19
23
|
}) => Promise<RunSingleTaskResult>;
|
|
20
24
|
/** Snapshot the working tree into one commit after a task passes. */
|
|
21
25
|
commit: (cwd: string, message: string) => Promise<CommitResult>;
|
|
@@ -40,6 +44,15 @@ export interface AutoDeps {
|
|
|
40
44
|
* it). Needs the inner taskId to read that task's spec.
|
|
41
45
|
*/
|
|
42
46
|
verify?: (ctx: ExtensionCommandContext, cwd: string, taskTitle: string, taskId: string) => Promise<VerifyOutcome>;
|
|
47
|
+
/**
|
|
48
|
+
* After a verify FAIL, research whether to recommend AUTOFIX (re-do the work)
|
|
49
|
+
* or ACCEPT (the gate misjudged a good artifact) — read-only, same local
|
|
50
|
+
* model. Only sets which card the picker tints RECOMMENDED; the user always
|
|
51
|
+
* makes the final call. Optional: absent in tests or when verify is off, in
|
|
52
|
+
* which case the loop defaults the recommendation to AUTOFIX. Needs the inner
|
|
53
|
+
* taskId (to read the spec) and the gate's failure reason.
|
|
54
|
+
*/
|
|
55
|
+
recommend?: (ctx: ExtensionCommandContext, cwd: string, taskTitle: string, taskId: string, failReason: string) => Promise<ResolutionOutcome>;
|
|
43
56
|
}
|
|
44
57
|
/**
|
|
45
58
|
* Expand any @file references in the feature text by appending each referenced
|
|
@@ -17,6 +17,7 @@ import { writeTaskFile, readTaskFile, updateTaskFrontMatter, taskFilePath, tasks
|
|
|
17
17
|
import { gitCommitAll } from './auto-commit.js';
|
|
18
18
|
import { runGuidelineEnforcement, classifyEnforceChildFailure } from './enforce-guidelines.js';
|
|
19
19
|
import { runWorkVerification, extractSpecForVerification } from './verify-work.js';
|
|
20
|
+
import { researchResolution, resolutionOptions, classifyResolutionAnswer } from './verify-resolution.js';
|
|
20
21
|
import { runWorker } from '../workers/pi-worker-core.js';
|
|
21
22
|
import { findPhantomImports, rewritePhantomSpecifiers } from '../workers/phantom-imports.js';
|
|
22
23
|
import { runPhaseChild, prependHint, formatLoopHint, USER_CANCELLED } from './child-runner.js';
|
|
@@ -344,6 +345,61 @@ function defaultDeps(ctx, cwd, signal, title) {
|
|
|
344
345
|
let lastLine;
|
|
345
346
|
let contextUsage;
|
|
346
347
|
const parentContextWindow = getParentContextWindow(ctx);
|
|
348
|
+
// Shared runner for the per-task GATE children (verify + post-FAIL recommend).
|
|
349
|
+
// Both are read-only passes of the same local model that must run to
|
|
350
|
+
// completion: unguarded (no wall-clock timeout, exact-match loop guard only,
|
|
351
|
+
// path-revisit disabled because re-running the same check is the job), with a
|
|
352
|
+
// status widget and a per-gate debug log. Returns the closure
|
|
353
|
+
// runWorkVerification / researchResolution expect as `runChild`.
|
|
354
|
+
const makeGateChild = (gateCtx, cwd2, taskTitle, kind, logFile) => async (tools, prompt, sig) => {
|
|
355
|
+
lastLine = undefined;
|
|
356
|
+
contextUsage = undefined;
|
|
357
|
+
const startedAt = Date.now();
|
|
358
|
+
const logPath = path.join(tasksDir(cwd2), logFile);
|
|
359
|
+
const log = (msg) => {
|
|
360
|
+
void fsp.appendFile(logPath, `${new Date().toISOString()} ${msg}\n`).catch(() => { });
|
|
361
|
+
};
|
|
362
|
+
log(`=== ${kind} start: ${taskTitle} ===`);
|
|
363
|
+
const stopLoader = startAutoLoader(gateCtx, () => ({
|
|
364
|
+
title: taskTitle,
|
|
365
|
+
kind,
|
|
366
|
+
step: kind,
|
|
367
|
+
stepNum: 1,
|
|
368
|
+
stepTotal: 1,
|
|
369
|
+
startedAt,
|
|
370
|
+
lastLine,
|
|
371
|
+
contextUsage
|
|
372
|
+
}));
|
|
373
|
+
try {
|
|
374
|
+
const r = await runWorker({
|
|
375
|
+
prompt,
|
|
376
|
+
cwd: cwd2,
|
|
377
|
+
signal: sig,
|
|
378
|
+
tools,
|
|
379
|
+
timeoutMs: 0,
|
|
380
|
+
loop: { pathThreshold: Number.POSITIVE_INFINITY },
|
|
381
|
+
onLine: line => {
|
|
382
|
+
lastLine = line;
|
|
383
|
+
log(line);
|
|
384
|
+
},
|
|
385
|
+
onContextUsage: snapshot => {
|
|
386
|
+
contextUsage = resolveContextUsage(snapshot, contextUsage, parentContextWindow);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
if (r.loopHit) {
|
|
390
|
+
log(`=== ${kind} LOOP WARNING — ${formatLoopHint(r.loopHit)} ===`);
|
|
391
|
+
gateCtx.ui.notify(`${taskTitle}: ${kind} worker looped past the nudges — continuing (not blocked).`, 'warning');
|
|
392
|
+
}
|
|
393
|
+
const failure = classifyEnforceChildFailure(r);
|
|
394
|
+
log(failure ? `=== ${kind} end: FAIL — ${failure} ===` : `=== ${kind} end: ok ===`);
|
|
395
|
+
if (failure)
|
|
396
|
+
throw new Error(failure);
|
|
397
|
+
return r.text;
|
|
398
|
+
}
|
|
399
|
+
finally {
|
|
400
|
+
stopLoader();
|
|
401
|
+
}
|
|
402
|
+
};
|
|
347
403
|
const phaseDeps = {
|
|
348
404
|
cwd,
|
|
349
405
|
taskId: '',
|
|
@@ -384,7 +440,8 @@ function defaultDeps(ctx, cwd, signal, title) {
|
|
|
384
440
|
waitForImplementation: true,
|
|
385
441
|
resumeId: opts?.resumeId,
|
|
386
442
|
onStart: opts?.onStart,
|
|
387
|
-
planContext: opts?.planContext
|
|
443
|
+
planContext: opts?.planContext,
|
|
444
|
+
fixInstruction: opts?.fixInstruction
|
|
388
445
|
}),
|
|
389
446
|
commit: (cwd2, message) => getConfig().autoCommit ?
|
|
390
447
|
gitCommitAll(cwd2, message, signal)
|
|
@@ -502,70 +559,54 @@ function defaultDeps(ctx, cwd, signal, title) {
|
|
|
502
559
|
cwd: cwd2,
|
|
503
560
|
signal,
|
|
504
561
|
spec,
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
stepNum: 1,
|
|
527
|
-
stepTotal: 1,
|
|
528
|
-
startedAt,
|
|
529
|
-
lastLine,
|
|
530
|
-
contextUsage
|
|
531
|
-
}));
|
|
532
|
-
try {
|
|
533
|
-
const r = await runWorker({
|
|
534
|
-
prompt,
|
|
535
|
-
cwd: cwd2,
|
|
536
|
-
signal: sig,
|
|
537
|
-
tools,
|
|
538
|
-
timeoutMs: 0,
|
|
539
|
-
loop: { pathThreshold: Number.POSITIVE_INFINITY },
|
|
540
|
-
onLine: line => {
|
|
541
|
-
lastLine = line;
|
|
542
|
-
logVerify(line);
|
|
543
|
-
},
|
|
544
|
-
onContextUsage: snapshot => {
|
|
545
|
-
contextUsage = resolveContextUsage(snapshot, contextUsage, parentContextWindow);
|
|
546
|
-
}
|
|
547
|
-
});
|
|
548
|
-
if (r.loopHit) {
|
|
549
|
-
logVerify(`=== verify LOOP WARNING — ${formatLoopHint(r.loopHit)} ===`);
|
|
550
|
-
verifyCtx.ui.notify(`${taskTitle}: verify worker looped past the nudges — continuing (not blocked).`, 'warning');
|
|
551
|
-
}
|
|
552
|
-
const failure = classifyEnforceChildFailure(r);
|
|
553
|
-
logVerify(failure ?
|
|
554
|
-
`=== verify end: FAIL — ${failure} ===`
|
|
555
|
-
: '=== verify end: verdict captured ===');
|
|
556
|
-
if (failure)
|
|
557
|
-
throw new Error(failure);
|
|
558
|
-
return r.text;
|
|
559
|
-
}
|
|
560
|
-
finally {
|
|
561
|
-
stopLoader();
|
|
562
|
-
}
|
|
563
|
-
}
|
|
562
|
+
runChild: makeGateChild(verifyCtx, cwd2, taskTitle, 'verify', 'verify-debug.log')
|
|
563
|
+
});
|
|
564
|
+
},
|
|
565
|
+
recommend: async (recCtx, cwd2, taskTitle, taskId, failReason) => {
|
|
566
|
+
// Read the same composed spec the verify gate judged against, so the
|
|
567
|
+
// recommendation reasons over the real contract (a missing spec is
|
|
568
|
+
// unusual here — verify already ran — but degrade to the bare title).
|
|
569
|
+
let spec;
|
|
570
|
+
try {
|
|
571
|
+
const { body } = await readTaskFile(cwd2, taskId);
|
|
572
|
+
spec = extractSpecForVerification(body) ?? taskTitle;
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
spec = taskTitle;
|
|
576
|
+
}
|
|
577
|
+
return researchResolution({
|
|
578
|
+
cwd: cwd2,
|
|
579
|
+
signal,
|
|
580
|
+
spec,
|
|
581
|
+
failReason,
|
|
582
|
+
runChild: makeGateChild(recCtx, cwd2, taskTitle, 'recommend', 'verify-debug.log')
|
|
564
583
|
});
|
|
565
584
|
}
|
|
566
585
|
};
|
|
567
586
|
}
|
|
568
587
|
// ─── Loop ────────────────────────────────────────────────────────────────────
|
|
588
|
+
/**
|
|
589
|
+
* Show the boxed two-choice picker after a verify FAIL and return what the user
|
|
590
|
+
* decided. The model-recommended card is placed first so the renderer tints it
|
|
591
|
+
* green; the user ALWAYS makes the final call (there is no auto-pick). Mirrors the
|
|
592
|
+
* clarify/grill dialog: the same SessionUI.ask races the local boxed picker
|
|
593
|
+
* against a remote answer, with the two actions also surfaced as remote buttons.
|
|
594
|
+
*/
|
|
595
|
+
async function askVerifyResolution(ctx, title, failReason, rec) {
|
|
596
|
+
const options = resolutionOptions(rec.recommend);
|
|
597
|
+
const question = `Verification FAILED for "${title}".\n\n${failReason}\n\n`
|
|
598
|
+
+ `Recommended: ${rec.recommend.toUpperCase()} — ${rec.rationale}`;
|
|
599
|
+
const answer = await new SessionUI(ctx).ask({
|
|
600
|
+
localTitle: 'Verification failed — how should pi proceed?',
|
|
601
|
+
displayQuestion: question,
|
|
602
|
+
question,
|
|
603
|
+
recommended: options[0].label,
|
|
604
|
+
recommended2: options[1].label,
|
|
605
|
+
allowSkip: false,
|
|
606
|
+
options
|
|
607
|
+
});
|
|
608
|
+
return classifyResolutionAnswer(answer);
|
|
609
|
+
}
|
|
569
610
|
let cancelRequested = false;
|
|
570
611
|
let autoRunning = false;
|
|
571
612
|
export function requestAutoCancel() {
|
|
@@ -678,12 +719,64 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
678
719
|
// task has no composed spec to verify against.
|
|
679
720
|
if (deps.verify) {
|
|
680
721
|
active.ui.notify(`${id}: verifying "${next.title}"…`, 'info');
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
722
|
+
let verified = await deps.verify(active, cwd, next.title, res.taskId);
|
|
723
|
+
// A FAIL no longer dead-stops the run. The user is offered a boxed
|
|
724
|
+
// two-choice picker — AUTOFIX (re-run the implementation turn against
|
|
725
|
+
// the failure, then re-verify) or ACCEPT (the gate misjudged a good
|
|
726
|
+
// artifact; override and proceed). A read-only research pass picks
|
|
727
|
+
// which card is recommended, but the USER always decides — there is
|
|
728
|
+
// no attempt cap: AUTOFIX loops straight back to the gate as many
|
|
729
|
+
// times as the user keeps choosing it. The run only leaves the loop
|
|
730
|
+
// when the work verifies, the user ACCEPTs, or the user dismisses
|
|
731
|
+
// the picker (which pauses the run, resumable).
|
|
732
|
+
while (!verified.ok) {
|
|
733
|
+
const failReason = verified.reason ?? 'did not verify';
|
|
734
|
+
const rec = deps.recommend ?
|
|
735
|
+
await deps.recommend(active, cwd, next.title, res.taskId, failReason)
|
|
736
|
+
: { recommend: 'autofix', rationale: failReason };
|
|
737
|
+
const choice = await askVerifyResolution(active, next.title, failReason, rec);
|
|
738
|
+
if (choice.action === 'cancel') {
|
|
739
|
+
await updateTaskFrontMatter(cwd, id, { state: 'failed' });
|
|
740
|
+
announceDone(active, `${id} paused at "${next.title}" — verification failed and you dismissed the choice; resume with /task-auto-resume.`, 'warning');
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (choice.action === 'accept') {
|
|
744
|
+
active.ui.notify(`${id}: accepted "${next.title}" despite verify FAIL (${failReason.slice(0, 120)}) — proceeding.`, 'warning');
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
// AUTOFIX: re-run the implementation turn with the failure (and
|
|
748
|
+
// any typed guidance) prepended as a RE-ATTEMPT banner, then
|
|
749
|
+
// re-verify — looping back to the gate. No cap: the user decides
|
|
750
|
+
// when to stop (by accepting or dismissing the next picker).
|
|
751
|
+
active.ui.notify(`${id}: autofixing "${next.title}"…`, 'info');
|
|
752
|
+
const fixInstruction = choice.guidance ?
|
|
753
|
+
`${failReason}\n\nUser guidance: ${choice.guidance}`
|
|
754
|
+
: failReason;
|
|
755
|
+
const fixRes = await deps.runTask(active, cwd, next.title, {
|
|
756
|
+
resumeId: res.taskId,
|
|
757
|
+
planContext: buildScopeFence(entries.map(e => e.title), next.index),
|
|
758
|
+
fixInstruction
|
|
759
|
+
});
|
|
760
|
+
active = fixRes.ctx ?? active;
|
|
761
|
+
if (fixRes.sessionCancelled) {
|
|
762
|
+
announceDone(active, `${id} paused — could not start a session for autofix. Run /task-auto-resume to retry.`, 'warning');
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
if (fixRes.interrupted) {
|
|
766
|
+
announceDone(active, `${id} paused at "${next.title}" — resume with /task-auto-resume.`, 'warning');
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (!fixRes.ok) {
|
|
770
|
+
await updateTaskFrontMatter(cwd, id, { state: 'failed' });
|
|
771
|
+
const why = fixRes.reason ? ` — ${fixRes.reason.slice(0, 160)}` : '';
|
|
772
|
+
announceDone(active, `${id} stopped at "${next.title}"${why} — fix and run /task-auto-resume.`, 'error');
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
// Resume reuses the same inner task id, so res.taskId is stable.
|
|
776
|
+
verified = await deps.verify(active, cwd, next.title, res.taskId);
|
|
686
777
|
}
|
|
778
|
+
// Loop exited because the work verified OR the user accepted the
|
|
779
|
+
// artifact — either way fall through to check-off/commit/enforce.
|
|
687
780
|
}
|
|
688
781
|
// res.ok === true means runner.run() completed, so res.taskId is the
|
|
689
782
|
// allocated TASK_NNNN id (never empty here). checkOffTask tolerates an
|
|
@@ -27,6 +27,7 @@ export declare class TaskRunner {
|
|
|
27
27
|
private readonly _sendSpec;
|
|
28
28
|
private readonly _onStart;
|
|
29
29
|
private readonly _planContext;
|
|
30
|
+
private readonly _fixInstruction;
|
|
30
31
|
private readonly _abort;
|
|
31
32
|
private readonly _startedAt;
|
|
32
33
|
private readonly _widgetState;
|
|
@@ -42,7 +43,7 @@ export declare class TaskRunner {
|
|
|
42
43
|
*/
|
|
43
44
|
private readonly _timings;
|
|
44
45
|
private _currentPhaseChildren;
|
|
45
|
-
constructor(ctx: ExtensionCommandContext, cwd: string, rawPrompt: string, resumeId?: string, sendSpec?: (spec: string) => Promise<void>, spawnFn?: SpawnFn, onStart?: (taskId: string) => void | Promise<void>, planContext?: string);
|
|
46
|
+
constructor(ctx: ExtensionCommandContext, cwd: string, rawPrompt: string, resumeId?: string, sendSpec?: (spec: string) => Promise<void>, spawnFn?: SpawnFn, onStart?: (taskId: string) => void | Promise<void>, planContext?: string, fixInstruction?: string);
|
|
46
47
|
get taskId(): string;
|
|
47
48
|
get signal(): AbortSignal;
|
|
48
49
|
/** Return the current widget state, or null if not started. */
|
|
@@ -102,6 +103,15 @@ export interface RunSingleTaskOptions {
|
|
|
102
103
|
* a bare /task leaves it undefined and the refine prompt is unchanged.
|
|
103
104
|
*/
|
|
104
105
|
planContext?: string;
|
|
106
|
+
/**
|
|
107
|
+
* Marks this run as a verify-FAIL re-attempt. When set (only by /task-auto's
|
|
108
|
+
* autofix path, with `resumeId` pointing at the already-composed task), the
|
|
109
|
+
* text — the verify gate's failure reason plus any guidance the user typed —
|
|
110
|
+
* is prepended to the delivered spec as a RE-ATTEMPT banner, so the
|
|
111
|
+
* implementer fixes the specific failure and re-satisfies the VERIFY block
|
|
112
|
+
* rather than blindly redoing the task. Empty/undefined on a first attempt.
|
|
113
|
+
*/
|
|
114
|
+
fixInstruction?: string;
|
|
105
115
|
}
|
|
106
116
|
export interface RunSingleTaskResult {
|
|
107
117
|
taskId: string;
|
|
@@ -53,6 +53,7 @@ export class TaskRunner {
|
|
|
53
53
|
_sendSpec;
|
|
54
54
|
_onStart;
|
|
55
55
|
_planContext;
|
|
56
|
+
_fixInstruction;
|
|
56
57
|
_abort = new AbortController();
|
|
57
58
|
_startedAt;
|
|
58
59
|
_widgetState;
|
|
@@ -68,7 +69,7 @@ export class TaskRunner {
|
|
|
68
69
|
*/
|
|
69
70
|
_timings = [];
|
|
70
71
|
_currentPhaseChildren = null;
|
|
71
|
-
constructor(ctx, cwd, rawPrompt, resumeId, sendSpec, spawnFn, onStart, planContext) {
|
|
72
|
+
constructor(ctx, cwd, rawPrompt, resumeId, sendSpec, spawnFn, onStart, planContext, fixInstruction) {
|
|
72
73
|
this._ctx = ctx;
|
|
73
74
|
this._cwd = cwd;
|
|
74
75
|
this._rawPrompt = rawPrompt;
|
|
@@ -76,6 +77,7 @@ export class TaskRunner {
|
|
|
76
77
|
this._sendSpec = sendSpec;
|
|
77
78
|
this._onStart = onStart;
|
|
78
79
|
this._planContext = planContext;
|
|
80
|
+
this._fixInstruction = fixInstruction;
|
|
79
81
|
this._startedAt = Date.now();
|
|
80
82
|
// We'll populate id/title/phase lazily in run().
|
|
81
83
|
// Placeholder — real values set in run().
|
|
@@ -288,11 +290,25 @@ export class TaskRunner {
|
|
|
288
290
|
*/
|
|
289
291
|
_specForDelivery() {
|
|
290
292
|
const phantoms = findDeliveryPhantoms(this._pc.spec, this._cwd);
|
|
291
|
-
const
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
293
|
+
const apiBanner = formatApiOverrideBanner(phantoms);
|
|
294
|
+
if (apiBanner) {
|
|
295
|
+
this._deps.logDebug?.(`impl-handoff API override banner prepended for: ${phantoms.map(p => p.spec).join(', ')}`);
|
|
296
|
+
}
|
|
297
|
+
// A verify-FAIL re-attempt: the work was already implemented once and the
|
|
298
|
+
// verification gate rejected it. Lead with WHY so the model fixes that
|
|
299
|
+
// specific failure and re-satisfies the VERIFY block, rather than redoing
|
|
300
|
+
// the task from scratch (or repeating the same mistake).
|
|
301
|
+
let fixBanner = '';
|
|
302
|
+
if (this._fixInstruction && this._fixInstruction.trim().length > 0) {
|
|
303
|
+
this._deps.logDebug?.('impl-handoff RE-ATTEMPT banner prepended (verify FAIL fix)');
|
|
304
|
+
fixBanner =
|
|
305
|
+
'RE-ATTEMPT — your previous implementation of this task FAILED verification.\n'
|
|
306
|
+
+ "Fix the cause below, then make the spec's VERIFY block pass. Do NOT start over;\n"
|
|
307
|
+
+ 'change only what is needed to resolve the failure.\n\n'
|
|
308
|
+
+ `VERIFICATION FAILURE:\n${this._fixInstruction.trim()}`;
|
|
309
|
+
}
|
|
310
|
+
const banners = [fixBanner, apiBanner].filter(b => b && b.length > 0).join('\n\n');
|
|
311
|
+
return banners ? `${banners}\n\n${this._pc.spec}` : this._pc.spec;
|
|
296
312
|
}
|
|
297
313
|
}
|
|
298
314
|
/** Dialog copy for the post-interrupt steering prompt. */
|
|
@@ -394,7 +410,7 @@ export async function runSingleTask(ctx, cwd, rawPrompt, opts = {}) {
|
|
|
394
410
|
if (!interrupted)
|
|
395
411
|
implError = implementationError(newCtx);
|
|
396
412
|
}
|
|
397
|
-
}, opts.spawnFn, opts.onStart, opts.planContext);
|
|
413
|
+
}, opts.spawnFn, opts.onStart, opts.planContext, opts.fixInstruction);
|
|
398
414
|
await runner.run();
|
|
399
415
|
taskId = runner.taskId;
|
|
400
416
|
}
|
package/dist/task/task-io.js
CHANGED
|
@@ -90,7 +90,15 @@ export async function setTaskSection(cwd, id, heading, content) {
|
|
|
90
90
|
const re = sectionRegex(heading);
|
|
91
91
|
let next;
|
|
92
92
|
if (re.test(body)) {
|
|
93
|
-
|
|
93
|
+
// Use a replacer FUNCTION, not a replacement string: `content` is
|
|
94
|
+
// untrusted model output and may contain `$`-sequences (`$\``, `$'`,
|
|
95
|
+
// `$&`, `$1`, `$$`). In a replacement string those are special patterns
|
|
96
|
+
// String.prototype.replace would expand — e.g. a spec line with the
|
|
97
|
+
// regex `^\+[1-9]\d{1,14}$` ends in the literal `` $` `` (dollar +
|
|
98
|
+
// closing backtick), which expands to "everything before the match" and
|
|
99
|
+
// silently mangles/truncates the stored section (dropping ACCEPTANCE /
|
|
100
|
+
// VERIFY), making the spec unrunnable. A function return is literal.
|
|
101
|
+
next = body.replace(re, (_m, p1) => `${p1}\n${content.trim()}\n\n`);
|
|
94
102
|
}
|
|
95
103
|
else {
|
|
96
104
|
const sep = body.endsWith('\n\n') ? ''
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** Same read-only contract as the verify pass: observe, never edit. */
|
|
2
|
+
declare const RESOLUTION_TOOLS = "read,bash";
|
|
3
|
+
/** Which card the picker tints RECOMMENDED. */
|
|
4
|
+
export type ResolutionRecommendation = 'autofix' | 'accept';
|
|
5
|
+
export interface ResolutionOutcome {
|
|
6
|
+
/** The card to recommend. Defaults to 'autofix' (conservative: re-do, don't
|
|
7
|
+
* bless) when the research could not run or emitted no verdict. */
|
|
8
|
+
recommend: ResolutionRecommendation;
|
|
9
|
+
/** Short, human-readable rationale shown under the picker. Always set. */
|
|
10
|
+
rationale: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Build the resolution research child's prompt. Pure, so the wording is
|
|
14
|
+
* unit-tested without spawning pi. The contract: investigate the real workspace
|
|
15
|
+
* and end on exactly one RESOLUTION verdict line.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildResolutionPrompt(spec: string, failReason: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Parse the child's recommendation. Scans for the LAST
|
|
20
|
+
* `VERIFY-RESOLUTION: AUTOFIX|ACCEPT` marker (the model reasons before
|
|
21
|
+
* concluding, and bash output may echo these words, so a distinct token and
|
|
22
|
+
* last-match-wins both matter).
|
|
23
|
+
*
|
|
24
|
+
* No marker → recommend AUTOFIX (conservative: re-do rather than bless), with a
|
|
25
|
+
* rationale that says the research was inconclusive.
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseResolutionVerdict(text: string): ResolutionOutcome;
|
|
28
|
+
export declare const ACCEPT_VALUE = "accept";
|
|
29
|
+
export declare const AUTOFIX_VALUE = "autofix";
|
|
30
|
+
export declare const ACCEPT_LABEL = "Accept \u2014 keep the artifact as-is and continue";
|
|
31
|
+
export declare const AUTOFIX_LABEL = "Autofix \u2014 re-run the task to fix it, then verify again";
|
|
32
|
+
export interface ResolutionChoice {
|
|
33
|
+
/** What the user decided. 'cancel' = dismissed the picker (pause, resumable). */
|
|
34
|
+
action: 'accept' | 'autofix' | 'cancel';
|
|
35
|
+
/** Free-text guidance the user typed instead of picking a card. Folded into
|
|
36
|
+
* the autofix instruction so a re-run can act on it. Only set with 'autofix'. */
|
|
37
|
+
guidance?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The two cards for the picker, with the model-recommended one FIRST so the boxed
|
|
41
|
+
* renderer tints it green (it marks index 0 as recommended). Values are the bare
|
|
42
|
+
* tokens; remote buttons use the labels — {@link classifyResolutionAnswer}
|
|
43
|
+
* accepts both.
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolutionOptions(recommend: ResolutionRecommendation): {
|
|
46
|
+
label: string;
|
|
47
|
+
value: string;
|
|
48
|
+
recommended: boolean;
|
|
49
|
+
}[];
|
|
50
|
+
/**
|
|
51
|
+
* Map a picker answer to an action. Handles the card value tokens, the remote
|
|
52
|
+
* button labels, an empty/undefined dismissal (cancel), and arbitrary free text
|
|
53
|
+
* typed via the fallback — which becomes an AUTOFIX with that text as guidance
|
|
54
|
+
* (the user is steering the re-run rather than blessing the artifact).
|
|
55
|
+
*/
|
|
56
|
+
export declare function classifyResolutionAnswer(answer: string | undefined): ResolutionChoice;
|
|
57
|
+
export interface ResolutionDeps {
|
|
58
|
+
cwd: string;
|
|
59
|
+
signal?: AbortSignal;
|
|
60
|
+
/** The composed spec the task was verified against. */
|
|
61
|
+
spec: string;
|
|
62
|
+
/** The verify gate's FAIL reason (VerifyOutcome.reason). */
|
|
63
|
+
failReason: string;
|
|
64
|
+
/** Runs the research child and returns its assistant text. Injected so the
|
|
65
|
+
* orchestrator wires the real worker and tests use a fake. */
|
|
66
|
+
runChild: (tools: string, prompt: string, signal?: AbortSignal) => Promise<string>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Research a verify FAIL and return which action to recommend. Never throws
|
|
70
|
+
* (except a user cancel, which propagates so the loop's USER_CANCELLED handler
|
|
71
|
+
* reports a clean "cancelled — resume"). A child crash defaults to recommending
|
|
72
|
+
* AUTOFIX — the picker is still shown, only the default tint changes.
|
|
73
|
+
*/
|
|
74
|
+
export declare function researchResolution(deps: ResolutionDeps): Promise<ResolutionOutcome>;
|
|
75
|
+
export { RESOLUTION_TOOLS };
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify-FAIL resolution research for /task-auto.
|
|
3
|
+
*
|
|
4
|
+
* When the work-verification gate (verify-work.ts) returns a FAIL, the run no
|
|
5
|
+
* longer dead-stops with "fix and run /task-auto-resume". Instead the user is
|
|
6
|
+
* shown a boxed two-choice picker:
|
|
7
|
+
*
|
|
8
|
+
* • Autofix — re-run the task's implementation turn against the failure, then
|
|
9
|
+
* re-enter the verify gate.
|
|
10
|
+
* • Accept — treat the artifact as good, override the gate, and proceed
|
|
11
|
+
* (check off + commit) exactly as a PASS would.
|
|
12
|
+
*
|
|
13
|
+
* The user ALWAYS makes the final call. This module only computes which of the
|
|
14
|
+
* two cards is tinted RECOMMENDED, by handing the FAIL reason + the task spec to
|
|
15
|
+
* a fresh read+bash child of the SAME local model and letting it INVESTIGATE the
|
|
16
|
+
* real workspace: is the FAIL a genuine defect in the work (→ recommend AUTOFIX),
|
|
17
|
+
* or a false alarm / an acceptable artifact the gate misjudged (→ recommend
|
|
18
|
+
* ACCEPT)? This is the same shape of judgement a human reviewer makes when a CI
|
|
19
|
+
* check is red — sometimes the code is broken, sometimes the check is wrong.
|
|
20
|
+
*
|
|
21
|
+
* Tools: `read` + `bash` only — identical to the verify pass. The recommendation
|
|
22
|
+
* observes; it never edits. (Autofix, when chosen, is what edits — and it goes
|
|
23
|
+
* through the full implementation runner, not this pass.) A read-only research
|
|
24
|
+
* step can never itself change the tree it is judging.
|
|
25
|
+
*
|
|
26
|
+
* The verdict is advisory: a missing/garbled verdict defaults to recommending
|
|
27
|
+
* AUTOFIX, the conservative choice (re-do the work rather than bless it). The
|
|
28
|
+
* picker is shown regardless; this only moves the green tint.
|
|
29
|
+
*/
|
|
30
|
+
import { USER_CANCELLED } from './child-runner.js';
|
|
31
|
+
/** Same read-only contract as the verify pass: observe, never edit. */
|
|
32
|
+
const RESOLUTION_TOOLS = 'read,bash';
|
|
33
|
+
/**
|
|
34
|
+
* Build the resolution research child's prompt. Pure, so the wording is
|
|
35
|
+
* unit-tested without spawning pi. The contract: investigate the real workspace
|
|
36
|
+
* and end on exactly one RESOLUTION verdict line.
|
|
37
|
+
*/
|
|
38
|
+
export function buildResolutionPrompt(spec, failReason) {
|
|
39
|
+
return [
|
|
40
|
+
"A strict verification gate just FAILED an AI coding agent's task and the",
|
|
41
|
+
'run paused. You are the reviewer who decides what to recommend next. The',
|
|
42
|
+
'agent will NOT act on your verdict automatically — a human picks the final',
|
|
43
|
+
'action — but your recommendation sets the default, so be right.',
|
|
44
|
+
'',
|
|
45
|
+
'You have a `read` tool and a `bash` tool — nothing else. Investigate the',
|
|
46
|
+
'REAL files in this workspace; you CANNOT edit or create anything.',
|
|
47
|
+
'',
|
|
48
|
+
'THE TASK SPEC (its ACCEPTANCE criteria and VERIFY block were the contract):',
|
|
49
|
+
spec.trim(),
|
|
50
|
+
'',
|
|
51
|
+
"THE GATE'S FAILURE REASON:",
|
|
52
|
+
failReason.trim(),
|
|
53
|
+
'',
|
|
54
|
+
'Your job: decide which of these two is the right next step.',
|
|
55
|
+
' AUTOFIX — the work is genuinely wrong: a real defect, a missing or broken',
|
|
56
|
+
' artifact, behaviour that truly contradicts an ACCEPTANCE criterion.',
|
|
57
|
+
' The task should be re-done to fix it.',
|
|
58
|
+
' ACCEPT — the work is actually fine: the gate misjudged it (a false alarm,',
|
|
59
|
+
' an over-strict or factually-wrong check, an environment gap, a',
|
|
60
|
+
' difference in FORM that still satisfies the intent). The artifact',
|
|
61
|
+
' is good as-is and the FAIL should be overridden.',
|
|
62
|
+
'',
|
|
63
|
+
'CRITICAL — judge by FUNCTION, not by literal text. Verification gates (and',
|
|
64
|
+
'even the spec wording above) are frequently OVER-LITERAL. A FAIL that',
|
|
65
|
+
'complains that something is written in the wrong FORM, SHAPE, SYNTAX, SECTION',
|
|
66
|
+
'or WORDING — rather than that it BEHAVES wrong — is very often a false alarm:',
|
|
67
|
+
'the artifact reaches the same result through an equivalent form. The spec',
|
|
68
|
+
'wording is intent, not a string to diff against. Example: a config value',
|
|
69
|
+
'written as a nested table header vs. a dotted key are the same value to the',
|
|
70
|
+
'tool that reads it; demanding one exact spelling is a false alarm if the tool',
|
|
71
|
+
'accepts both and the intended setting is present.',
|
|
72
|
+
'',
|
|
73
|
+
'How to decide:',
|
|
74
|
+
'1. Do NOT decide from the FAIL text alone. Read the actual file(s) it points',
|
|
75
|
+
' at. When the check is runnable, RUN the real tool/command that consumes',
|
|
76
|
+
' that artifact (the build, the parser, the test) with `bash` and observe',
|
|
77
|
+
' whether it actually works. Reality overrules both the gate and the spec',
|
|
78
|
+
' wording.',
|
|
79
|
+
'2. If the failure is about HOW something is written and you confirm the',
|
|
80
|
+
' artifact still FUNCTIONALLY does what the ACCEPTANCE intends (the tool',
|
|
81
|
+
' accepts it, the value/element is present and effective, the build runs) —',
|
|
82
|
+
" that is ACCEPT, even though the text does not literally match the gate's or",
|
|
83
|
+
" the spec's phrasing.",
|
|
84
|
+
'3. If an intended element is genuinely ABSENT or the artifact actually fails',
|
|
85
|
+
' when you run it — that is AUTOFIX. When you cannot positively confirm the',
|
|
86
|
+
' artifact works, recommend AUTOFIX: re-doing the work is the safe default;',
|
|
87
|
+
' blessing broken work is the failure mode this whole gate exists to stop.',
|
|
88
|
+
'',
|
|
89
|
+
'When done, output EXACTLY ONE of these as the final line:',
|
|
90
|
+
' VERIFY-RESOLUTION: AUTOFIX <one sentence why the work must be re-done>',
|
|
91
|
+
' VERIFY-RESOLUTION: ACCEPT <one sentence why the artifact is good as-is>',
|
|
92
|
+
'Output the verdict line verbatim — it is parsed mechanically.'
|
|
93
|
+
].join('\n');
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Parse the child's recommendation. Scans for the LAST
|
|
97
|
+
* `VERIFY-RESOLUTION: AUTOFIX|ACCEPT` marker (the model reasons before
|
|
98
|
+
* concluding, and bash output may echo these words, so a distinct token and
|
|
99
|
+
* last-match-wins both matter).
|
|
100
|
+
*
|
|
101
|
+
* No marker → recommend AUTOFIX (conservative: re-do rather than bless), with a
|
|
102
|
+
* rationale that says the research was inconclusive.
|
|
103
|
+
*/
|
|
104
|
+
export function parseResolutionVerdict(text) {
|
|
105
|
+
const re = /VERIFY-RESOLUTION:\s*(AUTOFIX|ACCEPT)\b[ \t]*(.*)/gi;
|
|
106
|
+
let last = null;
|
|
107
|
+
for (let m = re.exec(text); m !== null; m = re.exec(text))
|
|
108
|
+
last = m;
|
|
109
|
+
if (!last) {
|
|
110
|
+
return {
|
|
111
|
+
recommend: 'autofix',
|
|
112
|
+
rationale: 'research inconclusive — defaulting to re-doing the work'
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const recommend = last[1].toUpperCase() === 'ACCEPT' ? 'accept' : 'autofix';
|
|
116
|
+
const rationale = last[2].trim()
|
|
117
|
+
|| (recommend === 'accept' ? 'artifact judged acceptable' : 'work judged to need fixing');
|
|
118
|
+
return { recommend, rationale };
|
|
119
|
+
}
|
|
120
|
+
// ─── Picker plumbing ─────────────────────────────────────────────────────────
|
|
121
|
+
//
|
|
122
|
+
// The user ALWAYS makes the final call on a verify FAIL. These pure helpers shape
|
|
123
|
+
// the boxed two-choice picker (verify-resolution → SessionUI.ask in the loop) and
|
|
124
|
+
// classify whatever comes back — a card value, a remote button label, or free
|
|
125
|
+
// text typed via the "type a different answer" fallback — so the orchestration
|
|
126
|
+
// stays mechanical and unit-tested.
|
|
127
|
+
export const ACCEPT_VALUE = 'accept';
|
|
128
|
+
export const AUTOFIX_VALUE = 'autofix';
|
|
129
|
+
export const ACCEPT_LABEL = 'Accept — keep the artifact as-is and continue';
|
|
130
|
+
export const AUTOFIX_LABEL = 'Autofix — re-run the task to fix it, then verify again';
|
|
131
|
+
/**
|
|
132
|
+
* The two cards for the picker, with the model-recommended one FIRST so the boxed
|
|
133
|
+
* renderer tints it green (it marks index 0 as recommended). Values are the bare
|
|
134
|
+
* tokens; remote buttons use the labels — {@link classifyResolutionAnswer}
|
|
135
|
+
* accepts both.
|
|
136
|
+
*/
|
|
137
|
+
export function resolutionOptions(recommend) {
|
|
138
|
+
const accept = { label: ACCEPT_LABEL, value: ACCEPT_VALUE };
|
|
139
|
+
const autofix = { label: AUTOFIX_LABEL, value: AUTOFIX_VALUE };
|
|
140
|
+
return recommend === 'accept' ?
|
|
141
|
+
[
|
|
142
|
+
{ ...accept, recommended: true },
|
|
143
|
+
{ ...autofix, recommended: false }
|
|
144
|
+
]
|
|
145
|
+
: [
|
|
146
|
+
{ ...autofix, recommended: true },
|
|
147
|
+
{ ...accept, recommended: false }
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Map a picker answer to an action. Handles the card value tokens, the remote
|
|
152
|
+
* button labels, an empty/undefined dismissal (cancel), and arbitrary free text
|
|
153
|
+
* typed via the fallback — which becomes an AUTOFIX with that text as guidance
|
|
154
|
+
* (the user is steering the re-run rather than blessing the artifact).
|
|
155
|
+
*/
|
|
156
|
+
export function classifyResolutionAnswer(answer) {
|
|
157
|
+
if (answer === undefined)
|
|
158
|
+
return { action: 'cancel' };
|
|
159
|
+
const t = answer.trim();
|
|
160
|
+
if (t.length === 0)
|
|
161
|
+
return { action: 'cancel' };
|
|
162
|
+
if (t === ACCEPT_VALUE || /^accept\b/i.test(t))
|
|
163
|
+
return { action: 'accept' };
|
|
164
|
+
if (t === AUTOFIX_VALUE || /^autofix\b/i.test(t))
|
|
165
|
+
return { action: 'autofix' };
|
|
166
|
+
// Free-text override → re-run the task with the typed text as fix guidance.
|
|
167
|
+
return { action: 'autofix', guidance: t };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Research a verify FAIL and return which action to recommend. Never throws
|
|
171
|
+
* (except a user cancel, which propagates so the loop's USER_CANCELLED handler
|
|
172
|
+
* reports a clean "cancelled — resume"). A child crash defaults to recommending
|
|
173
|
+
* AUTOFIX — the picker is still shown, only the default tint changes.
|
|
174
|
+
*/
|
|
175
|
+
export async function researchResolution(deps) {
|
|
176
|
+
let text;
|
|
177
|
+
try {
|
|
178
|
+
text = await deps.runChild(RESOLUTION_TOOLS, buildResolutionPrompt(deps.spec, deps.failReason), deps.signal);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
if (err instanceof Error && err.message === USER_CANCELLED)
|
|
182
|
+
throw err;
|
|
183
|
+
return {
|
|
184
|
+
recommend: 'autofix',
|
|
185
|
+
rationale: 'resolution research could not run — defaulting to re-doing the work'
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return parseResolutionVerdict(text);
|
|
189
|
+
}
|
|
190
|
+
export { RESOLUTION_TOOLS };
|
package/dist/task/widget.d.ts
CHANGED
|
@@ -52,8 +52,9 @@ export interface AutoLoaderState {
|
|
|
52
52
|
/** Which /task-auto stage this loader is for. Defaults to 'planning' (the
|
|
53
53
|
* numbered clarify/decompose steps); 'enforce' is the per-task guideline
|
|
54
54
|
* pass and 'verify' is the per-task work-verification pass, neither of which
|
|
55
|
-
* has step numbering.
|
|
56
|
-
|
|
55
|
+
* has step numbering. 'recommend' is the read-only research that picks the
|
|
56
|
+
* recommended action after a verify FAIL. */
|
|
57
|
+
kind?: 'planning' | 'enforce' | 'verify' | 'recommend';
|
|
57
58
|
}
|
|
58
59
|
export declare function buildAutoLoaderLines(s: AutoLoaderState, theme?: WidgetTheme): string[];
|
|
59
60
|
/**
|
package/dist/task/widget.js
CHANGED
|
@@ -123,7 +123,8 @@ export function buildAutoLoaderLines(s, theme) {
|
|
|
123
123
|
const head = `/task-auto · ${s.title}`;
|
|
124
124
|
let detail = s.kind === 'enforce' ? `enforcing guidelines · ${elapsed}`
|
|
125
125
|
: s.kind === 'verify' ? `verifying work · ${elapsed}`
|
|
126
|
-
:
|
|
126
|
+
: s.kind === 'recommend' ? `assessing the failure · ${elapsed}`
|
|
127
|
+
: `planning ${s.stepNum}/${s.stepTotal} ${s.step} · ${elapsed}`;
|
|
127
128
|
if (s.contextUsage) {
|
|
128
129
|
const ctxDetail = formatContextDetail(s.contextUsage, theme);
|
|
129
130
|
if (ctxDetail)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.17",
|
|
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",
|