@mjasnikovs/pi-task 0.38.15 → 0.38.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/config/config.d.ts +26 -0
- package/dist/config/config.js +68 -17
- package/dist/shared/child-process.js +9 -16
- package/dist/task/accept-debt.d.ts +7 -5
- package/dist/task/accept-debt.js +18 -14
- package/dist/task/artifact-closure.js +18 -63
- package/dist/task/auto-orchestrator.js +211 -218
- package/dist/task/autofix-ledger.d.ts +113 -0
- package/dist/task/autofix-ledger.js +152 -0
- package/dist/task/boot-probe.d.ts +109 -1
- package/dist/task/boot-probe.js +139 -23
- package/dist/task/child-runner.d.ts +50 -6
- package/dist/task/child-runner.js +48 -69
- package/dist/task/command-run.d.ts +49 -6
- package/dist/task/command-run.js +154 -18
- package/dist/task/coverage-loop.d.ts +11 -0
- package/dist/task/coverage-loop.js +16 -0
- package/dist/task/external-context.d.ts +9 -12
- package/dist/task/external-context.js +5 -5
- package/dist/task/failure-classifier.d.ts +9 -1
- package/dist/task/failure-classifier.js +9 -0
- package/dist/task/final-gate-fix.d.ts +22 -26
- package/dist/task/final-gate-fix.js +16 -31
- package/dist/task/final-gate.d.ts +10 -2
- package/dist/task/final-gate.js +55 -89
- package/dist/task/fix-child.d.ts +64 -0
- package/dist/task/fix-child.js +66 -0
- package/dist/task/gate-deps.js +20 -13
- package/dist/task/lint-fix.d.ts +7 -0
- package/dist/task/lint-fix.js +45 -9
- package/dist/task/orchestrator.d.ts +33 -24
- package/dist/task/orchestrator.js +75 -46
- package/dist/task/phases.d.ts +120 -34
- package/dist/task/phases.js +221 -134
- package/dist/task/plan-orchestrator.js +2 -2
- package/dist/task/plan-rounds.d.ts +86 -0
- package/dist/task/plan-rounds.js +105 -0
- package/dist/task/plan-session.d.ts +31 -21
- package/dist/task/plan-session.js +97 -120
- package/dist/task/qa-transcript.d.ts +100 -0
- package/dist/task/qa-transcript.js +99 -0
- package/dist/task/question-source.d.ts +117 -0
- package/dist/task/question-source.js +174 -0
- package/dist/task/repo-health-check.d.ts +21 -21
- package/dist/task/repo-health-check.js +43 -112
- package/dist/task/run-end.d.ts +77 -0
- package/dist/task/run-end.js +37 -0
- package/dist/task/run-final-gate.js +71 -79
- package/dist/task/serve-entry.js +6 -57
- package/dist/task/shipped-source.d.ts +67 -0
- package/dist/task/shipped-source.js +144 -0
- package/dist/task/task-gates.d.ts +9 -1
- package/dist/task/task-gates.js +27 -6
- package/dist/task/terminal-outcome.d.ts +1 -1
- package/dist/task/terminal-outcome.js +12 -0
- package/dist/task/verify-work.d.ts +46 -0
- package/dist/task/verify-work.js +51 -3
- package/dist/workers/brave-search.d.ts +7 -0
- package/dist/workers/brave-search.js +36 -55
- package/dist/workers/ddg-search.d.ts +1 -1
- package/dist/workers/ddg-search.js +27 -47
- package/dist/workers/docs-core.d.ts +71 -1
- package/dist/workers/docs-core.js +131 -71
- package/dist/workers/exa-search.d.ts +2 -2
- package/dist/workers/exa-search.js +53 -68
- package/dist/workers/html-clean.js +67 -88
- package/dist/workers/http-request.d.ts +74 -0
- package/dist/workers/http-request.js +103 -0
- package/dist/workers/npm-version.js +37 -42
- package/dist/workers/pi-worker-core.d.ts +13 -2
- package/dist/workers/pi-worker-core.js +35 -25
- package/dist/workers/pi-worker-docs.d.ts +1 -1
- package/dist/workers/pi-worker-docs.js +49 -68
- package/dist/workers/pi-worker-fetch.d.ts +1 -1
- package/dist/workers/pi-worker-fetch.js +20 -21
- package/dist/workers/pi-worker-search.js +6 -4
- package/dist/workers/pi-worker.js +5 -4
- package/dist/workers/search-core.d.ts +1 -1
- package/dist/workers/search-core.js +36 -42
- package/dist/workers/search-types.d.ts +13 -0
- package/dist/workers/search-types.js +27 -0
- package/dist/workers/shared.d.ts +51 -11
- package/dist/workers/shared.js +0 -0
- package/dist/workers/worker-channels.d.ts +60 -0
- package/dist/workers/worker-channels.js +98 -0
- package/package.json +1 -1
package/dist/task/gate-deps.js
CHANGED
|
@@ -24,7 +24,7 @@ import { readEnvNotes, appendEnvNotes } from './env-notes.js';
|
|
|
24
24
|
import { readContracts } from './contracts.js';
|
|
25
25
|
import { recordDebt } from './accept-debt.js';
|
|
26
26
|
import { recordRepairCandidate } from './root-cause-repair.js';
|
|
27
|
-
import { runRepoHealthCheck
|
|
27
|
+
import { runRepoHealthCheck } from './repo-health-check.js';
|
|
28
28
|
import { runFinalIntegrationGate, discoverGateCommandLabels, discoverGateCommandBodies } from './final-gate.js';
|
|
29
29
|
import { runFinalGateAutofix } from './final-gate-fix.js';
|
|
30
30
|
import { researchResolution } from './verify-resolution.js';
|
|
@@ -755,14 +755,19 @@ export function buildGateDeps(params) {
|
|
|
755
755
|
// own lint/typecheck and fails on a real non-zero exit, independent of
|
|
756
756
|
// the model-authored VERIFY block (which may not lint at all). ASYNC:
|
|
757
757
|
// the sync runner froze the event loop for the whole lint (see above).
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
758
|
+
// ONE call, both arms. The baseline arm used to call this
|
|
759
|
+
// without the signal or the progress hook, because
|
|
760
|
+
// `runRepoHealthCheck` was SYNCHRONOUS and blocking the event
|
|
761
|
+
// loop was the thing being measured. It is async now, so that
|
|
762
|
+
// branch measured treatment against treatment — and it also
|
|
763
|
+
// made `DEADAIR_AB_ARM=baseline` silently uncancellable and
|
|
764
|
+
// mute. The arm's real difference is the LOADER, above.
|
|
765
|
+
repoHealth: () => runRepoHealthCheck(cwd2, {
|
|
766
|
+
signal,
|
|
767
|
+
onCommand: c => {
|
|
768
|
+
stageLine = `repo health · ${c}`;
|
|
769
|
+
}
|
|
770
|
+
}),
|
|
766
771
|
// The deterministic probes, bound in one place (buildVerifyProbes
|
|
767
772
|
// above); the PROBE_ADAPTERS table in verify-work.ts runs them.
|
|
768
773
|
probes: buildVerifyProbes({
|
|
@@ -814,7 +819,7 @@ export function buildGateDeps(params) {
|
|
|
814
819
|
signal,
|
|
815
820
|
failReason,
|
|
816
821
|
runChild: gateChild(fixCtx, cwd2, taskTitle, 'lint-fix', 'verify-debug.log'),
|
|
817
|
-
repoHealth: () =>
|
|
822
|
+
repoHealth: () => runRepoHealthCheck(cwd2, { signal }),
|
|
818
823
|
git: async (args) => {
|
|
819
824
|
const r = await git(cwd2, args, signal);
|
|
820
825
|
return { exitCode: r.exitCode, stdout: r.stdout };
|
|
@@ -846,7 +851,7 @@ export function buildGateDeps(params) {
|
|
|
846
851
|
startedAt,
|
|
847
852
|
lastLine: running ? `repo health · ${running}` : 'repo health'
|
|
848
853
|
}));
|
|
849
|
-
return
|
|
854
|
+
return runRepoHealthCheck(cwd2, {
|
|
850
855
|
signal,
|
|
851
856
|
onCommand: c => {
|
|
852
857
|
running = c;
|
|
@@ -865,7 +870,9 @@ export function buildGateDeps(params) {
|
|
|
865
870
|
runChild: gateChild(fixCtx, cwd2, 'final integration gate', 'final-fix', 'final-gate-debug.log'),
|
|
866
871
|
// The gate re-run is the only arbiter of convergence, and the
|
|
867
872
|
// shrink guard's discovery is the gate's own (see final-gate.ts).
|
|
868
|
-
|
|
873
|
+
// The run's cancel reaches the re-run too. Without it the whole
|
|
874
|
+
// `FinalGateOptions.signal` path is inert in the shipped code.
|
|
875
|
+
gate: c => runFinalIntegrationGate(c, { signal }),
|
|
869
876
|
discoverLabels: discoverGateCommandLabels,
|
|
870
877
|
discoverBodies: discoverGateCommandBodies,
|
|
871
878
|
discard: discardTreeEdits,
|
|
@@ -893,7 +900,7 @@ export function buildGateDeps(params) {
|
|
|
893
900
|
// depend on it.
|
|
894
901
|
ignoredSnapshot: () => collectIgnoredSnapshot(cwd2, signal),
|
|
895
902
|
...(ignoredKnown && ignoredKnown.length > 0 ? { ignoredKnown } : {}),
|
|
896
|
-
gateWithoutIgnored: paths => gatePassesWithoutIgnored(cwd2, paths, c => runFinalIntegrationGate(c), makeDebugAppender(path.join(tasksDir(cwd2), 'final-gate-debug.log'))),
|
|
903
|
+
gateWithoutIgnored: paths => gatePassesWithoutIgnored(cwd2, paths, c => runFinalIntegrationGate(c, { signal }), makeDebugAppender(path.join(tasksDir(cwd2), 'final-gate-debug.log'))),
|
|
897
904
|
log: makeDebugAppender(path.join(tasksDir(cwd2), 'final-gate-debug.log'))
|
|
898
905
|
}),
|
|
899
906
|
recommend: async (recCtx, cwd2, taskTitle, taskId, failReason) => {
|
package/dist/task/lint-fix.d.ts
CHANGED
|
@@ -42,6 +42,13 @@ export interface LintFixDeps {
|
|
|
42
42
|
* deletion guard is disarmed (prior behavior).
|
|
43
43
|
*/
|
|
44
44
|
introducedBy?: (rel: string) => Promise<string | null>;
|
|
45
|
+
/**
|
|
46
|
+
* Debug-log sink. This dep did not exist, so all four of this pass's guard
|
|
47
|
+
* trips were invisible in the trail while the twin (`FinalFixDeps.log`) logged
|
|
48
|
+
* three of its own — and a guard whose firing leaves no record cannot be
|
|
49
|
+
* distinguished from one that never armed.
|
|
50
|
+
*/
|
|
51
|
+
log?: (msg: string) => void;
|
|
45
52
|
}
|
|
46
53
|
/** The fix child edits and runs the checker; bash exists to RUN the check, not git. */
|
|
47
54
|
export declare const LINT_FIX_TOOLS = "read,edit,bash";
|
package/dist/task/lint-fix.js
CHANGED
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
* `currentTaskId` and `introducedBy` are wired.
|
|
59
59
|
*/
|
|
60
60
|
import { parseChangedFrozenFiles, pathNamedIn, revertFrozenPaths } from './frozen-path-guard.js';
|
|
61
|
+
import { runFixChild } from './fix-child.js';
|
|
61
62
|
import { parseTreeChanges } from './write-guard.js';
|
|
62
63
|
import { findCrossTaskDeletions } from './task-provenance.js';
|
|
63
64
|
/** The fix child edits and runs the checker; bash exists to RUN the check, not git. */
|
|
@@ -204,16 +205,27 @@ export async function runBoundedLintFix(deps) {
|
|
|
204
205
|
// guard for this run (inconclusive ≠ evidence).
|
|
205
206
|
const deletionGuardArmed = Boolean(deps.currentTaskId && deps.introducedBy);
|
|
206
207
|
const preChanges = deletionGuardArmed ? await treeChanges(deps) : null;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
208
|
+
// The four-rung ladder lives in task/fix-child.ts. A cancel still propagates
|
|
209
|
+
// (it THROWS, so the caller's USER_CANCELLED path is unchanged); what is new
|
|
210
|
+
// here is the BLOCKED rung, which this pass instructed in its prompt and then
|
|
211
|
+
// discarded the reply of — so a child that reported BLOCKED still paid the
|
|
212
|
+
// whole guard stack and a repo-health re-run (15–69s measured) to be told
|
|
213
|
+
// `did not converge`, instead of its own stated reason.
|
|
214
|
+
const end = await runFixChild({
|
|
215
|
+
runChild: deps.runChild,
|
|
216
|
+
tools: LINT_FIX_TOOLS,
|
|
217
|
+
prompt: buildLintFixPrompt(deps.failReason, frozen),
|
|
218
|
+
signal: deps.signal,
|
|
219
|
+
marker: 'LINT-FIX'
|
|
220
|
+
});
|
|
221
|
+
if (end.kind === 'error') {
|
|
222
|
+
deps.log?.(`lint-fix child failed — ${end.msg}`);
|
|
223
|
+
return { ok: false, reason: `fix child failed: ${end.msg}` };
|
|
216
224
|
}
|
|
225
|
+
// A BLOCKED child is NOT an early return from here: the guards below exist to
|
|
226
|
+
// catch a child that discarded work, and a child can discard work and then
|
|
227
|
+
// block. The marker is consulted after them, in place of the re-run — which is
|
|
228
|
+
// where the twin consults its own.
|
|
217
229
|
// REVERT-GUARD: every pre-existing work file must still differ from HEAD, and
|
|
218
230
|
// every pre-existing untracked file must still exist. Trip → restore snapshot.
|
|
219
231
|
// Every comparison requires git to have actually SUCCEEDED: a git error after
|
|
@@ -250,6 +262,7 @@ export async function runBoundedLintFix(deps) {
|
|
|
250
262
|
await deps.git(['checkout', snapshot, '--', '.', EXCLUDE_TASKS_DIR]);
|
|
251
263
|
await deps.git(['reset']);
|
|
252
264
|
}
|
|
265
|
+
deps.log?.(`lint-fix REVERT-GUARD — discarded ${violations.length} work file(s)`);
|
|
253
266
|
return {
|
|
254
267
|
ok: false,
|
|
255
268
|
reason: `revert-guard: fix pass discarded work (${violations.slice(0, 3).join(', ')}`
|
|
@@ -277,6 +290,7 @@ export async function runBoundedLintFix(deps) {
|
|
|
277
290
|
if (crossDeletions.length > 0) {
|
|
278
291
|
await deps.git(['checkout', '-f', 'HEAD', '--', ...crossDeletions.map(d => d.path)]);
|
|
279
292
|
const named = crossDeletions.map(d => `${d.path} — ${d.owner}'s deliverable`);
|
|
293
|
+
deps.log?.(`lint-fix CROSS-TASK-DELETION GUARD — ${named[0]}`);
|
|
280
294
|
return {
|
|
281
295
|
ok: false,
|
|
282
296
|
reason: `cross-task-deletion: fix child DELETED sibling task deliverable(s) `
|
|
@@ -300,6 +314,7 @@ export async function runBoundedLintFix(deps) {
|
|
|
300
314
|
const frozenViolations = [...postFrozenDirty].filter(f => !preFrozenDirty.has(f));
|
|
301
315
|
if (frozenViolations.length > 0) {
|
|
302
316
|
const reverted = await revertFrozenPaths(frozenViolations, deps.git);
|
|
317
|
+
deps.log?.(`lint-fix FROZEN-PATH GUARD — ${frozenViolations.slice(0, 3).join(', ')}`);
|
|
303
318
|
return {
|
|
304
319
|
ok: false,
|
|
305
320
|
reason: `frozen-path: fix child modified spec-frozen path(s) `
|
|
@@ -311,8 +326,28 @@ export async function runBoundedLintFix(deps) {
|
|
|
311
326
|
}
|
|
312
327
|
}
|
|
313
328
|
}
|
|
329
|
+
if (end.kind === 'blocked')
|
|
330
|
+
deps.log?.(`lint-fix BLOCKED — ${end.note}`);
|
|
331
|
+
// The CHECK is the arbiter, including after a BLOCKED marker.
|
|
332
|
+
//
|
|
333
|
+
// The marker is scraped last-match-wins out of arbitrary child output, and
|
|
334
|
+
// LINT_FIX_TOOLS carries bash — so the child's own command output is in that
|
|
335
|
+
// text. A child can also genuinely converge and THEN block: `eslint --fix`
|
|
336
|
+
// silently clears the last finding, and the model still reports
|
|
337
|
+
// `LINT-FIX: BLOCKED the generated file is frozen`. Returning not-applied on
|
|
338
|
+
// the marker alone would send the gate to `deps.recommend(...)` with a
|
|
339
|
+
// failReason that no longer describes the tree, skipping the re-verify and
|
|
340
|
+
// burning a full implementation re-run on findings that are already gone —
|
|
341
|
+
// while the child's edits sit in the working tree.
|
|
342
|
+
//
|
|
343
|
+
// So BLOCKED does not decide; it only supplies a better REASON when the check
|
|
344
|
+
// agrees nothing converged. That was the durable half of the win. Skipping the
|
|
345
|
+
// re-run was the other half, and it is not worth this.
|
|
314
346
|
const health = await deps.repoHealth();
|
|
315
347
|
if (!health.ok) {
|
|
348
|
+
if (end.kind === 'blocked') {
|
|
349
|
+
return { ok: false, reason: `fix child blocked: ${end.note}` };
|
|
350
|
+
}
|
|
316
351
|
// FROZEN-PATH TRACE on non-convergence (PROMPT 1 layer B): when the child
|
|
317
352
|
// was honest — it did NOT touch the frozen path, so the guard above never
|
|
318
353
|
// tripped — but the check is still red and its own output NAMES a frozen
|
|
@@ -324,6 +359,7 @@ export async function runBoundedLintFix(deps) {
|
|
|
324
359
|
// rounds an impl re-run under the same freeze cannot converge out of.
|
|
325
360
|
const implicated = frozen.filter(p => pathNamedIn(`${health.reason}\n${health.output ?? ''}`, p));
|
|
326
361
|
if (implicated.length > 0) {
|
|
362
|
+
deps.log?.(`lint-fix FROZEN-PATH TRACE — ${implicated.slice(0, 3).join(', ')}`);
|
|
327
363
|
return {
|
|
328
364
|
ok: false,
|
|
329
365
|
reason: `frozen-path: static findings implicate spec-frozen path(s) `
|
|
@@ -20,6 +20,7 @@ import { type WidgetState } from './widget.js';
|
|
|
20
20
|
import { type RunTaskFn } from './gate-deps.js';
|
|
21
21
|
import { type GateDeps } from './task-gates.js';
|
|
22
22
|
import { type PhaseDeps } from './child-runner.js';
|
|
23
|
+
import { type RunEnd } from './run-end.js';
|
|
23
24
|
import type { SpawnFn } from '../shared/child-process.js';
|
|
24
25
|
import { type SuperviseOptions } from './implementation-turn.js';
|
|
25
26
|
/**
|
|
@@ -45,6 +46,16 @@ export interface TaskRunnerOptions {
|
|
|
45
46
|
* the child is a premise of the test, not its subject.
|
|
46
47
|
*/
|
|
47
48
|
runChild?: PhaseDeps['runChild'];
|
|
49
|
+
/**
|
|
50
|
+
* Test seam: `PhaseDeps.runWorker(label, input)`. Present → every research
|
|
51
|
+
* worker is answered BY NAME, so the three Research retry gates and the
|
|
52
|
+
* fatal/runaway/empty classification are reachable from a runner-driven test
|
|
53
|
+
* without matching a marker sentence inside the prompt.
|
|
54
|
+
*/
|
|
55
|
+
runWorker?: PhaseDeps['runWorker'];
|
|
56
|
+
/** Test seam: the four EXTERNAL CONTEXT lookups plus the file inventory. Each
|
|
57
|
+
* defaults to the real implementation when absent. */
|
|
58
|
+
lookups?: Pick<PhaseDeps, 'getFileInventory' | 'docsRaw' | 'fetchRaw' | 'npmVersionLookup' | 'docsFocused' | 'fetchFocused' | 'searchFn'>;
|
|
48
59
|
/** Called with the resolved task id once its file exists, before any phase
|
|
49
60
|
* work. Lets callers record the id (e.g. stamp the /task-auto entry) so an
|
|
50
61
|
* interrupted run can be resumed instead of restarted. */
|
|
@@ -110,7 +121,13 @@ export declare class TaskRunner {
|
|
|
110
121
|
* armed for the same window (`withRun`); nested inside `runGatedTask` or
|
|
111
122
|
* the `/task-auto` loop the bracket refcounts, so this changes nothing there
|
|
112
123
|
* and covers the fire-and-forget `runSingleTask` path on its own. */
|
|
113
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Run the task and NAME how it ended.
|
|
126
|
+
*
|
|
127
|
+
* This returned `void`, so the caller re-read the task file to learn what had
|
|
128
|
+
* just happened here — see run-end.ts for the report that got wrong.
|
|
129
|
+
*/
|
|
130
|
+
run(): Promise<RunEnd>;
|
|
114
131
|
private _run;
|
|
115
132
|
/** Stop the phase widget — clearing both the terminal and remote surfaces —
|
|
116
133
|
* exactly once. Nulling the disposer makes repeat calls no-ops, so the
|
|
@@ -129,7 +146,7 @@ export declare class TaskRunner {
|
|
|
129
146
|
*/
|
|
130
147
|
private _specForDelivery;
|
|
131
148
|
}
|
|
132
|
-
export interface RunSingleTaskOptions extends Pick<TaskRunnerOptions, 'resumeId' | 'spawnFn' | 'runChild' | 'onStart' | 'planContext' | 'fixInstruction'> {
|
|
149
|
+
export interface RunSingleTaskOptions extends Pick<TaskRunnerOptions, 'resumeId' | 'spawnFn' | 'runChild' | 'runWorker' | 'lookups' | 'onStart' | 'planContext' | 'fixInstruction'> {
|
|
133
150
|
/** Await the session going idle after the spec is delivered, so the caller
|
|
134
151
|
* blocks until the agent has implemented it. Default false. */
|
|
135
152
|
waitForImplementation?: boolean;
|
|
@@ -152,8 +169,15 @@ export interface RunSingleTaskOptions extends Pick<TaskRunnerOptions, 'resumeId'
|
|
|
152
169
|
}
|
|
153
170
|
export interface RunSingleTaskResult {
|
|
154
171
|
taskId: string;
|
|
155
|
-
|
|
156
|
-
|
|
172
|
+
/**
|
|
173
|
+
* How the run ended, named by the runner rather than re-derived from disk.
|
|
174
|
+
*
|
|
175
|
+
* This was `ok: boolean` plus `sessionCancelled`, `interrupted` and `reason`
|
|
176
|
+
* — four fields for one fact, three of them smuggled out of the `withSession`
|
|
177
|
+
* closure through mutable captures. `/task-cancel` fell into the `!ok` arm
|
|
178
|
+
* and was reported as a failure; see run-end.ts.
|
|
179
|
+
*/
|
|
180
|
+
end: RunEnd;
|
|
157
181
|
/**
|
|
158
182
|
* The session context the caller must use for any work after this call. A
|
|
159
183
|
* successful run replaces the session via ctx.newSession(), which leaves the
|
|
@@ -163,30 +187,15 @@ export interface RunSingleTaskResult {
|
|
|
163
187
|
* only so test fakes that don't model session replacement can omit it.
|
|
164
188
|
*/
|
|
165
189
|
ctx?: ExtensionCommandContext;
|
|
166
|
-
/**
|
|
167
|
-
* Set when the user interrupted the implementation (ESC) and then declined to
|
|
168
|
-
* steer (submitted an empty steer prompt) — i.e. they want the run to pause
|
|
169
|
-
* rather than continue. Only meaningful with waitForImplementation. The
|
|
170
|
-
* /task-auto loop reads this to pause (resumable) instead of checking the task
|
|
171
|
-
* off and advancing. A plain ESC that the user follows with steering text does
|
|
172
|
-
* NOT set this — that case loops on the same task until a turn finishes
|
|
173
|
-
* uninterrupted.
|
|
174
|
-
*/
|
|
175
|
-
interrupted?: boolean;
|
|
176
|
-
/**
|
|
177
|
-
* Why the run is not ok, when known. Set when a waitForImplementation turn
|
|
178
|
-
* ended with stopReason "error" (the model/provider died mid-implementation —
|
|
179
|
-
* e.g. a context-overflow 400 — after the task file was already marked
|
|
180
|
-
* `completed` at spec-handoff). The /task-auto loop surfaces this in its
|
|
181
|
-
* "stopped at …" message so the real cause isn't lost. Undefined otherwise.
|
|
182
|
-
*/
|
|
183
|
-
reason?: string;
|
|
184
190
|
}
|
|
185
191
|
/**
|
|
186
192
|
* Run one prompt through the full single-task pipeline in a fresh session and
|
|
187
193
|
* deliver its spec. With waitForImplementation, block until the agent finishes
|
|
188
|
-
* implementing the delivered spec.
|
|
189
|
-
*
|
|
194
|
+
* implementing the delivered spec.
|
|
195
|
+
*
|
|
196
|
+
* The ending comes from `TaskRunner.run`. It used to be read back off the task
|
|
197
|
+
* file's front matter — a disk round-trip this process made to learn what it had
|
|
198
|
+
* just done, and one that could not tell a cancel from a failure.
|
|
190
199
|
*/
|
|
191
200
|
export declare function runSingleTask(ctx: ExtensionCommandContext, cwd: string, rawPrompt: string, opts?: RunSingleTaskOptions): Promise<RunSingleTaskResult>;
|
|
192
201
|
/**
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import * as fsp from 'node:fs/promises';
|
|
19
19
|
import * as path from 'node:path';
|
|
20
|
-
import { PHASES, postCommitPhase } from './phases.js';
|
|
20
|
+
import { PHASES, postCommitPhase, replayPhaseCarry, runPhaseRow } from './phases.js';
|
|
21
21
|
import { handleFailure } from './failure-classifier.js';
|
|
22
22
|
import { PHASE_INDEX, PHASE_ORDER, RESUMABLE_STATES } from './task-types.js';
|
|
23
23
|
import { normaliseTaskId, parseFrontMatter, extractSection } from './task-parsers.js';
|
|
@@ -39,6 +39,7 @@ import { cancelCheckpoint } from './cancel-points.js';
|
|
|
39
39
|
import { rearmCancelListener } from './cancel-input.js';
|
|
40
40
|
import { takeHeldInput } from './mid-run-input.js';
|
|
41
41
|
import { withRun, announceTerminal } from './run-bracket.js';
|
|
42
|
+
import { RUN_END_POLICY, runSucceeded } from './run-end.js';
|
|
42
43
|
import { formatTimings } from './timings.js';
|
|
43
44
|
import { getParentContextWindow, resolveContextUsage } from './context-usage.js';
|
|
44
45
|
import { superviseImplementation } from './implementation-turn.js';
|
|
@@ -111,6 +112,8 @@ export class TaskRunner {
|
|
|
111
112
|
signal: this._abort.signal,
|
|
112
113
|
spawn: opts.spawnFn,
|
|
113
114
|
runChild: opts.runChild,
|
|
115
|
+
runWorker: opts.runWorker,
|
|
116
|
+
...opts.lookups,
|
|
114
117
|
// Deliberately NOT a ChildStatus (child-status.ts): the phase widget's
|
|
115
118
|
// state is the whole-run WidgetState — task id, phase, label — shared by
|
|
116
119
|
// reference with PhaseContext and written by the phases themselves
|
|
@@ -160,6 +163,12 @@ export class TaskRunner {
|
|
|
160
163
|
* armed for the same window (`withRun`); nested inside `runGatedTask` or
|
|
161
164
|
* the `/task-auto` loop the bracket refcounts, so this changes nothing there
|
|
162
165
|
* and covers the fire-and-forget `runSingleTask` path on its own. */
|
|
166
|
+
/**
|
|
167
|
+
* Run the task and NAME how it ended.
|
|
168
|
+
*
|
|
169
|
+
* This returned `void`, so the caller re-read the task file to learn what had
|
|
170
|
+
* just happened here — see run-end.ts for the report that got wrong.
|
|
171
|
+
*/
|
|
163
172
|
async run() {
|
|
164
173
|
return withRun(this._ctx, {}, () => this._run());
|
|
165
174
|
}
|
|
@@ -237,6 +246,13 @@ export class TaskRunner {
|
|
|
237
246
|
const idx = PHASE_INDEX[phase.name];
|
|
238
247
|
if (idx < resumeIdx) {
|
|
239
248
|
this._pc[phase.field] = (await readSection(cwd, id, phase.section)) ?? '';
|
|
249
|
+
// A row's `section` restores exactly ONE field. A phase that also
|
|
250
|
+
// settles another one declares that as its `carry`, and the replay
|
|
251
|
+
// is the only thing standing between a resume and losing it — the
|
|
252
|
+
// task file deliberately stores the PRE-carry text for the field
|
|
253
|
+
// compose rewrites. Trail lines are discarded: the live run that
|
|
254
|
+
// wrote this section already recorded them.
|
|
255
|
+
await replayPhaseCarry(phase, this._deps, this._pc);
|
|
240
256
|
continue;
|
|
241
257
|
}
|
|
242
258
|
await advance(phase.name);
|
|
@@ -246,7 +262,7 @@ export class TaskRunner {
|
|
|
246
262
|
const phaseStart = Date.now();
|
|
247
263
|
let out;
|
|
248
264
|
try {
|
|
249
|
-
out = await phase
|
|
265
|
+
out = await runPhaseRow(phase, this._deps, this._pc);
|
|
250
266
|
}
|
|
251
267
|
finally {
|
|
252
268
|
const phaseMs = Date.now() - phaseStart;
|
|
@@ -279,6 +295,7 @@ export class TaskRunner {
|
|
|
279
295
|
await setTaskSection(cwd, id, 'phase timings', formatTimings(this._timings));
|
|
280
296
|
await setTaskSection(cwd, id, 'handoff', `handoff_at: ${new Date().toISOString()}`);
|
|
281
297
|
await this._deliverSpec(ctx);
|
|
298
|
+
return { kind: 'completed' };
|
|
282
299
|
}
|
|
283
300
|
catch (err) {
|
|
284
301
|
this._disposeWidget();
|
|
@@ -292,7 +309,12 @@ export class TaskRunner {
|
|
|
292
309
|
/* ignore — preserve original failure */
|
|
293
310
|
}
|
|
294
311
|
}
|
|
295
|
-
|
|
312
|
+
// `classifyFailure` already decided whether this was a cancel or a
|
|
313
|
+
// fault; the value is the answer, not a side effect of writing a file.
|
|
314
|
+
const c = await handleFailure(err, ctx, cwd, id, this._abort.signal.aborted);
|
|
315
|
+
return c.state === 'cancelled' ?
|
|
316
|
+
{ kind: 'cancelled' }
|
|
317
|
+
: { kind: 'failed', ...(c.reason === undefined ? {} : { reason: c.reason }) };
|
|
296
318
|
}
|
|
297
319
|
finally {
|
|
298
320
|
this._disposeWidget();
|
|
@@ -374,11 +396,17 @@ export class TaskRunner {
|
|
|
374
396
|
/**
|
|
375
397
|
* Run one prompt through the full single-task pipeline in a fresh session and
|
|
376
398
|
* deliver its spec. With waitForImplementation, block until the agent finishes
|
|
377
|
-
* implementing the delivered spec.
|
|
378
|
-
*
|
|
399
|
+
* implementing the delivered spec.
|
|
400
|
+
*
|
|
401
|
+
* The ending comes from `TaskRunner.run`. It used to be read back off the task
|
|
402
|
+
* file's front matter — a disk round-trip this process made to learn what it had
|
|
403
|
+
* just done, and one that could not tell a cancel from a failure.
|
|
379
404
|
*/
|
|
380
405
|
export async function runSingleTask(ctx, cwd, rawPrompt, opts = {}) {
|
|
381
406
|
let taskId = '';
|
|
407
|
+
// How the runner said it ended. `no-session` until it has run at all — the
|
|
408
|
+
// withSession callback below may never be entered.
|
|
409
|
+
let runEnd = { kind: 'no-session' };
|
|
382
410
|
// The newSession replacement ctx, captured so the caller can keep driving the
|
|
383
411
|
// UI after the original ctx is torn down. Defaults to the original for the
|
|
384
412
|
// cancellation path (where no replacement occurs).
|
|
@@ -422,12 +450,14 @@ export async function runSingleTask(ctx, cwd, rawPrompt, opts = {}) {
|
|
|
422
450
|
},
|
|
423
451
|
spawnFn: opts.spawnFn,
|
|
424
452
|
runChild: opts.runChild,
|
|
453
|
+
runWorker: opts.runWorker,
|
|
454
|
+
lookups: opts.lookups,
|
|
425
455
|
onStart: opts.onStart,
|
|
426
456
|
planContext: opts.planContext,
|
|
427
457
|
fixInstruction: opts.fixInstruction,
|
|
428
458
|
implAwaited: opts.waitForImplementation
|
|
429
459
|
});
|
|
430
|
-
await runner.run();
|
|
460
|
+
runEnd = await runner.run();
|
|
431
461
|
taskId = runner.taskId;
|
|
432
462
|
}
|
|
433
463
|
});
|
|
@@ -436,32 +466,29 @@ export async function runSingleTask(ctx, cwd, rawPrompt, opts = {}) {
|
|
|
436
466
|
if (opts.notifyFinish) {
|
|
437
467
|
void pushNotify('Task finished', `${taskId || 'Task'} cancelled — could not start a session.`, 'pi-end').catch(() => { });
|
|
438
468
|
}
|
|
439
|
-
return { taskId,
|
|
440
|
-
}
|
|
441
|
-
let ok = false;
|
|
442
|
-
let state;
|
|
443
|
-
if (taskId) {
|
|
444
|
-
try {
|
|
445
|
-
const { frontMatter } = await readTaskFile(cwd, taskId);
|
|
446
|
-
state = frontMatter.state;
|
|
447
|
-
ok = state === 'completed';
|
|
448
|
-
}
|
|
449
|
-
catch {
|
|
450
|
-
ok = false;
|
|
451
|
-
}
|
|
469
|
+
return { taskId, end: { kind: 'no-session' }, ctx };
|
|
452
470
|
}
|
|
453
|
-
// The
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
|
|
457
|
-
|
|
471
|
+
// The runner already named the ending. What the SUPERVISION adds is the two
|
|
472
|
+
// endings the runner cannot see, because they happen after the spec is
|
|
473
|
+
// delivered and the task file already reads `completed`:
|
|
474
|
+
//
|
|
475
|
+
// • the user interrupted and declined to steer — a pause, not a fault;
|
|
476
|
+
// • the implementation turn died with stopReason "error" (a context-overflow
|
|
477
|
+
// 400, say), which must stop /task-auto here rather than let it commit and
|
|
478
|
+
// advance on a file that says `completed`.
|
|
479
|
+
//
|
|
480
|
+
// Both used to reach the caller as extra fields beside a boolean, and both are
|
|
481
|
+
// endings.
|
|
482
|
+
let end = runEnd;
|
|
483
|
+
if (end.kind === 'completed' && interrupted)
|
|
484
|
+
end = { kind: 'interrupted' };
|
|
485
|
+
else if (end.kind === 'completed' && implError)
|
|
486
|
+
end = { kind: 'failed', reason: implError };
|
|
458
487
|
if (opts.notifyFinish) {
|
|
459
|
-
// One push per top-level /task or /task-resume, on any terminal end.
|
|
460
|
-
|
|
461
|
-
// 'cancelled' otherwise; an unreadable/absent file falls back to 'ended'.
|
|
462
|
-
void pushNotify('Task finished', `${taskId || 'Task'} ${state ?? 'ended'}.`, 'pi-end').catch(() => { });
|
|
488
|
+
// One push per top-level /task or /task-resume, on any terminal end.
|
|
489
|
+
void pushNotify('Task finished', `${taskId || 'Task'} ${end.kind}.`, 'pi-end').catch(() => { });
|
|
463
490
|
}
|
|
464
|
-
return { taskId,
|
|
491
|
+
return { taskId, end, ctx: freshCtx };
|
|
465
492
|
}
|
|
466
493
|
// ─── Gated single-task flow ────────────────────────────────────────────────────
|
|
467
494
|
/**
|
|
@@ -533,19 +560,21 @@ async function runGatedTaskInner(ctx, cwd, raw, opts = {}) {
|
|
|
533
560
|
const res = await deps.runTask(active, cwd, raw, { resumeId: opts.resumeId });
|
|
534
561
|
active = res.ctx ?? active;
|
|
535
562
|
const tag = res.taskId || 'Task';
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
563
|
+
// One dispatch over the named ending. The four-branch ladder this replaces put
|
|
564
|
+
// a CANCELLED run into the `!ok` arm, so `/task-cancel` overwrote the file's
|
|
565
|
+
// `cancelled` with `failed` and told the user their task had stopped and needed
|
|
566
|
+
// fixing. Resumability comes from RUN_END_POLICY; the wording stays here,
|
|
567
|
+
// because `/task-auto` says `/task-auto-resume` where this says `/task-resume`.
|
|
568
|
+
if (!runSucceeded(res.end)) {
|
|
569
|
+
const policy = RUN_END_POLICY[res.end.kind];
|
|
570
|
+
if (policy.resumable)
|
|
571
|
+
await markResumable(cwd, res.taskId);
|
|
572
|
+
const why = res.end.kind === 'failed' && res.end.reason ? ` — ${res.end.reason.slice(0, 160)}` : '';
|
|
573
|
+
const msg = res.end.kind === 'no-session' ? `${tag} — could not start a fresh session for /task.`
|
|
574
|
+
: res.end.kind === 'cancelled' ? `${tag} cancelled.`
|
|
575
|
+
: res.end.kind === 'interrupted' ? `${tag} paused — resume with /task-resume.`
|
|
576
|
+
: `${tag} stopped${why} — fix and run /task-resume.`;
|
|
577
|
+
announce(msg, policy.level);
|
|
549
578
|
return;
|
|
550
579
|
}
|
|
551
580
|
// The composed task's own front-matter title — used in commit messages and
|
|
@@ -598,8 +627,8 @@ async function handleTask(args, ctx) {
|
|
|
598
627
|
await runGatedTask(ctx, cwd, raw);
|
|
599
628
|
return;
|
|
600
629
|
}
|
|
601
|
-
const {
|
|
602
|
-
if (
|
|
630
|
+
const { end } = await runSingleTask(ctx, cwd, raw, { notifyFinish: true });
|
|
631
|
+
if (end.kind === 'no-session') {
|
|
603
632
|
ctx.ui.notify('Could not start a fresh session for /task.', 'warning');
|
|
604
633
|
}
|
|
605
634
|
}
|
|
@@ -687,8 +716,8 @@ async function handleTaskResume(args, ctx) {
|
|
|
687
716
|
await runGatedTask(ctx, cwd, '', { resumeId: id });
|
|
688
717
|
return;
|
|
689
718
|
}
|
|
690
|
-
const {
|
|
691
|
-
if (
|
|
719
|
+
const { end } = await runSingleTask(ctx, cwd, '', { resumeId: id, notifyFinish: true });
|
|
720
|
+
if (end.kind === 'no-session') {
|
|
692
721
|
ctx.ui.notify('Could not start a fresh session for /task-resume.', 'warning');
|
|
693
722
|
}
|
|
694
723
|
}
|