@mjasnikovs/pi-task 0.38.14 → 0.38.16
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/shared/child-process.js +9 -16
- package/dist/task/accept-debt.d.ts +7 -5
- package/dist/task/accept-debt.js +16 -13
- package/dist/task/auto-orchestrator.js +38 -36
- package/dist/task/autofix-ledger.d.ts +113 -0
- package/dist/task/autofix-ledger.js +152 -0
- package/dist/task/boot-probe.d.ts +63 -1
- package/dist/task/boot-probe.js +98 -2
- 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/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 +2 -7
- package/dist/task/final-gate.d.ts +10 -2
- package/dist/task/final-gate.js +49 -88
- package/dist/task/gate-deps.js +20 -13
- package/dist/task/orchestrator.d.ts +33 -24
- package/dist/task/orchestrator.js +66 -44
- package/dist/task/phases.d.ts +58 -34
- package/dist/task/phases.js +140 -113
- package/dist/task/plan-orchestrator.js +2 -2
- 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/task-gates.d.ts +8 -0
- package/dist/task/task-gates.js +23 -4
- package/dist/task/terminal-outcome.d.ts +1 -1
- package/dist/task/terminal-outcome.js +12 -0
- 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/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 +12 -17
- 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/final-gate.js
CHANGED
|
@@ -51,10 +51,8 @@ import { runRepoHealthCheck, discoverHealthCommands } from './repo-health-check.
|
|
|
51
51
|
import { deriveOpenDebts, rerunDebtVerifyCommand } from './accept-debt.js';
|
|
52
52
|
import { readDeclaredScripts, missingDeclaredScripts, runnableDeclaredScripts } from './launch-contract.js';
|
|
53
53
|
import { readLaunchManifest, inertLaunchContractNote, packageScripts, makeHasTarget } from './launch-manifest.js';
|
|
54
|
-
import { discoverBootCommand, detectsServedApp, runBootCheck, bootSkipVerdict, nonLaunchScriptReason, rejectedLaunchScript, parseSsListeners, parseNetstatListeners, parseLsofListeners, pickFreePort, preferredDeclaredPort, canEnumerateListeners
|
|
54
|
+
import { discoverBootCommand, detectsServedApp, runBootCheck, runBootSection, bootSkipVerdict, nonLaunchScriptReason, rejectedLaunchScript, parseSsListeners, parseNetstatListeners, parseLsofListeners, pickFreePort, preferredDeclaredPort, canEnumerateListeners } from './boot-probe.js';
|
|
55
55
|
import { readEnvNotes, parseEnvNotes, isExcuseNote } from './env-notes.js';
|
|
56
|
-
import { runRenderCheck } from './render-check.js';
|
|
57
|
-
import { runDeepRenderCheck } from './deep-render-check.js';
|
|
58
56
|
import { resolveRunner, runnerEnv } from './runner-resolve.js';
|
|
59
57
|
import { classifyCommandRun, spawnCommand, INFRA_GAP_OUTPUT_RE } from './command-run.js';
|
|
60
58
|
import { findLaunchConfigGap, probeEnv, configGapUnobservedNote } from './launch-config-gap.js';
|
|
@@ -255,19 +253,26 @@ function resolveCommandBody(bin, args, scripts, makefile) {
|
|
|
255
253
|
* passes `extraGapRe` (launch scripts), missing external infrastructure. Only a
|
|
256
254
|
* command that actually ran and exited non-zero for a real reason fails.
|
|
257
255
|
*/
|
|
258
|
-
function runGateCommand(cwd, [bin, args], timeoutMs, extraGapRe,
|
|
256
|
+
async function runGateCommand(cwd, [bin, args], timeoutMs, extraGapRe,
|
|
259
257
|
/** Replaces the child's environment wholesale (config-gap probe re-run only —
|
|
260
258
|
* see launch-config-gap.ts). Absent ⇒ `runnerEnv(runner)`, i.e. unchanged. */
|
|
261
259
|
envOverride,
|
|
262
260
|
/** The spawner. Injected so the gate's own tests can script a verdict. */
|
|
263
|
-
run = spawnCommand) {
|
|
261
|
+
run = spawnCommand, signal) {
|
|
264
262
|
// Runner resolution (mx5 run 16): a login-shell-stripped PATH left `bun`
|
|
265
263
|
// unspawnable, so every dynamic check skipped and the gate went blind. The
|
|
266
264
|
// resolved binary is spawned, and its directory rides on the child's PATH so
|
|
267
265
|
// the SCRIPT CHAIN can re-invoke the runner (`bun run test` runs `bun test`
|
|
268
266
|
// inside — a bare 127 there is the same blindness one level down).
|
|
269
267
|
const runner = resolveRunner(bin);
|
|
270
|
-
const verdict = classifyCommandRun(run({
|
|
268
|
+
const verdict = classifyCommandRun(await run({
|
|
269
|
+
cwd,
|
|
270
|
+
bin: runner.bin,
|
|
271
|
+
args,
|
|
272
|
+
timeoutMs,
|
|
273
|
+
env: envOverride ?? runnerEnv(runner),
|
|
274
|
+
...(signal === undefined ? {} : { signal })
|
|
275
|
+
}), extraGapRe ? [extraGapRe] : []);
|
|
271
276
|
if (verdict.outcome === 'gap') {
|
|
272
277
|
return { outcome: 'skip', spawnFailed: verdict.gap === 'spawn-failed' };
|
|
273
278
|
}
|
|
@@ -288,7 +293,7 @@ export { taskThatIntroduced };
|
|
|
288
293
|
// src/). Re-exported so the seven validation harnesses under scripts/ — which have
|
|
289
294
|
// always imported exactly this surface and nothing else from the gate — keep
|
|
290
295
|
// working unchanged. Same pattern as taskThatIntroduced above.
|
|
291
|
-
export { discoverBootCommand, detectsServedApp, runBootCheck, bootSkipVerdict, nonLaunchScriptReason, rejectedLaunchScript, parseSsListeners, parseNetstatListeners, parseLsofListeners, pickFreePort, preferredDeclaredPort, canEnumerateListeners };
|
|
296
|
+
export { discoverBootCommand, detectsServedApp, runBootCheck, runBootSection, bootSkipVerdict, nonLaunchScriptReason, rejectedLaunchScript, parseSsListeners, parseNetstatListeners, parseLsofListeners, pickFreePort, preferredDeclaredPort, canEnumerateListeners };
|
|
292
297
|
// The ACCEPT-debt re-check (`deriveOpenDebts`, `rerunDebtVerifyCommand`) lives in
|
|
293
298
|
// accept-debt.ts with the ledger it reads and writes; re-exported so the
|
|
294
299
|
// orchestrator and the harnesses under scripts/ keep working unchanged.
|
|
@@ -394,12 +399,18 @@ export async function runFinalIntegrationGate(cwd, opts = {}) {
|
|
|
394
399
|
return inertClosure();
|
|
395
400
|
}
|
|
396
401
|
}, trackedFiles: trackedFilesFn = trackedFiles } = opts;
|
|
397
|
-
|
|
402
|
+
// ASYNC: the run-end gate no longer blocks the event loop for the project's own
|
|
403
|
+
// lint (measured 15s mx5 / 69s aiz-client with zero timer ticks), so a loader can
|
|
404
|
+
// paint and a cancel can reach the child.
|
|
405
|
+
const stat = await runRepoHealthCheck(cwd, {
|
|
406
|
+
run: runCmd,
|
|
407
|
+
...(opts.signal === undefined ? {} : { signal: opts.signal })
|
|
408
|
+
});
|
|
398
409
|
// Debts are derived once, before any section runs, and ride on every verdict
|
|
399
410
|
// shape (GateTally.verdict): `reason` stays the mechanical failure because it
|
|
400
411
|
// seeds the autofix child's prompt — run 11's fix child executed a recorded
|
|
401
412
|
// claim as an instruction.
|
|
402
|
-
const debts = await deriveOpenDebts(cwd, stat.ok);
|
|
413
|
+
const debts = await deriveOpenDebts(cwd, stat.ok, runCmd, opts.signal);
|
|
403
414
|
// Every section below RECORDS into the tally (failures ranked, the four
|
|
404
415
|
// dynamic counters, the notes) and the verdict is assembled ONCE at the end —
|
|
405
416
|
// see gate-tally.ts for what each method means.
|
|
@@ -446,7 +457,7 @@ export async function runFinalIntegrationGate(cwd, opts = {}) {
|
|
|
446
457
|
for (const cmd of list) {
|
|
447
458
|
const label = `${cmd[0]} ${cmd[1].join(' ')}`;
|
|
448
459
|
tally.attempted(cmd[0]);
|
|
449
|
-
const r = runGateCommand(cwd, cmd, timeoutMs, undefined, undefined, runCmd);
|
|
460
|
+
const r = await runGateCommand(cwd, cmd, timeoutMs, undefined, undefined, runCmd, opts.signal);
|
|
450
461
|
if (r.outcome === 'skip') {
|
|
451
462
|
if (r.spawnFailed)
|
|
452
463
|
tally.spawnFailure(cmd[0]);
|
|
@@ -496,7 +507,7 @@ export async function runFinalIntegrationGate(cwd, opts = {}) {
|
|
|
496
507
|
const cmd = ['bun', ['run', name]];
|
|
497
508
|
const label = `${cmd[0]} ${cmd[1].join(' ')}`;
|
|
498
509
|
tally.attempted(cmd[0]);
|
|
499
|
-
const r = runGateCommand(cwd, cmd, launchTimeout, INFRA_GAP_OUTPUT_RE, undefined, runCmd);
|
|
510
|
+
const r = await runGateCommand(cwd, cmd, launchTimeout, INFRA_GAP_OUTPUT_RE, undefined, runCmd, opts.signal);
|
|
500
511
|
if (r.outcome === 'skip') {
|
|
501
512
|
if (r.spawnFailed)
|
|
502
513
|
tally.spawnFailure(cmd[0]);
|
|
@@ -525,7 +536,7 @@ export async function runFinalIntegrationGate(cwd, opts = {}) {
|
|
|
525
536
|
env: process.env
|
|
526
537
|
});
|
|
527
538
|
if (gap) {
|
|
528
|
-
const probe = runGateCommand(cwd, cmd, launchTimeout, INFRA_GAP_OUTPUT_RE, probeEnv(runnerEnv(resolveRunner(cmd[0])), gap), runCmd);
|
|
539
|
+
const probe = await runGateCommand(cwd, cmd, launchTimeout, INFRA_GAP_OUTPUT_RE, probeEnv(runnerEnv(resolveRunner(cmd[0])), gap), runCmd, opts.signal);
|
|
529
540
|
if (probe.outcome === 'pass') {
|
|
530
541
|
// Nothing about this script was OBSERVED: the real run could
|
|
531
542
|
// not reach it and the probe run is a diagnostic, never an
|
|
@@ -585,83 +596,33 @@ export async function runFinalIntegrationGate(cwd, opts = {}) {
|
|
|
585
596
|
// statement about when they are meaningful.
|
|
586
597
|
if (!boot && tally.silent())
|
|
587
598
|
return tally.verdict(debts);
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
};
|
|
611
|
-
let b = await runBootCheck(cwd, boot, bootGraceMs, {
|
|
612
|
-
expectServer,
|
|
613
|
-
deps: bootDepsWithRender
|
|
614
|
-
});
|
|
615
|
-
if (b.outcome === 'orphan-port') {
|
|
616
|
-
b = await recoverOrphanPort(cwd, boot, b, bootGraceMs, bootDepsWithRender, expectServer);
|
|
617
|
-
}
|
|
618
|
-
if (b.outcome !== 'skip')
|
|
619
|
-
tally.observed();
|
|
620
|
-
else if (b.spawnFailed)
|
|
621
|
-
tally.spawnFailure(boot[0]);
|
|
622
|
-
tally.bootUnobserved(bootSkipVerdict({
|
|
623
|
-
label,
|
|
624
|
-
skipped: b.outcome === 'skip',
|
|
625
|
-
expectServer
|
|
626
|
-
}));
|
|
627
|
-
if (b.outcome === 'fail') {
|
|
628
|
-
// OBSERVED (nexttask 19A). Every path that produces `fail` here is a
|
|
629
|
-
// probe that looked: the render judge saw an empty body, the deep
|
|
630
|
-
// session saw the authenticated half dead, the enumerator saw no
|
|
631
|
-
// listener, or the launch command itself exited non-zero. The one
|
|
632
|
-
// condition that means "we could not look" — no ss/netstat/lsof, mx5
|
|
633
|
-
// run 14 — returns PASS stamped UNOBSERVED and never reaches here
|
|
634
|
-
// (`b0f90a7`, final-gate.ts `if (!canEnumerate) return passAndKill(…)`).
|
|
635
|
-
tally.failObserved(`boot check: \`${label}\` ${b.detail}`, 0);
|
|
636
|
-
}
|
|
637
|
-
else if (b.outcome === 'orphan-port') {
|
|
638
|
-
// Could not clear the port. Distinct HARNESS diagnosis, never a bare app
|
|
639
|
-
// FAIL: name the port and (when known) the process squatting on it.
|
|
640
|
-
const holder = b.port !== null ? (bootDeps.findPortHolder ?? defaultFindPortHolder)(b.port) : null;
|
|
641
|
-
const who = holder ? ` — held by an orphaned process (pid ${holder.pid}: ${holder.command})`
|
|
642
|
-
: b.port !== null ? ` — port ${b.port} is held by another process`
|
|
643
|
-
: '';
|
|
644
|
-
tally.fail(`boot check: \`${label}\` could not bind: orphaned process / port already in use${who} (harness condition, not an app fault)`, 0);
|
|
645
|
-
}
|
|
646
|
-
else if (b.outcome === 'pass') {
|
|
647
|
-
tally.ran(label);
|
|
648
|
-
// A listener that served, but whose page could not be OBSERVED to render
|
|
649
|
-
// (no browser, undeterminable port) → UNOBSERVED warning, not a silent pass.
|
|
650
|
-
if (b.renderNote)
|
|
651
|
-
tally.warn(b.renderNote);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
else {
|
|
655
|
-
// Nothing to boot — but if the reason is that the project's only launch
|
|
656
|
-
// script was REJECTED as not-a-launch (2A), that is not the same thing as a
|
|
657
|
-
// project with no launch surface, and it must not degrade into silence.
|
|
658
|
-
const rejected = rejectedLaunchScript(cwd);
|
|
659
|
-
if (rejected && detectsServedApp(cwd, planText)) {
|
|
660
|
-
tally.bootUnobserved(`boot check: this project's only launch script (\`${rejected.name}\`) is not a `
|
|
661
|
-
+ `launch — ${rejected.reason} — so nothing was started and the app was never `
|
|
662
|
-
+ 'observed to run.');
|
|
663
|
-
}
|
|
599
|
+
// The boot CONCEPT lives in boot-probe.ts (runBootSection): discovery,
|
|
600
|
+
// served-app detection, the probe defaults, the boot check, orphan-port
|
|
601
|
+
// recovery, the port-holder diagnosis, the skip verdict and the
|
|
602
|
+
// rejected-launch-script branch. This is the record, and nothing else.
|
|
603
|
+
const bootSection = await runBootSection(cwd, {
|
|
604
|
+
...(planText === undefined ? {} : { planText }),
|
|
605
|
+
...(bootGraceMs === undefined ? {} : { graceMs: bootGraceMs }),
|
|
606
|
+
deps: bootDeps
|
|
607
|
+
});
|
|
608
|
+
if (bootSection.attempted)
|
|
609
|
+
tally.attempted(bootSection.attempted);
|
|
610
|
+
if (bootSection.observed)
|
|
611
|
+
tally.observed();
|
|
612
|
+
else if (bootSection.spawnFailedBin)
|
|
613
|
+
tally.spawnFailure(bootSection.spawnFailedBin);
|
|
614
|
+
tally.bootUnobserved(bootSection.unobservedNote ?? null);
|
|
615
|
+
if (bootSection.failure) {
|
|
616
|
+
const { detail, rank, observed } = bootSection.failure;
|
|
617
|
+
if (observed)
|
|
618
|
+
tally.failObserved(detail, rank);
|
|
619
|
+
else
|
|
620
|
+
tally.fail(detail, rank);
|
|
664
621
|
}
|
|
622
|
+
if (bootSection.ranLabel)
|
|
623
|
+
tally.ran(bootSection.ranLabel);
|
|
624
|
+
for (const w of bootSection.warnings)
|
|
625
|
+
tally.warn(w);
|
|
665
626
|
// Full-skip blindness guard (mx5 run 16): commands were discovered but every
|
|
666
627
|
// one skipped → rank-0 failure, never a static-only PASS. Runner resolvability
|
|
667
628
|
// is checked through resolveRunner so the failure text can name the missing
|
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) => {
|
|
@@ -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
|
/**
|
|
@@ -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
|
}
|
|
@@ -279,6 +288,7 @@ export class TaskRunner {
|
|
|
279
288
|
await setTaskSection(cwd, id, 'phase timings', formatTimings(this._timings));
|
|
280
289
|
await setTaskSection(cwd, id, 'handoff', `handoff_at: ${new Date().toISOString()}`);
|
|
281
290
|
await this._deliverSpec(ctx);
|
|
291
|
+
return { kind: 'completed' };
|
|
282
292
|
}
|
|
283
293
|
catch (err) {
|
|
284
294
|
this._disposeWidget();
|
|
@@ -292,7 +302,12 @@ export class TaskRunner {
|
|
|
292
302
|
/* ignore — preserve original failure */
|
|
293
303
|
}
|
|
294
304
|
}
|
|
295
|
-
|
|
305
|
+
// `classifyFailure` already decided whether this was a cancel or a
|
|
306
|
+
// fault; the value is the answer, not a side effect of writing a file.
|
|
307
|
+
const c = await handleFailure(err, ctx, cwd, id, this._abort.signal.aborted);
|
|
308
|
+
return c.state === 'cancelled' ?
|
|
309
|
+
{ kind: 'cancelled' }
|
|
310
|
+
: { kind: 'failed', ...(c.reason === undefined ? {} : { reason: c.reason }) };
|
|
296
311
|
}
|
|
297
312
|
finally {
|
|
298
313
|
this._disposeWidget();
|
|
@@ -374,11 +389,17 @@ export class TaskRunner {
|
|
|
374
389
|
/**
|
|
375
390
|
* Run one prompt through the full single-task pipeline in a fresh session and
|
|
376
391
|
* deliver its spec. With waitForImplementation, block until the agent finishes
|
|
377
|
-
* implementing the delivered spec.
|
|
378
|
-
*
|
|
392
|
+
* implementing the delivered spec.
|
|
393
|
+
*
|
|
394
|
+
* The ending comes from `TaskRunner.run`. It used to be read back off the task
|
|
395
|
+
* file's front matter — a disk round-trip this process made to learn what it had
|
|
396
|
+
* just done, and one that could not tell a cancel from a failure.
|
|
379
397
|
*/
|
|
380
398
|
export async function runSingleTask(ctx, cwd, rawPrompt, opts = {}) {
|
|
381
399
|
let taskId = '';
|
|
400
|
+
// How the runner said it ended. `no-session` until it has run at all — the
|
|
401
|
+
// withSession callback below may never be entered.
|
|
402
|
+
let runEnd = { kind: 'no-session' };
|
|
382
403
|
// The newSession replacement ctx, captured so the caller can keep driving the
|
|
383
404
|
// UI after the original ctx is torn down. Defaults to the original for the
|
|
384
405
|
// cancellation path (where no replacement occurs).
|
|
@@ -422,12 +443,14 @@ export async function runSingleTask(ctx, cwd, rawPrompt, opts = {}) {
|
|
|
422
443
|
},
|
|
423
444
|
spawnFn: opts.spawnFn,
|
|
424
445
|
runChild: opts.runChild,
|
|
446
|
+
runWorker: opts.runWorker,
|
|
447
|
+
lookups: opts.lookups,
|
|
425
448
|
onStart: opts.onStart,
|
|
426
449
|
planContext: opts.planContext,
|
|
427
450
|
fixInstruction: opts.fixInstruction,
|
|
428
451
|
implAwaited: opts.waitForImplementation
|
|
429
452
|
});
|
|
430
|
-
await runner.run();
|
|
453
|
+
runEnd = await runner.run();
|
|
431
454
|
taskId = runner.taskId;
|
|
432
455
|
}
|
|
433
456
|
});
|
|
@@ -436,32 +459,29 @@ export async function runSingleTask(ctx, cwd, rawPrompt, opts = {}) {
|
|
|
436
459
|
if (opts.notifyFinish) {
|
|
437
460
|
void pushNotify('Task finished', `${taskId || 'Task'} cancelled — could not start a session.`, 'pi-end').catch(() => { });
|
|
438
461
|
}
|
|
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
|
-
}
|
|
462
|
+
return { taskId, end: { kind: 'no-session' }, ctx };
|
|
452
463
|
}
|
|
453
|
-
// The
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
|
|
457
|
-
|
|
464
|
+
// The runner already named the ending. What the SUPERVISION adds is the two
|
|
465
|
+
// endings the runner cannot see, because they happen after the spec is
|
|
466
|
+
// delivered and the task file already reads `completed`:
|
|
467
|
+
//
|
|
468
|
+
// • the user interrupted and declined to steer — a pause, not a fault;
|
|
469
|
+
// • the implementation turn died with stopReason "error" (a context-overflow
|
|
470
|
+
// 400, say), which must stop /task-auto here rather than let it commit and
|
|
471
|
+
// advance on a file that says `completed`.
|
|
472
|
+
//
|
|
473
|
+
// Both used to reach the caller as extra fields beside a boolean, and both are
|
|
474
|
+
// endings.
|
|
475
|
+
let end = runEnd;
|
|
476
|
+
if (end.kind === 'completed' && interrupted)
|
|
477
|
+
end = { kind: 'interrupted' };
|
|
478
|
+
else if (end.kind === 'completed' && implError)
|
|
479
|
+
end = { kind: 'failed', reason: implError };
|
|
458
480
|
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(() => { });
|
|
481
|
+
// One push per top-level /task or /task-resume, on any terminal end.
|
|
482
|
+
void pushNotify('Task finished', `${taskId || 'Task'} ${end.kind}.`, 'pi-end').catch(() => { });
|
|
463
483
|
}
|
|
464
|
-
return { taskId,
|
|
484
|
+
return { taskId, end, ctx: freshCtx };
|
|
465
485
|
}
|
|
466
486
|
// ─── Gated single-task flow ────────────────────────────────────────────────────
|
|
467
487
|
/**
|
|
@@ -533,19 +553,21 @@ async function runGatedTaskInner(ctx, cwd, raw, opts = {}) {
|
|
|
533
553
|
const res = await deps.runTask(active, cwd, raw, { resumeId: opts.resumeId });
|
|
534
554
|
active = res.ctx ?? active;
|
|
535
555
|
const tag = res.taskId || 'Task';
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
556
|
+
// One dispatch over the named ending. The four-branch ladder this replaces put
|
|
557
|
+
// a CANCELLED run into the `!ok` arm, so `/task-cancel` overwrote the file's
|
|
558
|
+
// `cancelled` with `failed` and told the user their task had stopped and needed
|
|
559
|
+
// fixing. Resumability comes from RUN_END_POLICY; the wording stays here,
|
|
560
|
+
// because `/task-auto` says `/task-auto-resume` where this says `/task-resume`.
|
|
561
|
+
if (!runSucceeded(res.end)) {
|
|
562
|
+
const policy = RUN_END_POLICY[res.end.kind];
|
|
563
|
+
if (policy.resumable)
|
|
564
|
+
await markResumable(cwd, res.taskId);
|
|
565
|
+
const why = res.end.kind === 'failed' && res.end.reason ? ` — ${res.end.reason.slice(0, 160)}` : '';
|
|
566
|
+
const msg = res.end.kind === 'no-session' ? `${tag} — could not start a fresh session for /task.`
|
|
567
|
+
: res.end.kind === 'cancelled' ? `${tag} cancelled.`
|
|
568
|
+
: res.end.kind === 'interrupted' ? `${tag} paused — resume with /task-resume.`
|
|
569
|
+
: `${tag} stopped${why} — fix and run /task-resume.`;
|
|
570
|
+
announce(msg, policy.level);
|
|
549
571
|
return;
|
|
550
572
|
}
|
|
551
573
|
// The composed task's own front-matter title — used in commit messages and
|
|
@@ -598,8 +620,8 @@ async function handleTask(args, ctx) {
|
|
|
598
620
|
await runGatedTask(ctx, cwd, raw);
|
|
599
621
|
return;
|
|
600
622
|
}
|
|
601
|
-
const {
|
|
602
|
-
if (
|
|
623
|
+
const { end } = await runSingleTask(ctx, cwd, raw, { notifyFinish: true });
|
|
624
|
+
if (end.kind === 'no-session') {
|
|
603
625
|
ctx.ui.notify('Could not start a fresh session for /task.', 'warning');
|
|
604
626
|
}
|
|
605
627
|
}
|
|
@@ -687,8 +709,8 @@ async function handleTaskResume(args, ctx) {
|
|
|
687
709
|
await runGatedTask(ctx, cwd, '', { resumeId: id });
|
|
688
710
|
return;
|
|
689
711
|
}
|
|
690
|
-
const {
|
|
691
|
-
if (
|
|
712
|
+
const { end } = await runSingleTask(ctx, cwd, '', { resumeId: id, notifyFinish: true });
|
|
713
|
+
if (end.kind === 'no-session') {
|
|
692
714
|
ctx.ui.notify('Could not start a fresh session for /task-resume.', 'warning');
|
|
693
715
|
}
|
|
694
716
|
}
|