@mjasnikovs/pi-task 0.39.1 → 0.39.3
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 +2 -2
- package/dist/config/register.js +8 -2
- package/dist/remote/bridge.d.ts +65 -1
- package/dist/remote/bridge.js +142 -22
- package/dist/remote/protocol.d.ts +9 -6
- package/dist/remote/register.js +13 -7
- package/dist/remote/session-state.js +6 -1
- package/dist/remote/ui-script.js +13 -10
- package/dist/remote/ui-styles.d.ts +1 -1
- package/dist/remote/ui-styles.js +3 -4
- package/dist/remote/ui.js +0 -1
- package/dist/task/auto-orchestrator.js +21 -15
- package/dist/task/failure-classifier.js +4 -5
- package/dist/task/gate-child.js +3 -2
- package/dist/task/orchestrator.js +18 -15
- package/dist/task/phases.js +2 -2
- package/dist/task/plan-orchestrator.js +14 -14
- package/dist/task/run-final-gate.js +11 -11
- package/dist/task/task-gates.js +22 -16
- package/dist/task/widget.js +30 -39
- package/package.json +1 -1
package/dist/task/task-gates.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { verifyFailClass } from './verify-work.js';
|
|
2
2
|
import { resolutionOptions, classifyResolutionAnswer } from './verify-resolution.js';
|
|
3
|
-
import { SessionUI } from '../remote/bridge.js';
|
|
3
|
+
import { SessionUI, notifyBoth, notifyRun } from '../remote/bridge.js';
|
|
4
4
|
import { isYoloMode, yoloVerifyResolution, YOLO_STAMP } from './yolo.js';
|
|
5
5
|
import { extractFailingCommand, findRepairCandidate, summariseDefect } from './root-cause-repair.js';
|
|
6
6
|
import { attributeEnforceFailure } from './enforce-attribution.js';
|
|
@@ -92,7 +92,7 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
92
92
|
// accept-override leaves this false → enforce runs flag-only.
|
|
93
93
|
let verifyCleanPass = false;
|
|
94
94
|
if (deps.verify) {
|
|
95
|
-
active
|
|
95
|
+
notifyBoth(active, `${p.tag}: verifying "${p.title}"…`, 'info');
|
|
96
96
|
let verified = await deps.verify(active, p.cwd, p.title, p.taskId);
|
|
97
97
|
await rec(verdictLine(verified));
|
|
98
98
|
// A FAIL no longer dead-stops. When the research recommends AUTOFIX, pi
|
|
@@ -122,7 +122,7 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
122
122
|
const failClass = verifyFailClass(verified);
|
|
123
123
|
if (!lintFixAttempted && deps.lintFix && failClass === 'repo-health') {
|
|
124
124
|
lintFixAttempted = true;
|
|
125
|
-
active
|
|
125
|
+
notifyRun(active, `${p.tag}: static findings on "${p.title}" — attempting bounded lint fix…`, 'info');
|
|
126
126
|
const fix = await deps.lintFix(active, p.cwd, p.title, p.taskId, failReason);
|
|
127
127
|
await rec(`lint-fix: ${fix.ok ?
|
|
128
128
|
`applied${fix.reason ? ` (${fix.reason})` : ''} — re-verifying`
|
|
@@ -239,7 +239,7 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
239
239
|
`resolution: auto-AUTOFIX (${YOLO_STAMP} rescue — judge recommended ACCEPT with the unattended `
|
|
240
240
|
+ `budget unspent; one attempt, ${autoFixCount}/${MAX_AUTO_AUTOFIX})`
|
|
241
241
|
: `resolution: auto-AUTOFIX (recommended, unattended ${autoFixCount}/${MAX_AUTO_AUTOFIX})`);
|
|
242
|
-
active
|
|
242
|
+
notifyRun(active, `${p.tag}: verify FAIL on "${p.title}" — auto-fixing (${yoloRescueNow ? `${YOLO_STAMP} one attempt before accepting` : 'recommended'}, ${autoFixCount}/${MAX_AUTO_AUTOFIX})…`, 'info');
|
|
243
243
|
choice = { action: 'autofix' };
|
|
244
244
|
}
|
|
245
245
|
else {
|
|
@@ -286,7 +286,7 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
286
286
|
// recording must never break the gate sequence
|
|
287
287
|
}
|
|
288
288
|
}
|
|
289
|
-
active
|
|
289
|
+
notifyRun(active, `${p.tag}: accepted "${p.title}" despite verify FAIL (${failReason.slice(0, 120)}) — proceeding.${byYolo ? ` ${YOLO_STAMP}` : ''}`, 'warning');
|
|
290
290
|
break;
|
|
291
291
|
}
|
|
292
292
|
// AUTOFIX: re-run the implementation turn with the failure (and any typed
|
|
@@ -302,7 +302,7 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
302
302
|
if (!autoFixNow && !yoloRescueNow) {
|
|
303
303
|
await rec('resolution: user chose AUTOFIX — re-running the implementation turn');
|
|
304
304
|
}
|
|
305
|
-
active
|
|
305
|
+
notifyBoth(active, `${p.tag}: autofixing "${p.title}"…`, 'info');
|
|
306
306
|
const diagnosis = (recOutcome.recommend === 'autofix'
|
|
307
307
|
&& recOutcome.rationale.length > 0
|
|
308
308
|
&& recOutcome.rationale !== failReason) ?
|
|
@@ -396,7 +396,7 @@ export async function runEnforcePass(active, deps, p, rec, routeRootCause, args)
|
|
|
396
396
|
const reverted = await deps.revertFrozenPaths(p.cwd, frozen);
|
|
397
397
|
if (reverted.length > 0) {
|
|
398
398
|
await rec(`enforce: frozen-path write DENIED — reverted ${reverted.length} spec-frozen file(s) the edit pass modified: ${reverted.join(', ')}`);
|
|
399
|
-
active
|
|
399
|
+
notifyRun(active, `${p.tag}: guideline edits on "${p.title}" touched spec-frozen path(s) (${reverted.join(', ').slice(0, 120)}) — reverted before commit.`, 'warning');
|
|
400
400
|
}
|
|
401
401
|
}
|
|
402
402
|
}
|
|
@@ -408,7 +408,7 @@ export async function runEnforcePass(active, deps, p, rec, routeRootCause, args)
|
|
|
408
408
|
const editsMade = mode === 'edit' && deps.dirty ? await deps.dirty(p.cwd) : undefined;
|
|
409
409
|
await rec(`enforce(${mode}): ${verdict.ok ? `clean${verdict.reason ? ` (${verdict.reason})` : ''}` : (verdict.reason ?? 'not clean')}${editsMade ? ' — edits in tree' : ''}`);
|
|
410
410
|
if (!verdict.ok) {
|
|
411
|
-
active
|
|
411
|
+
notifyRun(active, `${p.tag}: guideline ${mode === 'edit' ? 'enforcement' : 'review'} on "${p.title}" — ${verdict.reason ?? 'not clean'} — continuing.`, 'warning');
|
|
412
412
|
}
|
|
413
413
|
// PRE-COMMIT health gate on enforce edits: an edit pass that breaks the
|
|
414
414
|
// repo's own lint would otherwise burn a commit, a model re-verify and a
|
|
@@ -443,7 +443,7 @@ export async function runEnforcePass(active, deps, p, rec, routeRootCause, args)
|
|
|
443
443
|
else {
|
|
444
444
|
await rec(`enforce: edits REGRESSED repo health pre-commit (${after.reason}) — no discard available, left uncommitted${outputTail}`);
|
|
445
445
|
}
|
|
446
|
-
active
|
|
446
|
+
notifyRun(active, `${p.tag}: guideline edits on "${p.title}" regressed repo health (${after.reason.slice(0, 120)}) — discarded before commit.`, 'warning');
|
|
447
447
|
}
|
|
448
448
|
else if (!after.ok) {
|
|
449
449
|
// Failing both before and after → not enforce's fault. Keep the edits;
|
|
@@ -512,7 +512,7 @@ export async function runEnforcePass(active, deps, p, rec, routeRootCause, args)
|
|
|
512
512
|
if (!after.ok && rootCause) {
|
|
513
513
|
await rec(`enforce: re-verify FAILED (${afterReason.slice(0, 200)}) but the failure is attributed to a PRE-EXISTING defect in \`${rootCause.file}\` `
|
|
514
514
|
+ `(${rootCause.owner}'s file, untouched by the ENFORCE COMMIT whose fate this differential decides) — edits KEPT, not reverted; repair task queued`);
|
|
515
|
-
active
|
|
515
|
+
notifyRun(active, `${p.tag}: guideline fixes on "${p.title}" re-verified red on a pre-existing defect in ${rootCause.file} (${rootCause.owner}'s file) — keeping the fixes, queued a repair task.`, 'warning');
|
|
516
516
|
}
|
|
517
517
|
else if (!after.ok && attribution?.verdict === 'keep') {
|
|
518
518
|
// KEEP, mechanically justified: every file the failing check named
|
|
@@ -547,7 +547,7 @@ export async function runEnforcePass(active, deps, p, rec, routeRootCause, args)
|
|
|
547
547
|
// queueing a repair must never break the gate sequence
|
|
548
548
|
}
|
|
549
549
|
}
|
|
550
|
-
active
|
|
550
|
+
notifyRun(active, `${p.tag}: guideline fixes on "${p.title}" re-verified red on ${attribution.file ?? 'a file'} — outside the enforce diff, so keeping the fixes and recording the defect.`, 'warning');
|
|
551
551
|
}
|
|
552
552
|
else if (!after.ok) {
|
|
553
553
|
if (deps.revert)
|
|
@@ -567,11 +567,11 @@ export async function runEnforcePass(active, deps, p, rec, routeRootCause, args)
|
|
|
567
567
|
// gate re-checks and surfaces it (static-class auto-closes if a
|
|
568
568
|
// later task fixed the statics; otherwise it stays open).
|
|
569
569
|
await deps.recordDebt?.(p.cwd, p.taskId, after.reason ?? 'enforce re-verify failed', 'enforce-revert');
|
|
570
|
-
active
|
|
570
|
+
notifyRun(active, `${p.tag}: guideline fixes regressed verification on "${p.title}" (${(after.reason ?? 'now fails').slice(0, 120)}) — ${deps.revert ? 'reverted them, kept the verified work' : 'left in place (no revert available)'}.`, 'warning');
|
|
571
571
|
}
|
|
572
572
|
else {
|
|
573
573
|
await rec('enforce: fixes committed — re-verify PASS, kept');
|
|
574
|
-
active
|
|
574
|
+
notifyRun(active, `${p.tag}: committed guideline fixes for "${p.title}".`, 'info');
|
|
575
575
|
}
|
|
576
576
|
}
|
|
577
577
|
else {
|
|
@@ -651,7 +651,7 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
651
651
|
+ `${commit.excluded.slice(0, 8).join(', ')}`
|
|
652
652
|
+ `${commit.excluded.length > 8 ? `, +${commit.excluded.length - 8} more` : ''}`);
|
|
653
653
|
}
|
|
654
|
-
active
|
|
654
|
+
notifyBoth(active, `${p.tag}: committed "${p.title}".`, 'info');
|
|
655
655
|
}
|
|
656
656
|
else {
|
|
657
657
|
await rec(`commit: skipped (${commit.reason ?? 'unknown'})`);
|
|
@@ -661,9 +661,15 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
661
661
|
// warnings to show for it. "blocked" is the unmerged-index refusal
|
|
662
662
|
// (gitCommitAll) — the same severity: nothing can commit until it's resolved.
|
|
663
663
|
const gitFailure = /^git (commit|add) (failed|blocked)/.test(commit.reason ?? '');
|
|
664
|
-
|
|
664
|
+
const line = gitFailure ?
|
|
665
665
|
`${p.tag}: COMMIT FAILED (${commit.reason}) — enforce and revert guards are disabled for this task.`
|
|
666
|
-
: `${p.tag}: not committed (${commit.reason ?? 'unknown'}) — continuing
|
|
666
|
+
: `${p.tag}: not committed (${commit.reason ?? 'unknown'}) — continuing.`;
|
|
667
|
+
// A git failure means the whole task ran unguarded. A remote user cannot
|
|
668
|
+
// infer that from the widget, and a toast is gone before they look.
|
|
669
|
+
if (gitFailure)
|
|
670
|
+
notifyRun(active, line, 'error');
|
|
671
|
+
else
|
|
672
|
+
notifyBoth(active, line, 'warning');
|
|
667
673
|
}
|
|
668
674
|
await runEnforcePass(active, deps, p, rec, routeRootCause, { cleanPass, commit });
|
|
669
675
|
return { kind: 'done', ctx: active };
|
package/dist/task/widget.js
CHANGED
|
@@ -115,8 +115,6 @@ export function buildWidgetData(s) {
|
|
|
115
115
|
}
|
|
116
116
|
// ─── Widget lifecycle ────────────────────────────────────────────────────────
|
|
117
117
|
export function startWidget(ctx, getState) {
|
|
118
|
-
if (!ctx.hasUI)
|
|
119
|
-
return () => { };
|
|
120
118
|
// pi's `ctx.ui` getter calls `runner.assertActive()`, which THROWS once the
|
|
121
119
|
// ctx goes stale (/reload, session replacement) — so the theme read belongs
|
|
122
120
|
// INSIDE the guard, not one line above it. render() runs from a timer, and a
|
|
@@ -128,7 +126,12 @@ export function startWidget(ctx, getState) {
|
|
|
128
126
|
if (stale)
|
|
129
127
|
return;
|
|
130
128
|
const s = getState();
|
|
131
|
-
|
|
129
|
+
// The wire half FIRST, and unguarded: it needs no ctx, so gating it on a
|
|
130
|
+
// local terminal that may not exist would leave a headless host's browser
|
|
131
|
+
// with no phase, no progress and no failure flash.
|
|
132
|
+
setTaskWidget(s ? buildWidgetLines(s, undefined) : undefined, s ? buildWidgetData(s) : null);
|
|
133
|
+
if (!ctx.hasUI)
|
|
134
|
+
return;
|
|
132
135
|
try {
|
|
133
136
|
const lines = s ? buildWidgetLines(s, ctx.ui.theme) : undefined;
|
|
134
137
|
ctx.ui.setWidget(WIDGET_KEY, lines);
|
|
@@ -136,9 +139,7 @@ export function startWidget(ctx, getState) {
|
|
|
136
139
|
catch {
|
|
137
140
|
stale = true;
|
|
138
141
|
clearInterval(timer);
|
|
139
|
-
return;
|
|
140
142
|
}
|
|
141
|
-
setTaskWidget(plain, s ? buildWidgetData(s) : null);
|
|
142
143
|
};
|
|
143
144
|
// The timer is created BEFORE the first render so `timer` is always bound
|
|
144
145
|
// when render's catch reaches for it. Calling render() first instead would
|
|
@@ -208,16 +209,15 @@ export function buildAutoLoaderData(s) {
|
|
|
208
209
|
* (returns a no-op disposer) when there's no UI.
|
|
209
210
|
*/
|
|
210
211
|
export function startAutoLoader(ctx, getState) {
|
|
211
|
-
|
|
212
|
-
return () => { };
|
|
213
|
-
// Same staleness hazard as startWidget: the theme read must sit inside the
|
|
214
|
-
// guard, and a stale ctx stops the timer rather than throwing every tick.
|
|
212
|
+
// Same staleness hazard as startWidget, and the same wire-half-first order.
|
|
215
213
|
let stale = false;
|
|
216
214
|
const render = () => {
|
|
217
215
|
if (stale)
|
|
218
216
|
return;
|
|
219
217
|
const s = getState();
|
|
220
|
-
|
|
218
|
+
setTaskWidget(s ? buildAutoLoaderLines(s, undefined) : undefined, s ? buildAutoLoaderData(s) : null);
|
|
219
|
+
if (!ctx.hasUI)
|
|
220
|
+
return;
|
|
221
221
|
try {
|
|
222
222
|
const lines = s ? buildAutoLoaderLines(s, ctx.ui.theme) : undefined;
|
|
223
223
|
ctx.ui.setWidget(AUTO_WIDGET_KEY, lines);
|
|
@@ -225,9 +225,7 @@ export function startAutoLoader(ctx, getState) {
|
|
|
225
225
|
catch {
|
|
226
226
|
stale = true;
|
|
227
227
|
clearInterval(timer);
|
|
228
|
-
return;
|
|
229
228
|
}
|
|
230
|
-
setTaskWidget(plain, s ? buildAutoLoaderData(s) : null);
|
|
231
229
|
};
|
|
232
230
|
// The timer is created BEFORE the first render so `timer` is always bound
|
|
233
231
|
// when render's catch reaches for it. Calling render() first instead would
|
|
@@ -277,40 +275,33 @@ export function buildImplData(s) {
|
|
|
277
275
|
return d;
|
|
278
276
|
}
|
|
279
277
|
export function flashTerminalWidget(ctx, state, taskId, reason) {
|
|
280
|
-
if (!ctx.hasUI)
|
|
281
|
-
return;
|
|
282
|
-
// Same staleness hazard as the render timers: bail rather than throw.
|
|
283
|
-
let theme;
|
|
284
|
-
try {
|
|
285
|
-
theme = ctx.ui.theme;
|
|
286
|
-
}
|
|
287
|
-
catch {
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
let line;
|
|
291
|
-
let clearMs;
|
|
292
|
-
if (state === 'cancelled') {
|
|
293
|
-
line = theme.fg('warning', `⚠ ${taskId} cancelled`);
|
|
294
|
-
clearMs = NOTIFY_CLEAR_MS;
|
|
295
|
-
}
|
|
296
|
-
else {
|
|
297
|
-
line = theme.fg('error', `✘ ${taskId} failed${reason ? ': ' + reason : ''}`);
|
|
298
|
-
clearMs = FAIL_CLEAR_MS;
|
|
299
|
-
}
|
|
300
278
|
const plainLine = state === 'cancelled' ?
|
|
301
279
|
`⚠ ${taskId} cancelled`
|
|
302
280
|
: `✘ ${taskId} failed${reason ? ': ' + reason : ''}`;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
281
|
+
const clearMs = state === 'cancelled' ? NOTIFY_CLEAR_MS : FAIL_CLEAR_MS;
|
|
282
|
+
setTaskWidget([plainLine]);
|
|
283
|
+
// The wire half is already done, so a missing or stale local ctx no longer
|
|
284
|
+
// costs the browser the flash as well. Same staleness hazard as the render
|
|
285
|
+
// timers: bail rather than throw.
|
|
286
|
+
if (ctx.hasUI) {
|
|
287
|
+
try {
|
|
288
|
+
const theme = ctx.ui.theme;
|
|
289
|
+
const tint = state === 'cancelled' ? 'warning' : 'error';
|
|
290
|
+
ctx.ui.setWidget(WIDGET_KEY, [theme.fg(tint, plainLine)]);
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
/* stale ctx */
|
|
294
|
+
}
|
|
309
295
|
}
|
|
310
296
|
setTimeout(() => {
|
|
297
|
+
// Wire first, for the same reason as the flash above: a ctx that went
|
|
298
|
+
// stale in the meantime would otherwise leave the browser showing the
|
|
299
|
+
// flash for the rest of the session.
|
|
300
|
+
setTaskWidget(undefined);
|
|
301
|
+
if (!ctx.hasUI)
|
|
302
|
+
return;
|
|
311
303
|
try {
|
|
312
304
|
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
313
|
-
setTaskWidget(undefined);
|
|
314
305
|
}
|
|
315
306
|
catch {
|
|
316
307
|
/* stale ctx */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.39.
|
|
3
|
+
"version": "0.39.3",
|
|
4
4
|
"description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|