@mjasnikovs/pi-task 0.17.26 → 0.18.0
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 +7 -0
- package/dist/config/config.js +2 -1
- package/dist/config/register.js +5 -0
- package/dist/remote/bridge.d.ts +8 -0
- package/dist/remote/bridge.js +3 -1
- package/dist/shared/child-process.d.ts +21 -0
- package/dist/shared/child-process.js +52 -4
- package/dist/shared/model-endpoint.d.ts +8 -0
- package/dist/shared/model-endpoint.js +56 -0
- package/dist/task/auto-orchestrator.d.ts +9 -0
- package/dist/task/auto-orchestrator.js +67 -16
- package/dist/task/child-runner.js +1 -1
- package/dist/task/enforce-guidelines.d.ts +1 -0
- package/dist/task/enforce-guidelines.js +6 -0
- package/dist/task/env-notes.d.ts +23 -0
- package/dist/task/env-notes.js +121 -0
- package/dist/task/final-gate-fix.d.ts +91 -0
- package/dist/task/final-gate-fix.js +182 -0
- package/dist/task/final-gate.d.ts +40 -4
- package/dist/task/final-gate.js +205 -23
- package/dist/task/gate-deps.d.ts +8 -1
- package/dist/task/gate-deps.js +31 -7
- package/dist/task/orchestrator.d.ts +3 -2
- package/dist/task/orchestrator.js +19 -2
- package/dist/task/phases.js +62 -23
- package/dist/task/prompts.d.ts +1 -1
- package/dist/task/prompts.js +8 -1
- package/dist/task/verify-work.d.ts +12 -1
- package/dist/task/verify-work.js +28 -2
- package/dist/task/widget.d.ts +3 -2
- package/dist/task/widget.js +4 -2
- package/dist/workers/pi-worker-core.d.ts +16 -0
- package/dist/workers/pi-worker-core.js +21 -1
- package/package.json +1 -1
package/dist/task/gate-deps.js
CHANGED
|
@@ -19,7 +19,10 @@ import { tasksDir, readTaskFile, appendGateRecord } from './task-io.js';
|
|
|
19
19
|
import { gitCommitAll, gitDropLastCommit, git } from './auto-commit.js';
|
|
20
20
|
import { runGuidelineEnforcement, classifyEnforceChildFailure } from './enforce-guidelines.js';
|
|
21
21
|
import { runWorkVerification, extractSpecForVerification } from './verify-work.js';
|
|
22
|
+
import { readEnvNotes, appendEnvNotes } from './env-notes.js';
|
|
22
23
|
import { runRepoHealthCheck } from './repo-health-check.js';
|
|
24
|
+
import { runFinalIntegrationGate, discoverGateCommandLabels } from './final-gate.js';
|
|
25
|
+
import { runFinalGateAutofix } from './final-gate-fix.js';
|
|
23
26
|
import { researchResolution } from './verify-resolution.js';
|
|
24
27
|
import { findSubstitutionSuspects } from './substitution-probe.js';
|
|
25
28
|
import { runBoundedLintFix } from './lint-fix.js';
|
|
@@ -88,6 +91,13 @@ export function buildGateDeps(params) {
|
|
|
88
91
|
// child run (see git-state-guard.ts). runWorkVerification reads this through its
|
|
89
92
|
// mutationCheck dep to discard a verdict computed on a mutated tree.
|
|
90
93
|
let lastGuardReconcile = null;
|
|
94
|
+
// Restore tracked files to HEAD and drop files a pass created; the .pi-tasks
|
|
95
|
+
// trail/log writes made during the pass survive both. Shared by the enforce
|
|
96
|
+
// pre-commit gate (discardEdits) and the final-gate autofix shrink guard.
|
|
97
|
+
const discardTreeEdits = async (cwd2) => {
|
|
98
|
+
await git(cwd2, ['checkout', '--', '.', EXCLUDE_TASKS_DIR], signal);
|
|
99
|
+
await git(cwd2, ['clean', '-fd', '-e', '.pi-tasks'], signal);
|
|
100
|
+
};
|
|
91
101
|
// Shared runner for the per-task GATE children (verify + post-FAIL recommend).
|
|
92
102
|
// Both are read-only passes of the same local model that must run to completion:
|
|
93
103
|
// unguarded (no wall-clock timeout, exact-match loop guard only, path-revisit
|
|
@@ -285,7 +295,15 @@ export function buildGateDeps(params) {
|
|
|
285
295
|
// guard already restored the state — see git-state-guard.ts).
|
|
286
296
|
mutationCheck: () => lastGuardReconcile?.mutated ?
|
|
287
297
|
{ mutated: true, detail: lastGuardReconcile.actions.join('; ') }
|
|
288
|
-
: { mutated: false, detail: '' }
|
|
298
|
+
: { mutated: false, detail: '' },
|
|
299
|
+
// Per-run environment-facts cache under .pi-tasks/ (survives
|
|
300
|
+
// discardEdits): earlier children's discoveries save this child
|
|
301
|
+
// the re-archaeology; its own ENV-NOTE lines are stored for the
|
|
302
|
+
// next one. Facts only — verdict rules unaffected.
|
|
303
|
+
envNotes: {
|
|
304
|
+
read: () => readEnvNotes(cwd2),
|
|
305
|
+
append: notes => appendEnvNotes(cwd2, notes)
|
|
306
|
+
}
|
|
289
307
|
});
|
|
290
308
|
},
|
|
291
309
|
lintFix: (fixCtx, cwd2, taskTitle, failReason) => runBoundedLintFix({
|
|
@@ -305,12 +323,18 @@ export function buildGateDeps(params) {
|
|
|
305
323
|
const r = await git(cwd2, ['status', '--porcelain', '--', '.', EXCLUDE_TASKS_DIR], signal);
|
|
306
324
|
return r.exitCode === 0 && r.stdout.trim().length > 0;
|
|
307
325
|
},
|
|
308
|
-
discardEdits:
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
326
|
+
discardEdits: discardTreeEdits,
|
|
327
|
+
finalGateFix: (fixCtx, cwd2, failReason) => runFinalGateAutofix({
|
|
328
|
+
cwd: cwd2,
|
|
329
|
+
signal,
|
|
330
|
+
failReason,
|
|
331
|
+
runChild: makeGateChild(fixCtx, cwd2, 'final integration gate', 'final-fix', 'final-gate-debug.log'),
|
|
332
|
+
// The gate re-run is the only arbiter of convergence, and the
|
|
333
|
+
// shrink guard's discovery is the gate's own (see final-gate.ts).
|
|
334
|
+
gate: c => runFinalIntegrationGate(c),
|
|
335
|
+
discoverLabels: discoverGateCommandLabels,
|
|
336
|
+
discard: discardTreeEdits
|
|
337
|
+
}),
|
|
314
338
|
recommend: async (recCtx, cwd2, taskTitle, taskId, failReason) => {
|
|
315
339
|
// Read the same composed spec the verify gate judged against, so the
|
|
316
340
|
// recommendation reasons over the real contract (degrade to the bare title).
|
|
@@ -92,8 +92,9 @@ export interface RunSingleTaskOptions {
|
|
|
92
92
|
* Ask the user for a steering message after they interrupt (ESC) the
|
|
93
93
|
* implementation turn. Return text to continue the same task as another turn,
|
|
94
94
|
* or undefined/empty to pause the run. Only consulted with
|
|
95
|
-
* waitForImplementation. Defaults to a
|
|
96
|
-
*
|
|
95
|
+
* waitForImplementation. Defaults to a bridged SessionUI.ask (local TUI input
|
|
96
|
+
* raced against a remote browser card); injectable so the steer loop is
|
|
97
|
+
* testable without a real dialog.
|
|
97
98
|
*/
|
|
98
99
|
promptSteer?: (ctx: ExtensionCommandContext) => Promise<string | undefined>;
|
|
99
100
|
/**
|
|
@@ -25,7 +25,7 @@ import { readTextFile } from '../shared/fs-text.js';
|
|
|
25
25
|
import { allocateTaskId, ensureTasksDir, readSection, readTaskFile, setTaskSection, taskFilePath, tasksDir, updateTaskFrontMatter, writeTaskFile } from './task-io.js';
|
|
26
26
|
import { startWidget } from './widget.js';
|
|
27
27
|
import { armImplWidget, disarmImplWidget, setupImplWidget } from './impl-widget.js';
|
|
28
|
-
import { publishViewer, publishNotify, publishLifecycleNotice, registerBridgeCommand, getBridge } from '../remote/bridge.js';
|
|
28
|
+
import { publishViewer, publishNotify, publishLifecycleNotice, registerBridgeCommand, getBridge, SessionUI } from '../remote/bridge.js';
|
|
29
29
|
import { pushNotify } from '../remote/push.js';
|
|
30
30
|
import { getConfig } from '../config/config.js';
|
|
31
31
|
import { buildGateDeps } from './gate-deps.js';
|
|
@@ -342,6 +342,11 @@ export class TaskRunner {
|
|
|
342
342
|
/** Dialog copy for the post-interrupt steering prompt. */
|
|
343
343
|
const STEER_TITLE = 'Paused — steer the model';
|
|
344
344
|
const STEER_PLACEHOLDER = 'Type guidance to continue this task, or leave empty to pause';
|
|
345
|
+
/** Remote-card copy for the same prompt. The browser has no placeholder ghost
|
|
346
|
+
* text, so the pause affordance must be spelled out in the question itself
|
|
347
|
+
* (Skip = empty answer = pause, same as an empty local submit). */
|
|
348
|
+
const STEER_QUESTION = 'Paused — the implementation was interrupted.\n'
|
|
349
|
+
+ 'Type guidance to continue this task, or Skip to pause the run.';
|
|
345
350
|
/**
|
|
346
351
|
* True when the most recent assistant turn ended because the user interrupted it
|
|
347
352
|
* (pressed ESC). pi records a user abort as stopReason "aborted" on the assistant
|
|
@@ -472,7 +477,19 @@ export async function resumeAcrossCompactions(ctx) {
|
|
|
472
477
|
* should pause; false when the implementation completed (steered or not).
|
|
473
478
|
*/
|
|
474
479
|
async function steerUntilDone(ctx, promptSteer) {
|
|
475
|
-
|
|
480
|
+
// Fan the prompt out through the bridge (local TUI input + remote browser
|
|
481
|
+
// card, first answer wins) instead of a raw ctx.ui.input: an interrupt can
|
|
482
|
+
// come from the remote Stop button just as well as a terminal ESC, and a
|
|
483
|
+
// terminal-only dialog leaves the remote viewer staring at a silently
|
|
484
|
+
// paused run. Remote Skip returns '' → same pause path as an empty local
|
|
485
|
+
// submit.
|
|
486
|
+
const ask = promptSteer
|
|
487
|
+
?? (c => new SessionUI(c).ask({
|
|
488
|
+
localTitle: STEER_TITLE,
|
|
489
|
+
localPlaceholder: STEER_PLACEHOLDER,
|
|
490
|
+
question: STEER_QUESTION,
|
|
491
|
+
allowSkip: true
|
|
492
|
+
}));
|
|
476
493
|
while (wasInterrupted(ctx)) {
|
|
477
494
|
const steer = await ask(ctx);
|
|
478
495
|
if (steer === undefined || steer.trim().length === 0)
|
package/dist/task/phases.js
CHANGED
|
@@ -318,10 +318,10 @@ export async function phaseResearch(deps, refined, researchDeps = {}) {
|
|
|
318
318
|
}
|
|
319
319
|
};
|
|
320
320
|
// Per-worker timing split into wait (spawn → first byte) and work (first
|
|
321
|
-
// byte → exit).
|
|
321
|
+
// byte → exit). With the default serial execution each split is a clean
|
|
322
322
|
// per-worker measurement — waitMs the worker's own cold-start, workMs its
|
|
323
|
-
// generation+tool-call cost
|
|
324
|
-
//
|
|
323
|
+
// generation+tool-call cost. Under the opt-in parallel mode the numbers are
|
|
324
|
+
// wall-clock-relative (queueing shows up in waitMs).
|
|
325
325
|
const recordWorker = (label, p) => p.then(r => {
|
|
326
326
|
deps.recordSubStep?.(`${label} wait`, r.waitMs);
|
|
327
327
|
deps.recordSubStep?.(`${label} work`, r.workMs);
|
|
@@ -337,7 +337,9 @@ export async function phaseResearch(deps, refined, researchDeps = {}) {
|
|
|
337
337
|
// each other ~4x (context worker measured 27s solo vs 128s under load),
|
|
338
338
|
// so summed-but-fast (~100s) beats max-of-slowed (~130s).
|
|
339
339
|
// Every worker runs /no_think (below), so sequential is the faster regime.
|
|
340
|
-
// Do NOT switch
|
|
340
|
+
// Do NOT switch the DEFAULT back to concurrent without re-running that A/B;
|
|
341
|
+
// the opt-in `parallelResearchWorkers` config flag exists for backends that
|
|
342
|
+
// genuinely serve parallel streams.
|
|
341
343
|
//
|
|
342
344
|
// `/no_think` is the big win: these are agentic exploration loops, and on a
|
|
343
345
|
// reasoning model the child would otherwise emit a full <think> trace at
|
|
@@ -358,9 +360,13 @@ export async function phaseResearch(deps, refined, researchDeps = {}) {
|
|
|
358
360
|
label: 'worker:apis',
|
|
359
361
|
// Read-heavy: gets the orientation core (see note above). Search/fetch
|
|
360
362
|
// ride along only when a Brave key exists — see SEARCH_EXTENSION_PATH.
|
|
361
|
-
|
|
363
|
+
// FILES' finished map rides along when available (serial default), so
|
|
364
|
+
// the worker doesn't re-derive where-things-live via docs-"."
|
|
365
|
+
// queries the FILES worker just answered (run-7 F7: up to 10
|
|
366
|
+
// duplicate `.`-decodes per task through the serial bottleneck).
|
|
367
|
+
prompt: prior => appendNoThink(orientation.block
|
|
362
368
|
+ promptHeader
|
|
363
|
-
+ RESEARCH_APIS_PROMPT(refined)
|
|
369
|
+
+ RESEARCH_APIS_PROMPT(refined, prior.find(s => s.name === 'FILES')?.text || undefined)
|
|
364
370
|
+ (searchConfigured() ? RESEARCH_SEARCH_HINT : '')),
|
|
365
371
|
tools: 'read,grep,find,ls,pi-worker-docs'
|
|
366
372
|
+ (searchConfigured() ? ',pi-worker-search,pi-worker-fetch' : ''),
|
|
@@ -390,25 +396,36 @@ export async function phaseResearch(deps, refined, researchDeps = {}) {
|
|
|
390
396
|
extensions: [SINGLE_READ_EXTENSION_PATH]
|
|
391
397
|
}
|
|
392
398
|
];
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
399
|
+
// Persisting a worker's section is a read-modify-write of the shared task
|
|
400
|
+
// file, so writes are chained through one lock — a no-op in serial mode,
|
|
401
|
+
// load-bearing in parallel mode where two workers can settle together.
|
|
402
|
+
let persistChain = Promise.resolve();
|
|
403
|
+
const persistSection = (heading, text) => {
|
|
404
|
+
const next = persistChain.then(() => setTaskSection(deps.cwd, deps.taskId, heading, text));
|
|
405
|
+
persistChain = next.catch(() => { });
|
|
406
|
+
return next;
|
|
407
|
+
};
|
|
408
|
+
// One worker, cache-skip to persist: on a resume, a worker whose cached
|
|
409
|
+
// output is already on disk is skipped — so when one worker fails and the
|
|
410
|
+
// phase is re-run, the others don't burn minutes regenerating work that was
|
|
411
|
+
// already good. Each worker is validated inline (not in a second pass), so
|
|
412
|
+
// only trustworthy text is ever cached.
|
|
413
|
+
//
|
|
414
|
+
// A fatal failure (crash/empty/leak) still throws — the already-cached
|
|
415
|
+
// workers survive for the resume. A runaway (loop/timeout) degrades to its
|
|
416
|
+
// partial output instead, so one weak worker can't abort a whole auto-run;
|
|
417
|
+
// the degraded section is cached too, so a resume doesn't re-loop it.
|
|
418
|
+
const runSpec = async (spec, prior) => {
|
|
401
419
|
const cacheHeading = researchWorkerCacheHeading(spec.section);
|
|
402
420
|
const cached = (await readSection(deps.cwd, deps.taskId, cacheHeading)) ?? '';
|
|
403
421
|
if (cached.trim().length > 0) {
|
|
404
422
|
deps.logDebug?.(`${spec.label}: cached — skipping re-run`);
|
|
405
423
|
updateProgress();
|
|
406
|
-
|
|
407
|
-
continue;
|
|
424
|
+
return { name: spec.section, text: cached.trim() };
|
|
408
425
|
}
|
|
409
426
|
deps.logDebug?.(`${spec.label}: start`);
|
|
410
427
|
const r = await recordWorker(spec.label, runWorker({
|
|
411
|
-
prompt: spec.prompt,
|
|
428
|
+
prompt: typeof spec.prompt === 'function' ? spec.prompt(prior) : spec.prompt,
|
|
412
429
|
cwd: deps.cwd,
|
|
413
430
|
signal: deps.signal,
|
|
414
431
|
spawn: deps.spawn,
|
|
@@ -423,10 +440,6 @@ export async function phaseResearch(deps, refined, researchDeps = {}) {
|
|
|
423
440
|
+ (r.stderr ? ` stderr=${r.stderr.slice(0, 300)}` : '')
|
|
424
441
|
+ (r.leakedToolCall ? ` leaked=${r.leakedToolCall.trim().slice(0, 80)}` : ''));
|
|
425
442
|
updateProgress();
|
|
426
|
-
// A fatal failure (crash/empty/leak) still throws — the already-cached
|
|
427
|
-
// workers survive for the resume. A runaway (loop/timeout) degrades to its
|
|
428
|
-
// partial output instead, so one weak worker can't abort a whole auto-run;
|
|
429
|
-
// the degraded section is cached too, so a resume doesn't re-loop it.
|
|
430
443
|
const failure = classifyResearchWorker(spec.section, r);
|
|
431
444
|
if (failure?.kind === 'fatal')
|
|
432
445
|
throw failure.error;
|
|
@@ -436,8 +449,34 @@ export async function phaseResearch(deps, refined, researchDeps = {}) {
|
|
|
436
449
|
if (failure?.kind === 'runaway') {
|
|
437
450
|
deps.logDebug?.(`${spec.label}: degraded — ${failure.reason}`);
|
|
438
451
|
}
|
|
439
|
-
await
|
|
440
|
-
|
|
452
|
+
await persistSection(cacheHeading, sectionText);
|
|
453
|
+
return { name: spec.section, text: sectionText };
|
|
454
|
+
};
|
|
455
|
+
const sections = [];
|
|
456
|
+
if (!getConfig().parallelResearchWorkers) {
|
|
457
|
+
// Default: ONE AT A TIME (see the A/B note above the specs) — a fatal
|
|
458
|
+
// failure throws before later workers run, and each worker can see the
|
|
459
|
+
// finished sections before it (APIS builds on the FILES map).
|
|
460
|
+
for (const spec of workerSpecs) {
|
|
461
|
+
sections.push(await runSpec(spec, sections));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
// Opt-in for parallel-capable backends. allSettled (not all): every
|
|
466
|
+
// worker runs to its own outcome first, so one fatal failure cannot
|
|
467
|
+
// orphan the others' output — their sections persist for the resume
|
|
468
|
+
// before the failure is thrown. Assembly order stays the spec order
|
|
469
|
+
// regardless of completion order. No prior sections exist here, so
|
|
470
|
+
// prompt builders get none (APIS runs map-less, as before this option).
|
|
471
|
+
const settled = await Promise.allSettled(workerSpecs.map(spec => runSpec(spec, [])));
|
|
472
|
+
for (const s of settled) {
|
|
473
|
+
if (s.status === 'rejected')
|
|
474
|
+
throw s.reason;
|
|
475
|
+
}
|
|
476
|
+
for (const s of settled) {
|
|
477
|
+
if (s.status === 'fulfilled')
|
|
478
|
+
sections.push(s.value);
|
|
479
|
+
}
|
|
441
480
|
}
|
|
442
481
|
// All workers succeeded — the assembled output below becomes the canonical
|
|
443
482
|
// 'research' section (written by the orchestrator). The per-worker caches
|
package/dist/task/prompts.d.ts
CHANGED
|
@@ -50,7 +50,7 @@ export declare const COMPRESS_LABEL_PROMPT: (title: string, maxChars: number) =>
|
|
|
50
50
|
declare const REFINE_PROMPT: (raw: string, planContext?: string, existingFiles?: string) => string;
|
|
51
51
|
declare const RESEARCH_READ_ONLY_CONSTRAINT = "IMPORTANT: You are ONLY allowed to READ. Do NOT create, modify, or delete any files. Use the read, grep, find, and ls tools to inspect the repo.";
|
|
52
52
|
declare const RESEARCH_FILES_PROMPT: (refined: string) => string;
|
|
53
|
-
declare const RESEARCH_APIS_PROMPT: (refined: string) => string;
|
|
53
|
+
declare const RESEARCH_APIS_PROMPT: (refined: string, filesMap?: string) => string;
|
|
54
54
|
declare const RESEARCH_CONTEXT_PROMPT: (refined: string) => string;
|
|
55
55
|
declare const RESEARCH_TOOLING_PROMPT: (refined: string) => string;
|
|
56
56
|
declare const GRILL_GEN_PROMPT: (refined: string, research: string, priorQA: string) => string;
|
package/dist/task/prompts.js
CHANGED
|
@@ -122,7 +122,7 @@ No section header. No other sections. No preamble.
|
|
|
122
122
|
|
|
123
123
|
Task:
|
|
124
124
|
${refined}`;
|
|
125
|
-
const RESEARCH_APIS_PROMPT = (refined) => `You are doing targeted research for an AI coding agent. Use the read, grep, find, and ls tools — and \`pi-worker-docs\` for installed npm packages — to identify the commands, functions, types, and interfaces the agent will use for the following task.
|
|
125
|
+
const RESEARCH_APIS_PROMPT = (refined, filesMap) => `You are doing targeted research for an AI coding agent. Use the read, grep, find, and ls tools — and \`pi-worker-docs\` for installed npm packages — to identify the commands, functions, types, and interfaces the agent will use for the following task.
|
|
126
126
|
|
|
127
127
|
NPM PACKAGES — use pi-worker-docs, NOT file reads: for any third-party npm package (e.g. "zod", "hono", "drizzle-orm"), call \`pi-worker-docs(module, query)\` to get its type signatures and API surface. Do NOT open node_modules source files directly — those reads are expensive and produce far more noise than the tool. The tool returns a compact, focused excerpt in a fraction of the token cost.
|
|
128
128
|
|
|
@@ -133,7 +133,14 @@ RUNTIME BUILTINS — verify, do NOT echo: a task (or the spec doc it references)
|
|
|
133
133
|
APIS owns symbols and commands BY NAME ONLY. Do NOT include any file path or path fragment — no \`package.json\`, no \`./src/foo.ts\`, no \`package.json#scripts.lint\`. If the symbol is a script defined in package.json, write the invocation (\`npm run lint\`), not its location. If the symbol is a config file, it does not belong in APIS at all — it belongs in FILES.
|
|
134
134
|
|
|
135
135
|
RELEVANCE — read carefully: list ONLY the symbols the agent will call, implement, modify, or directly depend on for THIS task. Do NOT enumerate the project's entire public surface or dump every exported function in a touched file. A symbol unrelated to the task does not belong here just because it sits in the same module. Keep the smallest sufficient set: include every symbol the task actually exercises and nothing more. There is no fixed limit — list as many as the task truly needs and no padding beyond that.
|
|
136
|
+
${filesMap ?
|
|
137
|
+
`
|
|
138
|
+
PROJECT FILE MAP — already surveyed for this task by a prior worker (authoritative):
|
|
139
|
+
${filesMap}
|
|
136
140
|
|
|
141
|
+
USE THE MAP: where things live is ALREADY ANSWERED above. Do NOT re-derive it — never call \`pi-worker-docs(".", …)\` (or grep/find) for a question the map already answers: which file holds X, whether a path exists, what a file is for. Reserve \`.\`-queries for symbol-level facts the map cannot carry — signatures, parameter and return types, what a module exports. Go straight to the mapped files' symbols.
|
|
142
|
+
`
|
|
143
|
+
: ''}
|
|
137
144
|
${RESEARCH_INPUTS_NOT_DELIVERABLE}
|
|
138
145
|
|
|
139
146
|
${RESEARCH_READ_ONLY_CONSTRAINT}
|
|
@@ -42,7 +42,7 @@ export declare function extractSpecForVerification(taskBody: string): string | n
|
|
|
42
42
|
* pure git shape (test files the task itself changed), so the mandate is
|
|
43
43
|
* language- and framework-agnostic.
|
|
44
44
|
*/
|
|
45
|
-
export declare function buildVerifyPrompt(spec: string, probeFindings?: string[]): string;
|
|
45
|
+
export declare function buildVerifyPrompt(spec: string, probeFindings?: string[], envNotes?: string): string;
|
|
46
46
|
/**
|
|
47
47
|
* Parse the child's verdict. Scans for the LAST `WORK-VERIFIED: PASS|FAIL` marker
|
|
48
48
|
* (the model discusses before concluding, and bash output may echo the word
|
|
@@ -94,6 +94,17 @@ export interface VerificationDeps {
|
|
|
94
94
|
mutated: boolean;
|
|
95
95
|
detail: string;
|
|
96
96
|
};
|
|
97
|
+
/**
|
|
98
|
+
* Per-run environment-facts cache (see env-notes.ts): `read` supplies the
|
|
99
|
+
* facts earlier gate children discovered (inlined into the prompt with the
|
|
100
|
+
* no-waiver caveat); `append` stores the `ENV-NOTE:` lines this child
|
|
101
|
+
* emitted, host-side. ABSENT → no block, no capture (tests unchanged).
|
|
102
|
+
* Facts only cut re-discovery time; verdict rules are unaffected.
|
|
103
|
+
*/
|
|
104
|
+
envNotes?: {
|
|
105
|
+
read: () => Promise<string>;
|
|
106
|
+
append: (notes: string[]) => Promise<void>;
|
|
107
|
+
};
|
|
97
108
|
}
|
|
98
109
|
/**
|
|
99
110
|
* Run the verification pass for one task. A missing spec is a pass. Otherwise run
|
package/dist/task/verify-work.js
CHANGED
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
* verify, this is a pass (ok: true).
|
|
69
69
|
*/
|
|
70
70
|
import { USER_CANCELLED } from './child-runner.js';
|
|
71
|
+
import { buildEnvNotesBlock, ENV_NOTE_EMIT_INSTRUCTION, extractEnvNotes } from './env-notes.js';
|
|
71
72
|
/**
|
|
72
73
|
* The verification child gets exactly two tools: `read` and `bash`.
|
|
73
74
|
*
|
|
@@ -123,7 +124,7 @@ export function extractSpecForVerification(taskBody) {
|
|
|
123
124
|
* pure git shape (test files the task itself changed), so the mandate is
|
|
124
125
|
* language- and framework-agnostic.
|
|
125
126
|
*/
|
|
126
|
-
export function buildVerifyPrompt(spec, probeFindings) {
|
|
127
|
+
export function buildVerifyPrompt(spec, probeFindings, envNotes) {
|
|
127
128
|
const probeBlock = probeFindings && probeFindings.length > 0 ?
|
|
128
129
|
[
|
|
129
130
|
'SELF-VERIFICATION NOTICE (deterministic, computed by the orchestrator from the diff):',
|
|
@@ -137,6 +138,7 @@ export function buildVerifyPrompt(spec, probeFindings) {
|
|
|
137
138
|
''
|
|
138
139
|
]
|
|
139
140
|
: [];
|
|
141
|
+
const envBlock = envNotes && envNotes.trim().length > 0 ? [buildEnvNotesBlock(envNotes)] : [];
|
|
140
142
|
return [
|
|
141
143
|
'You are a strict verification pass running right after an AI coding agent',
|
|
142
144
|
'finished a task and committed it. The agent is known to mark work "done"',
|
|
@@ -150,6 +152,7 @@ export function buildVerifyPrompt(spec, probeFindings) {
|
|
|
150
152
|
'THE TASK SPEC (its ACCEPTANCE criteria and VERIFY block are the contract):',
|
|
151
153
|
spec.trim(),
|
|
152
154
|
'',
|
|
155
|
+
...envBlock,
|
|
153
156
|
...probeBlock,
|
|
154
157
|
'How to verify — verify the REAL, shipped deliverable exactly as an unaided fresh',
|
|
155
158
|
'checkout (or CI run) would experience it:',
|
|
@@ -249,6 +252,8 @@ export function buildVerifyPrompt(spec, probeFindings) {
|
|
|
249
252
|
'a required behavior missing — the verdict is FAIL, even if typecheck and lint are green',
|
|
250
253
|
'and even if the gap seems minor. Never downgrade an unmet criterion to a warning note.',
|
|
251
254
|
'',
|
|
255
|
+
ENV_NOTE_EMIT_INSTRUCTION,
|
|
256
|
+
'',
|
|
252
257
|
'When you are done, output EXACTLY ONE of these as the final line:',
|
|
253
258
|
" WORK-VERIFIED: PASS (the project's own command, run unaided, met the spec)",
|
|
254
259
|
' WORK-VERIFIED: FAIL <text> (the shipped command failed or did not meet the spec; say what failed)',
|
|
@@ -305,6 +310,17 @@ export async function runWorkVerification(deps) {
|
|
|
305
310
|
findings = [];
|
|
306
311
|
}
|
|
307
312
|
}
|
|
313
|
+
// Environment facts from earlier gate children (best-effort; a cache failure
|
|
314
|
+
// must never block verification).
|
|
315
|
+
let envNotes = '';
|
|
316
|
+
if (deps.envNotes) {
|
|
317
|
+
try {
|
|
318
|
+
envNotes = await deps.envNotes.read();
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
envNotes = '';
|
|
322
|
+
}
|
|
323
|
+
}
|
|
308
324
|
// A child that emits NO verdict never judged the work (budget/context death mid-
|
|
309
325
|
// investigation — seen live: an 11-minute verify wandered, died verdict-less, and
|
|
310
326
|
// the resulting FAIL burned a full implementation re-run on an unjudged artifact).
|
|
@@ -312,7 +328,7 @@ export async function runWorkVerification(deps) {
|
|
|
312
328
|
for (let attempt = 1;; attempt++) {
|
|
313
329
|
let text;
|
|
314
330
|
try {
|
|
315
|
-
text = await deps.runChild(VERIFY_TOOLS, buildVerifyPrompt(deps.spec, findings), deps.signal);
|
|
331
|
+
text = await deps.runChild(VERIFY_TOOLS, buildVerifyPrompt(deps.spec, findings, envNotes), deps.signal);
|
|
316
332
|
}
|
|
317
333
|
catch (err) {
|
|
318
334
|
if (err instanceof Error && err.message === USER_CANCELLED)
|
|
@@ -320,6 +336,16 @@ export async function runWorkVerification(deps) {
|
|
|
320
336
|
const msg = err instanceof Error ? err.message : String(err);
|
|
321
337
|
return { ok: false, reason: `verification pass could not run: ${msg}` };
|
|
322
338
|
}
|
|
339
|
+
// Capture the environment facts the child shared — regardless of verdict
|
|
340
|
+
// (a FAIL run's discoveries are just as reusable).
|
|
341
|
+
if (deps.envNotes) {
|
|
342
|
+
try {
|
|
343
|
+
await deps.envNotes.append(extractEnvNotes(text));
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// best-effort cache
|
|
347
|
+
}
|
|
348
|
+
}
|
|
323
349
|
// A child that mutated the repo (git-state guard fired) judged a tree it had
|
|
324
350
|
// itself changed — its verdict is meaningless in both directions, so discard
|
|
325
351
|
// it BEFORE parsing. The guard already restored the state, so one retry runs
|
package/dist/task/widget.d.ts
CHANGED
|
@@ -57,8 +57,9 @@ export interface AutoLoaderState {
|
|
|
57
57
|
* pass and 'verify' is the per-task work-verification pass, neither of which
|
|
58
58
|
* has step numbering. 'recommend' is the read-only research that picks the
|
|
59
59
|
* recommended action after a verify FAIL. 'lint-fix' is the bounded fix pass
|
|
60
|
-
* for a repo-health verify FAIL
|
|
61
|
-
|
|
60
|
+
* for a repo-health verify FAIL; 'final-fix' the bounded fix pass for a
|
|
61
|
+
* final-integration-gate FAIL. */
|
|
62
|
+
kind?: 'planning' | 'enforce' | 'verify' | 'recommend' | 'lint-fix' | 'final-fix';
|
|
62
63
|
}
|
|
63
64
|
export declare function buildAutoLoaderLines(s: AutoLoaderState, theme?: WidgetTheme): string[];
|
|
64
65
|
/** Structured mirror of buildAutoLoaderLines. Only the numbered planning stage
|
package/dist/task/widget.js
CHANGED
|
@@ -148,7 +148,8 @@ export function buildAutoLoaderLines(s, theme) {
|
|
|
148
148
|
: s.kind === 'verify' ? `verifying work · ${elapsed}`
|
|
149
149
|
: s.kind === 'recommend' ? `assessing the failure · ${elapsed}`
|
|
150
150
|
: s.kind === 'lint-fix' ? `fixing static findings · ${elapsed}`
|
|
151
|
-
:
|
|
151
|
+
: s.kind === 'final-fix' ? `fixing the final gate · ${elapsed}`
|
|
152
|
+
: `planning ${s.stepNum}/${s.stepTotal} ${s.step} · ${elapsed}`;
|
|
152
153
|
if (s.contextUsage) {
|
|
153
154
|
const ctxDetail = formatContextDetail(s.contextUsage, theme);
|
|
154
155
|
if (ctxDetail)
|
|
@@ -167,7 +168,8 @@ export function buildAutoLoaderData(s) {
|
|
|
167
168
|
: s.kind === 'verify' ? 'verifying work'
|
|
168
169
|
: s.kind === 'recommend' ? 'assessing the failure'
|
|
169
170
|
: s.kind === 'lint-fix' ? 'fixing static findings'
|
|
170
|
-
: s.
|
|
171
|
+
: s.kind === 'final-fix' ? 'fixing the final gate'
|
|
172
|
+
: s.step;
|
|
171
173
|
const d = {
|
|
172
174
|
title: `/task-auto · ${s.title}`,
|
|
173
175
|
phase,
|
|
@@ -36,6 +36,16 @@ export interface RunWorkerInput {
|
|
|
36
36
|
threshold?: number;
|
|
37
37
|
pathThreshold?: number;
|
|
38
38
|
} | false;
|
|
39
|
+
/**
|
|
40
|
+
* Dead-backend stall guard override. Default ON: no output for
|
|
41
|
+
* STALL_AFTER_MS → probe the model endpoints pi is configured with →
|
|
42
|
+
* unreachable → kill + `stalled: true`. Pass `false` to disable, or
|
|
43
|
+
* override the window/probe (tests, harnesses).
|
|
44
|
+
*/
|
|
45
|
+
stall?: {
|
|
46
|
+
afterMs?: number;
|
|
47
|
+
probe?: () => Promise<boolean>;
|
|
48
|
+
} | false;
|
|
39
49
|
}
|
|
40
50
|
export interface RunWorkerResult {
|
|
41
51
|
text: string;
|
|
@@ -73,5 +83,11 @@ export interface RunWorkerResult {
|
|
|
73
83
|
* the caller must treat it as a failure.
|
|
74
84
|
*/
|
|
75
85
|
timedOut?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Set when the stall guard killed the worker: no output progress AND the
|
|
88
|
+
* model endpoint unreachable. Check BEFORE `aborted` — the kill sets
|
|
89
|
+
* aborted too, and mislabeling this as a user cancel hides a dead backend.
|
|
90
|
+
*/
|
|
91
|
+
stalled?: boolean;
|
|
76
92
|
}
|
|
77
93
|
export declare function runWorker(input: RunWorkerInput): Promise<RunWorkerResult>;
|
|
@@ -3,6 +3,7 @@ import { CHILD_BASE_ARGS, runChildDefault } from '../shared/child-process.js';
|
|
|
3
3
|
import { LoopDetector } from '../task/loop-detector.js';
|
|
4
4
|
import { LOOP_WINDOW, LOOP_THRESHOLD, MAX_LOOP_RESTARTS, formatLoopHint } from '../task/child-runner.js';
|
|
5
5
|
import { detectLeakedToolCall, leakedToolCallHint, MAX_LEAK_RETRIES } from '../shared/leaked-tool-call.js';
|
|
6
|
+
import { discoverModelEndpoints, probeModelEndpoints } from '../shared/model-endpoint.js';
|
|
6
7
|
// `--mode json` makes pi emit structured events as they happen instead of
|
|
7
8
|
// buffering the assistant text and flushing on exit. That matters for the
|
|
8
9
|
// wait/work timing split: in text mode the first stdout chunk only arrives at
|
|
@@ -21,6 +22,15 @@ const DEFAULT_TOOLS = 'read,grep,find,ls';
|
|
|
21
22
|
* so it never trips a legitimately slow run.
|
|
22
23
|
*/
|
|
23
24
|
const RESEARCH_WORKER_TIMEOUT_MS = 240_000;
|
|
25
|
+
/**
|
|
26
|
+
* Output-stall window before the dead-backend probe fires (mx5 run 7: model
|
|
27
|
+
* server died mid-gate-child, the child hung MUTE for 64 minutes). This is NOT
|
|
28
|
+
* a wall-clock cap — output progress resets it, and even a fully stalled child
|
|
29
|
+
* is only killed when the model endpoint is actually unreachable. Sized so a
|
|
30
|
+
* long local prompt-processing pass (minutes of legitimate silence, server
|
|
31
|
+
* alive) just gets probed and waits on.
|
|
32
|
+
*/
|
|
33
|
+
const STALL_AFTER_MS = 180_000;
|
|
24
34
|
/** Restart hint after a wall-clock timeout — distinct from the loop hint. */
|
|
25
35
|
const WORKER_TIMEOUT_HINT = '[SYSTEM NOTE: Your previous attempt ran out of time before answering — you '
|
|
26
36
|
+ 'were exploring too long. Be decisive: do the minimum reads/greps needed, '
|
|
@@ -95,6 +105,15 @@ export async function runWorker(input) {
|
|
|
95
105
|
try {
|
|
96
106
|
result = await runChildDefault(invocation, input.cwd, timeout.signal, {
|
|
97
107
|
mode: 'json-events',
|
|
108
|
+
...(input.stall === false ?
|
|
109
|
+
{}
|
|
110
|
+
: {
|
|
111
|
+
stall: {
|
|
112
|
+
afterMs: input.stall?.afterMs ?? STALL_AFTER_MS,
|
|
113
|
+
probe: input.stall?.probe
|
|
114
|
+
?? (() => probeModelEndpoints(discoverModelEndpoints()))
|
|
115
|
+
}
|
|
116
|
+
}),
|
|
98
117
|
onFirstByte: () => (tFirstByte = Date.now()),
|
|
99
118
|
onToolCall: call => {
|
|
100
119
|
if (!loopDetector)
|
|
@@ -150,7 +169,8 @@ export async function runWorker(input) {
|
|
|
150
169
|
workMs,
|
|
151
170
|
...(leaked ? { leakedToolCall: leaked } : {}),
|
|
152
171
|
...(loopHit ? { loopHit } : {}),
|
|
153
|
-
...(timedOut ? { timedOut: true } : {})
|
|
172
|
+
...(timedOut ? { timedOut: true } : {}),
|
|
173
|
+
...(result.stalled ? { stalled: true } : {})
|
|
154
174
|
};
|
|
155
175
|
}
|
|
156
176
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|