@mjasnikovs/pi-task 0.39.2 → 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/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/remote/ui-styles.js
CHANGED
|
@@ -121,6 +121,9 @@ export const STYLES = ` :root {
|
|
|
121
121
|
/* Persistent inline system note (e.g. context compaction) — a muted centered
|
|
122
122
|
divider, distinct from chat bubbles. */
|
|
123
123
|
.sysnote {
|
|
124
|
+
/* publishNote sends multi-line text (the /task-config table); textContent
|
|
125
|
+
keeps the newlines only if the box does not collapse them. */
|
|
126
|
+
white-space: pre-wrap;
|
|
124
127
|
align-self: center; color: var(--subtext0); font-size: 11px;
|
|
125
128
|
font-family: ui-monospace, monospace; letter-spacing: 0.5px;
|
|
126
129
|
padding: 2px 10px; opacity: 0.85;
|
|
@@ -414,10 +417,6 @@ export const STYLES = ` :root {
|
|
|
414
417
|
background: var(--surface1); color: var(--text); z-index: 60; }
|
|
415
418
|
.toast.warning { background: var(--peach); color: var(--crust); }
|
|
416
419
|
.toast.error { background: var(--red); color: var(--crust); }
|
|
417
|
-
#viewer { position: fixed; inset: 24px; background: var(--mantle); border: 1px solid var(--surface2);
|
|
418
|
-
border-radius: 8px; padding: 16px; overflow: auto; white-space: pre-wrap;
|
|
419
|
-
overflow-wrap: anywhere; word-break: break-word; display: none; z-index: 70; }
|
|
420
|
-
#viewer .close { position: absolute; top: 8px; right: 12px; cursor: pointer; color: var(--subtext0); }
|
|
421
420
|
/* Desktop: center the transcript/status/input in a readable column instead of
|
|
422
421
|
hugging the left edge. The scrollbar stays at the true window edge; only the
|
|
423
422
|
content is inset. Mobile (below 960px) is unchanged. */
|
package/dist/remote/ui.js
CHANGED
|
@@ -79,7 +79,6 @@ ${STYLES}
|
|
|
79
79
|
<textarea id="prompt-input" rows="3" placeholder="Type your answer…" style="display:none"></textarea>
|
|
80
80
|
<div class="row" id="prompt-buttons"></div>
|
|
81
81
|
</div>
|
|
82
|
-
<div id="viewer"><span class="close" id="viewer-close">✕</span><div id="viewer-body"></div></div>
|
|
83
82
|
<script>
|
|
84
83
|
${renderModule()}
|
|
85
84
|
${highlightModule()}
|
|
@@ -26,7 +26,7 @@ import { isFatalChildCause, prependHint, USER_CANCELLED } from './child-runner.j
|
|
|
26
26
|
import { requestCancel, resetCancel, isCancelRequested, cancelCheckpoint } from './cancel-points.js';
|
|
27
27
|
import { withRun, announceTerminal } from './run-bracket.js';
|
|
28
28
|
import { refineExistingFilesBlock, SINGLE_READ_EXTENSION_PATH } from './phases.js';
|
|
29
|
-
import { SessionUI, registerBridgeCommand, publishLifecycleNotice } from '../remote/bridge.js';
|
|
29
|
+
import { SessionUI, registerBridgeCommand, publishLifecycleNotice, notifyBoth, notifyRun, isRemoteOrigin } from '../remote/bridge.js';
|
|
30
30
|
import { getParentContextWindow } from './context-usage.js';
|
|
31
31
|
import { ChildStatus, runPlanningChild, statusCallbacks } from './child-status.js';
|
|
32
32
|
import { buildGateDeps, collectTreeChanges } from './gate-deps.js';
|
|
@@ -421,7 +421,7 @@ async function schedulePendingRepairs(cwd, id, afterIndex, ctx, deps) {
|
|
|
421
421
|
titles.splice(at + 1, 0, title);
|
|
422
422
|
at += 1;
|
|
423
423
|
await deps.record?.(cwd, id, `plan: inserted scoped repair step after step ${afterIndex + 1} — ${title}`);
|
|
424
|
-
ctx
|
|
424
|
+
notifyRun(ctx, `${id}: queued a scoped repair for ${repair.file} (${repair.owner}'s file — root cause of ${repair.blamed.join(', ')}).`, 'warning');
|
|
425
425
|
}
|
|
426
426
|
}
|
|
427
427
|
catch {
|
|
@@ -623,7 +623,7 @@ export async function elicitClarifications(ctx, cwd, deps, oriented) {
|
|
|
623
623
|
}
|
|
624
624
|
}
|
|
625
625
|
if (transcript.length === 0) {
|
|
626
|
-
ctx
|
|
626
|
+
notifyBoth(ctx, 'No clarifying questions needed — planning tasks…', 'info');
|
|
627
627
|
}
|
|
628
628
|
return transcript.forRecord();
|
|
629
629
|
}
|
|
@@ -844,7 +844,7 @@ export async function coverPlan(ctx, cwd, deps, oriented, clarifications, decomp
|
|
|
844
844
|
// mode (bare verdict, indistinguishable from a real one). The plan still
|
|
845
845
|
// ships — the floor never rejects on count — but never silently.
|
|
846
846
|
if (best.suspect) {
|
|
847
|
-
ctx
|
|
847
|
+
notifyRun(ctx, `/task-auto: only ${best.plan.titles.length} task(s) planned for a large spec`
|
|
848
848
|
+ ' and the regeneration did not grow the list — review the plan before running.', 'warning');
|
|
849
849
|
}
|
|
850
850
|
break;
|
|
@@ -889,7 +889,7 @@ export async function coverPlan(ctx, cwd, deps, oriented, clarifications, decomp
|
|
|
889
889
|
if (unresolvedMissing !== null) {
|
|
890
890
|
logPlanDebug(cwd, `decompose-coverage exhausted ${round} round(s) still INCOMPLETE — missing: `
|
|
891
891
|
+ unresolvedMissing.join('; ').slice(0, 300));
|
|
892
|
-
ctx
|
|
892
|
+
notifyRun(ctx, `/task-auto: no task fully owns — ${unresolvedMissing.join('; ').slice(0, 200)}. `
|
|
893
893
|
+ 'Carried into every task via .pi-tasks/requirements.md, but not as a dedicated '
|
|
894
894
|
+ 'task. To give it one, stop now and add it to the plan in .pi-tasks/; otherwise '
|
|
895
895
|
+ 'it proceeds.', 'warning');
|
|
@@ -966,7 +966,7 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
966
966
|
carriedJudge.length > 0 ? `${carriedJudge.length} judge-flagged` : '',
|
|
967
967
|
carriedDangling.length > 0 ? `${carriedDangling.length} dangling-artifact` : ''
|
|
968
968
|
].filter(p => p.length > 0);
|
|
969
|
-
ctx
|
|
969
|
+
notifyRun(ctx, `/task-auto: carrying ${parts.join(', ')} requirement(s) into every task`
|
|
970
970
|
+ ' — see .pi-tasks/requirements.md.', 'info');
|
|
971
971
|
}
|
|
972
972
|
// Thread the feature's spec doc(s) into every title so each per-task
|
|
@@ -1191,7 +1191,7 @@ async function reportStashDrift(active, deps, cwd, id, title, before) {
|
|
|
1191
1191
|
const after = await deps.stashRef(cwd);
|
|
1192
1192
|
if (after === before)
|
|
1193
1193
|
return;
|
|
1194
|
-
active
|
|
1194
|
+
notifyRun(active, `${id}: the git stash stack changed during "${title}" and was left that way — `
|
|
1195
1195
|
+ 'inspect `git stash list`; an orphan stash later pops as an unresolvable conflict.', 'warning');
|
|
1196
1196
|
}
|
|
1197
1197
|
catch {
|
|
@@ -1269,8 +1269,7 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
1269
1269
|
// never learns how far the run has gotten — mirror it into the session
|
|
1270
1270
|
// view too (transient toast, same as announceDone's info path).
|
|
1271
1271
|
const progressMsg = `${id}: task ${next.index + 1}/${entries.length} — ${next.title}`;
|
|
1272
|
-
active
|
|
1273
|
-
publishLifecycleNotice(progressMsg, 'info');
|
|
1272
|
+
notifyBoth(active, progressMsg, 'info');
|
|
1274
1273
|
// If this entry already has a stamped inner id, it was started in a
|
|
1275
1274
|
// previous (interrupted) run — resume it from its saved phase rather
|
|
1276
1275
|
// than spawning a fresh task. But the stamped inner file can be gone
|
|
@@ -1298,7 +1297,7 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
1298
1297
|
// half. Only the success path is announced to keep the common no-op quiet.
|
|
1299
1298
|
const checkpoint = await deps.commit(cwd, `chore: checkpoint before "${next.title}"`);
|
|
1300
1299
|
if (checkpoint.committed) {
|
|
1301
|
-
active
|
|
1300
|
+
notifyRun(active, `${id}: checkpointed uncommitted work before "${next.title}".`, 'info');
|
|
1302
1301
|
}
|
|
1303
1302
|
// Stash ref before the task: compared after the gates so a stash pushed
|
|
1304
1303
|
// during the task (impl model or any child) and left behind is called
|
|
@@ -1444,8 +1443,11 @@ async function handleTaskAuto(args, ctx) {
|
|
|
1444
1443
|
const cwd = ctx.cwd;
|
|
1445
1444
|
const raw = args.trim();
|
|
1446
1445
|
if (raw.length === 0) {
|
|
1447
|
-
|
|
1448
|
-
|
|
1446
|
+
// Prefilling the composer answers a LOCAL user. From the browser it would
|
|
1447
|
+
// type into a terminal nobody is sitting at; the notice is the answer there.
|
|
1448
|
+
if (!isRemoteOrigin(ctx))
|
|
1449
|
+
ctx.ui.setEditorText('/task-auto ');
|
|
1450
|
+
notifyBoth(ctx, 'Describe the feature after /task-auto (use @ for file completion).', 'info');
|
|
1449
1451
|
return;
|
|
1450
1452
|
}
|
|
1451
1453
|
autoRunning = true;
|
|
@@ -1506,11 +1508,13 @@ async function handleTaskAutoResume(args, ctx) {
|
|
|
1506
1508
|
: await findResumableAutoDetailed(cwd);
|
|
1507
1509
|
const candidate = eligible ?? (unattended ? await findResumableAutoDetailed(cwd) : null);
|
|
1508
1510
|
const decision = decideResume(candidate, Date.now(), unattended);
|
|
1509
|
-
ctx.ui.notify(decision.banner, decision.level);
|
|
1510
1511
|
// An unattended refusal happens with nobody watching the terminal — the
|
|
1511
|
-
// remote view is the only surface that will still be there in the morning
|
|
1512
|
+
// remote view is the only surface that will still be there in the morning,
|
|
1513
|
+
// which is why it is a run notice and not a toast.
|
|
1512
1514
|
if (unattended)
|
|
1513
|
-
|
|
1515
|
+
notifyRun(ctx, decision.banner, decision.level);
|
|
1516
|
+
else
|
|
1517
|
+
notifyBoth(ctx, decision.banner, decision.level);
|
|
1514
1518
|
if (!decision.resume || !candidate)
|
|
1515
1519
|
return;
|
|
1516
1520
|
const id = candidate.id;
|
|
@@ -1543,10 +1547,12 @@ async function handleTaskAutoResume(args, ctx) {
|
|
|
1543
1547
|
async function handleTaskAutoCancel(_args, ctx) {
|
|
1544
1548
|
if (!autoRunning) {
|
|
1545
1549
|
ctx.ui.notify('No /task-auto loop is running.', 'info');
|
|
1550
|
+
publishLifecycleNotice('No /task-auto loop is running.', 'info');
|
|
1546
1551
|
return;
|
|
1547
1552
|
}
|
|
1548
1553
|
requestAutoCancel();
|
|
1549
1554
|
ctx.ui.notify(CANCEL_ACK, 'warning');
|
|
1555
|
+
publishLifecycleNotice(CANCEL_ACK, 'warning');
|
|
1550
1556
|
}
|
|
1551
1557
|
/**
|
|
1552
1558
|
* What the user is told the moment the request lands. Deliberately does NOT
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { updateTaskFrontMatter } from './task-io.js';
|
|
6
6
|
import { flashTerminalWidget } from './widget.js';
|
|
7
|
-
import {
|
|
7
|
+
import { notifyRun } from '../remote/bridge.js';
|
|
8
8
|
import { ChildFailureError, USER_CANCELLED } from './child-runner.js';
|
|
9
9
|
import { streamStallCause } from '../shared/stream-watchdog.js';
|
|
10
10
|
// ─── Classifier ──────────────────────────────────────────────────────────────
|
|
@@ -80,9 +80,8 @@ export async function handleFailure(err, ctx, cwd, id, aborted) {
|
|
|
80
80
|
const c = classifyFailure(err, aborted);
|
|
81
81
|
await updateTaskFrontMatter(cwd, id, { state: c.state, reason: c.reason });
|
|
82
82
|
flashTerminalWidget(ctx, c.state, id, c.flash);
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
publishLifecycleNotice(`${id} ${c.notify}`, c.level);
|
|
83
|
+
// How the run ENDED. Durable on the remote: a toast is gone before an absent
|
|
84
|
+
// user looks, and this is the one line that says the task did not finish.
|
|
85
|
+
notifyRun(ctx, `${id} ${c.notify}`, c.level);
|
|
87
86
|
return c;
|
|
88
87
|
}
|
package/dist/task/gate-child.js
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { formatLoopHint } from './loop-detector.js';
|
|
28
28
|
import { classifyEnforceChildFailure } from './enforce-guidelines.js';
|
|
29
|
+
import { notifyRun } from '../remote/bridge.js';
|
|
29
30
|
/**
|
|
30
31
|
* What each kind may do. Adding a child is a row; it cannot be added without
|
|
31
32
|
* deciding all four questions, which is the point.
|
|
@@ -145,7 +146,7 @@ export function makeGateChild(deps) {
|
|
|
145
146
|
log(`=== ${deps.kind} GIT-STATE GUARD — ${label}; `
|
|
146
147
|
+ `restored: ${rec.actions.join('; ')} ===`);
|
|
147
148
|
if (rec.verdictTainted) {
|
|
148
|
-
deps.ctx
|
|
149
|
+
notifyRun(deps.ctx, `${deps.taskTitle}: ${deps.kind} child mutated repo state — `
|
|
149
150
|
+ `restored (${rec.actions.join('; ').slice(0, 140)}).`, 'warning');
|
|
150
151
|
}
|
|
151
152
|
}
|
|
@@ -156,7 +157,7 @@ export function makeGateChild(deps) {
|
|
|
156
157
|
// only thing that can block.
|
|
157
158
|
if (r.loopHit) {
|
|
158
159
|
log(`=== ${deps.kind} LOOP WARNING — ${formatLoopHint(r.loopHit)} ===`);
|
|
159
|
-
deps.ctx
|
|
160
|
+
notifyRun(deps.ctx, `${deps.taskTitle}: ${deps.kind} worker looped past the nudges — `
|
|
160
161
|
+ 'continuing (not blocked).', 'warning');
|
|
161
162
|
}
|
|
162
163
|
const failure = classifyEnforceChildFailure(r);
|
|
@@ -27,7 +27,7 @@ import { allocateTaskId, ensureTasksDir, readSection, readTaskFile, setTaskSecti
|
|
|
27
27
|
import { startWidget } from './widget.js';
|
|
28
28
|
import { setupImplWidget } from './impl-widget.js';
|
|
29
29
|
import { enterImplementationTurn } from './implementation-scope.js';
|
|
30
|
-
import {
|
|
30
|
+
import { SessionUI, publishNotify, registerBridgeCommand, getBridge, notifyBoth, isRemoteOrigin } from '../remote/bridge.js';
|
|
31
31
|
import { pushNotify } from '../remote/push.js';
|
|
32
32
|
import { getConfig } from '../config/config.js';
|
|
33
33
|
import { gateDebugWriter } from './debug-log.js';
|
|
@@ -681,8 +681,11 @@ async function handleTask(args, ctx) {
|
|
|
681
681
|
const cwd = ctx.cwd;
|
|
682
682
|
const raw = args.trim();
|
|
683
683
|
if (raw.length === 0) {
|
|
684
|
-
|
|
685
|
-
|
|
684
|
+
// Prefilling the composer answers a LOCAL user. From the browser it would
|
|
685
|
+
// type into a terminal nobody is sitting at; the notice is the answer there.
|
|
686
|
+
if (!isRemoteOrigin(ctx))
|
|
687
|
+
ctx.ui.setEditorText('/task ');
|
|
688
|
+
notifyBoth(ctx, 'Type your prompt after /task (use @ for file completion).', 'info');
|
|
686
689
|
return;
|
|
687
690
|
}
|
|
688
691
|
// When a gate is enabled, /task awaits the implementation and runs the same
|
|
@@ -696,7 +699,7 @@ async function handleTask(args, ctx) {
|
|
|
696
699
|
}
|
|
697
700
|
const { end } = await runSingleTask(ctx, cwd, raw, { notifyFinish: true });
|
|
698
701
|
if (end.kind === 'no-session') {
|
|
699
|
-
ctx
|
|
702
|
+
notifyBoth(ctx, 'Could not start a fresh session for /task.', 'warning');
|
|
700
703
|
}
|
|
701
704
|
}
|
|
702
705
|
async function handleTaskList(_args, ctx) {
|
|
@@ -729,8 +732,12 @@ async function handleTaskList(_args, ctx) {
|
|
|
729
732
|
if (lines.length === 0)
|
|
730
733
|
lines.push('(no tasks in .pi-tasks/)');
|
|
731
734
|
lines.push('', 'resume: /task-resume <id> (eligible: in_progress, pending, cancelled, failed)');
|
|
732
|
-
|
|
733
|
-
|
|
735
|
+
await new SessionUI(ctx).show({
|
|
736
|
+
localTitle: 'Tasks',
|
|
737
|
+
localText: lines.join('\n'),
|
|
738
|
+
question: 'Tasks',
|
|
739
|
+
body: lines.join('\n')
|
|
740
|
+
});
|
|
734
741
|
}
|
|
735
742
|
async function handleTaskResume(args, ctx) {
|
|
736
743
|
await ctx.waitForIdle();
|
|
@@ -742,8 +749,7 @@ async function handleTaskResume(args, ctx) {
|
|
|
742
749
|
await fsp.access(taskFilePath(cwd, id));
|
|
743
750
|
}
|
|
744
751
|
catch {
|
|
745
|
-
ctx
|
|
746
|
-
publishNotify(`${id} not found in .pi-tasks/`, 'error');
|
|
752
|
+
notifyBoth(ctx, `${id} not found in .pi-tasks/`, 'error');
|
|
747
753
|
return;
|
|
748
754
|
}
|
|
749
755
|
}
|
|
@@ -771,8 +777,7 @@ async function handleTaskResume(args, ctx) {
|
|
|
771
777
|
}
|
|
772
778
|
candidates.sort((a, b) => b.mtime - a.mtime);
|
|
773
779
|
if (candidates.length === 0) {
|
|
774
|
-
ctx
|
|
775
|
-
publishNotify('No resumable tasks.', 'info');
|
|
780
|
+
notifyBoth(ctx, 'No resumable tasks.', 'info');
|
|
776
781
|
return;
|
|
777
782
|
}
|
|
778
783
|
id = candidates[0].id;
|
|
@@ -785,19 +790,17 @@ async function handleTaskResume(args, ctx) {
|
|
|
785
790
|
}
|
|
786
791
|
const { end } = await runSingleTask(ctx, cwd, '', { resumeId: id, notifyFinish: true });
|
|
787
792
|
if (end.kind === 'no-session') {
|
|
788
|
-
ctx
|
|
793
|
+
notifyBoth(ctx, 'Could not start a fresh session for /task-resume.', 'warning');
|
|
789
794
|
}
|
|
790
795
|
}
|
|
791
796
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
792
797
|
async function handleTaskCancel(_args, ctx) {
|
|
793
798
|
if (!activeTask) {
|
|
794
|
-
ctx
|
|
795
|
-
publishNotify('No task is running.', 'info');
|
|
799
|
+
notifyBoth(ctx, 'No task is running.', 'info');
|
|
796
800
|
return;
|
|
797
801
|
}
|
|
798
802
|
activeTask.cancel();
|
|
799
|
-
ctx
|
|
800
|
-
publishNotify(`Cancelling ${activeTask.taskId}…`, 'warning');
|
|
803
|
+
notifyBoth(ctx, `Cancelling ${activeTask.taskId}…`, 'warning');
|
|
801
804
|
}
|
|
802
805
|
// ─── Entry point ─────────────────────────────────────────────────────────────
|
|
803
806
|
export function registerTask(pi) {
|
package/dist/task/phases.js
CHANGED
|
@@ -40,7 +40,7 @@ import { detachUnsatisfiableRequirements, claimPendingRequirements, unclaimedPen
|
|
|
40
40
|
import { trackedSourceOracle } from './owned-freeze-conflict.js';
|
|
41
41
|
import { groupArgsForChild, runPhaseChild, runWithEmphasisRetry, prependHint, USER_CANCELLED, ChildFailureError, isFatalChildCause } from './child-runner.js';
|
|
42
42
|
import { runResearchWorker, researchWorkerCacheHeading } from './research-worker.js';
|
|
43
|
-
import { SessionUI } from '../remote/bridge.js';
|
|
43
|
+
import { SessionUI, notifyRun } from '../remote/bridge.js';
|
|
44
44
|
import { isYoloMode, yoloPickAutoAnswer } from './yolo.js';
|
|
45
45
|
import { QaTranscript, GRILL_QA_POLICY } from './qa-transcript.js';
|
|
46
46
|
// ─── Re-export constants from their home modules ────────────────────────────
|
|
@@ -1113,7 +1113,7 @@ export async function critiqueWithFallback(d, p) {
|
|
|
1113
1113
|
// it is the validated-shape fallback, and failing the run costs more.
|
|
1114
1114
|
if (parseVerifyBlock(p.spec) === null)
|
|
1115
1115
|
throw err;
|
|
1116
|
-
p.ctx
|
|
1116
|
+
notifyRun(p.ctx, msg === 'verify_grep_theater' ?
|
|
1117
1117
|
'Critique rewrite kept a grep-only VERIFY — using compose draft. Consider adding a command that RUNS the deliverable.'
|
|
1118
1118
|
: "Critique couldn't produce a VERIFY block — using compose draft. Edit the spec manually if needed.", 'warning');
|
|
1119
1119
|
return p.spec;
|
|
@@ -34,7 +34,7 @@ import { deriveTitle } from './parsers.js';
|
|
|
34
34
|
import { renderInlineMarkdown } from './inline-markdown.js';
|
|
35
35
|
import { expandFeatureMentions } from './auto-orchestrator.js';
|
|
36
36
|
import { runSingleTask, runGatedTask } from './orchestrator.js';
|
|
37
|
-
import { SessionUI,
|
|
37
|
+
import { SessionUI, registerBridgeCommand, notifyBoth, notifyRun, isRemoteOrigin } from '../remote/bridge.js';
|
|
38
38
|
import { withRun, announceTerminal } from './run-bracket.js';
|
|
39
39
|
import { getConfig } from '../config/config.js';
|
|
40
40
|
import { isYoloMode } from './yolo.js';
|
|
@@ -121,9 +121,12 @@ export function buildPlanDeps(ctx, cwd, planId, task, signal) {
|
|
|
121
121
|
allowSkip: true
|
|
122
122
|
}),
|
|
123
123
|
showAnswer: async (question, answer) => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
await ui.show({
|
|
125
|
+
localTitle: ASK_TITLE,
|
|
126
|
+
localText: `You asked:\n${question}\n\n${answer}\n`,
|
|
127
|
+
question: `You asked: ${question}`,
|
|
128
|
+
body: answer
|
|
129
|
+
});
|
|
127
130
|
},
|
|
128
131
|
onEntries: async (entries) => {
|
|
129
132
|
await persistEntries(cwd, planId, entries);
|
|
@@ -147,13 +150,7 @@ export function buildPlanDeps(ctx, cwd, planId, task, signal) {
|
|
|
147
150
|
async function reportReadOnlyViolation(ctx, cwd, planId, step, touched, logDebug) {
|
|
148
151
|
const line = formatReadOnlyViolation(step, touched);
|
|
149
152
|
logDebug?.(line);
|
|
150
|
-
|
|
151
|
-
ctx.ui.notify(line, 'error');
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
/* stale ctx — the record below is what matters */
|
|
155
|
-
}
|
|
156
|
-
publishLifecycleNotice(line, 'error');
|
|
153
|
+
notifyRun(ctx, line, 'error');
|
|
157
154
|
try {
|
|
158
155
|
const existing = (await readSection(cwd, planId, 'read-only violations')) ?? '';
|
|
159
156
|
await setTaskSection(cwd, planId, 'read-only violations', existing ? `${existing}\n- ${line}` : `- ${line}`);
|
|
@@ -193,7 +190,7 @@ async function defaultHandoff(ctx, cwd, prompt) {
|
|
|
193
190
|
}
|
|
194
191
|
const { taskId, end } = await runSingleTask(ctx, cwd, prompt, { notifyFinish: true });
|
|
195
192
|
if (end.kind === 'no-session') {
|
|
196
|
-
ctx
|
|
193
|
+
notifyBoth(ctx, 'Could not start a fresh session for /task-plan.', 'warning');
|
|
197
194
|
return undefined;
|
|
198
195
|
}
|
|
199
196
|
return taskId || undefined;
|
|
@@ -239,8 +236,11 @@ export async function handleTaskPlan(args, ctx, commandDeps = DEFAULT_COMMAND_DE
|
|
|
239
236
|
const cwd = ctx.cwd;
|
|
240
237
|
const raw = args.trim();
|
|
241
238
|
if (raw.length === 0) {
|
|
242
|
-
|
|
243
|
-
|
|
239
|
+
// Prefilling the composer answers a LOCAL user. From the browser it would
|
|
240
|
+
// type into a terminal nobody is sitting at; the notice is the answer there.
|
|
241
|
+
if (!isRemoteOrigin(ctx))
|
|
242
|
+
ctx.ui.setEditorText('/task-plan ');
|
|
243
|
+
notifyBoth(ctx, 'Describe the task after /task-plan (use @ for file completion).', 'info');
|
|
244
244
|
return;
|
|
245
245
|
}
|
|
246
246
|
// Inline any @file the user referenced, exactly as /task-auto's planner does:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AutofixLedger } from './autofix-ledger.js';
|
|
2
2
|
import { describeDebt, recordDebt } from './accept-debt.js';
|
|
3
3
|
import { cancelCheckpoint } from './cancel-points.js';
|
|
4
|
-
import { SessionUI } from '../remote/bridge.js';
|
|
4
|
+
import { SessionUI, notifyBoth, notifyRun } from '../remote/bridge.js';
|
|
5
5
|
import { isYoloMode, yoloFinalGateChoice, YOLO_STAMP } from './yolo.js';
|
|
6
6
|
import { ignoredWriteTrailLine, ignoredWriteDebtReason } from './write-guard.js';
|
|
7
7
|
import { readOwnedRequirements } from './requirements.js';
|
|
@@ -76,7 +76,7 @@ export async function runFinalGateStage(active, deps, p) {
|
|
|
76
76
|
// Set when the gate finished having observed NOTHING dynamic. Declared out here so
|
|
77
77
|
// the run-completion announcement can say so.
|
|
78
78
|
let unobservedNote = null;
|
|
79
|
-
active
|
|
79
|
+
notifyBoth(active, `${id}: running final integration gate…`, 'info');
|
|
80
80
|
// Run-level gate trail on the parent task file — same durable auditability
|
|
81
81
|
// contract as the per-task `## gates` records.
|
|
82
82
|
const recGate = async (line) => {
|
|
@@ -111,7 +111,7 @@ export async function runFinalGateStage(active, deps, p) {
|
|
|
111
111
|
unobservedNote = fin.unobserved;
|
|
112
112
|
await recGate(`final-gate: UNOBSERVED — ${fin.reason.slice(0, 300)}`);
|
|
113
113
|
await carryDebt(fin.unobserved);
|
|
114
|
-
active
|
|
114
|
+
notifyRun(active, `${id}: the final integration gate observed NOTHING dynamic — `
|
|
115
115
|
+ 'the run completed on static checks alone. Nothing verified '
|
|
116
116
|
+ 'that the assembled product builds, boots or works.', 'warning');
|
|
117
117
|
}
|
|
@@ -132,7 +132,7 @@ export async function runFinalGateStage(active, deps, p) {
|
|
|
132
132
|
for (const d of debts) {
|
|
133
133
|
await recGate(`defect STILL OPEN — ${d.taskId || '(unknown task)'}: ${describeDebt(d)}: ${d.reason.slice(0, 240)}${d.conflict ? ` [CONFLICTING CLAIM — ${d.conflict}]` : ''}`);
|
|
134
134
|
}
|
|
135
|
-
active
|
|
135
|
+
notifyRun(active, `${id}: ${debts.length} recorded verify-FAIL defect(s) are STILL unresolved at run end — see the gate trail.`, 'warning');
|
|
136
136
|
};
|
|
137
137
|
// What was REPORTED, so a post-autofix re-derivation can be compared against it
|
|
138
138
|
// rather than blindly re-printed.
|
|
@@ -149,7 +149,7 @@ export async function runFinalGateStage(active, deps, p) {
|
|
|
149
149
|
+ ` ${(o.pending ?? []).join(', ')}]`);
|
|
150
150
|
}
|
|
151
151
|
if (unclaimed.length > 0) {
|
|
152
|
-
active
|
|
152
|
+
notifyRun(active, `${id}: ${unclaimed.length} authoritative design requirement(s) ended the run`
|
|
153
153
|
+ ' owned by NO task — the task they were mapped to could not touch the'
|
|
154
154
|
+ ' file, and nothing else claimed it. See the gate trail.', 'warning');
|
|
155
155
|
}
|
|
@@ -315,7 +315,7 @@ export async function runFinalGateStage(active, deps, p) {
|
|
|
315
315
|
// commit — the ACCEPT is a decision about the FAILING gate, never an
|
|
316
316
|
// instruction to throw away work.
|
|
317
317
|
await commitStranded('accepted');
|
|
318
|
-
active
|
|
318
|
+
notifyRun(active, `${id}: final integration gate FAIL accepted by user — completing.`
|
|
319
319
|
+ (ledger.stranded().length > 0 ?
|
|
320
320
|
` ${ledger.stranded().length} uncommitted fix-pass change(s) committed separately.`
|
|
321
321
|
: ''), 'warning');
|
|
@@ -324,7 +324,7 @@ export async function runFinalGateStage(active, deps, p) {
|
|
|
324
324
|
if (choice.action === 'autofix' && canAutofix) {
|
|
325
325
|
const attempt = ledger.attempt();
|
|
326
326
|
await recGate(`final-gate: user chose AUTOFIX (attempt ${attempt}/${MAX_FINAL_GATE_AUTOFIX})`);
|
|
327
|
-
active
|
|
327
|
+
notifyRun(active, `${id}: final-gate autofix (${attempt}/${MAX_FINAL_GATE_AUTOFIX}) — bounded fix pass, then the gate re-runs…`, 'info');
|
|
328
328
|
const seed = choice.guidance ? `${fin.reason}\n\nUser guidance: ${choice.guidance}` : fin.reason;
|
|
329
329
|
const fix = await deps.finalGateFix(active, cwd, seed, [...ledger.ignoredWrites()]);
|
|
330
330
|
// IGNORED-PATH WRITES. The pass wrote file(s) git ignores, so
|
|
@@ -355,7 +355,7 @@ export async function runFinalGateStage(active, deps, p) {
|
|
|
355
355
|
await carryDebt(fix.unobserved);
|
|
356
356
|
}
|
|
357
357
|
await recGate(`final-gate: autofix ${fix.unobserved ? 'ended UNOBSERVED' : 'converged'} — ${fix.reason.slice(0, 200)}`);
|
|
358
|
-
active
|
|
358
|
+
notifyRun(active, `${id}: final integration gate ${fix.unobserved ? 'is UNOBSERVED' : 'PASSES'} after autofix — ${fix.reason.slice(0, 140)}`, fix.unobserved ? 'warning' : 'info');
|
|
359
359
|
// The gate's own outcome, WHOLE, with this door's reason on it.
|
|
360
360
|
// Rebuilding it as a two-key literal would drop `openDebts` and
|
|
361
361
|
// `observedFailures` from the value entirely.
|
|
@@ -380,7 +380,7 @@ export async function runFinalGateStage(active, deps, p) {
|
|
|
380
380
|
await recGate(`final-gate: autofix attempt ${attempt} left ${ledger.stranded().length} `
|
|
381
381
|
+ `uncommitted change(s) — ${ledger.stranded().slice(0, 8).join(', ')}`);
|
|
382
382
|
}
|
|
383
|
-
active
|
|
383
|
+
notifyRun(active, `${id}: final-gate autofix did not converge — ${fix.reason.slice(0, 140)}`, 'warning');
|
|
384
384
|
// NON-PROGRESS CLASSIFIER. An attempt that changed the tree, re-ran the
|
|
385
385
|
// gate, and got back the SAME ranked-first failure as the previous such
|
|
386
386
|
// attempt is evidence about the CHECK, not about the fix — a check that
|
|
@@ -396,7 +396,7 @@ export async function runFinalGateStage(active, deps, p) {
|
|
|
396
396
|
await recGate(`final-gate: check DEMOTED to UNOBSERVED after ${attempt} tree-changing `
|
|
397
397
|
+ `attempts returned an identical failure — carried as debt (origin final-gate) `
|
|
398
398
|
+ `and re-checked by the next run's gate: ${verdict.detail.slice(0, 240)}`);
|
|
399
|
-
active
|
|
399
|
+
notifyRun(active, `${id}: final-gate check is unfalsifiable in this environment — carried as debt; `
|
|
400
400
|
+ 'the remaining checks decide convergence.', 'warning');
|
|
401
401
|
}
|
|
402
402
|
// Convergence on the REMAINING checks: a demoted signature no longer counts
|
|
@@ -409,7 +409,7 @@ export async function runFinalGateStage(active, deps, p) {
|
|
|
409
409
|
const converged = `converged on all remaining checks; ${ledger.demotedCount()} check(s) `
|
|
410
410
|
+ 'carried as UNOBSERVED debt (unfalsifiable in this environment)';
|
|
411
411
|
await recGate(`final-gate: ${converged}`);
|
|
412
|
-
active
|
|
412
|
+
notifyRun(active, `${id}: final integration gate converged — ${converged}.`, 'warning');
|
|
413
413
|
// The gate's own outcome, with this door's reason on it — the
|
|
414
414
|
// whole value, so `openDebts` survives the assignment.
|
|
415
415
|
fin = { ...fix.gate, ok: true, reason: converged };
|
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 };
|