@mjasnikovs/pi-task 0.17.27 → 0.18.1
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/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 +42 -7
- package/dist/task/phases.js +62 -23
- package/dist/task/prohibition-probe.d.ts +53 -0
- package/dist/task/prohibition-probe.js +64 -0
- package/dist/task/prompts.d.ts +1 -1
- package/dist/task/prompts.js +8 -1
- package/dist/task/verify-work.d.ts +32 -2
- package/dist/task/verify-work.js +78 -3
- 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/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
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prohibition-probe — deterministic detection of VIOLATED SPEC PROHIBITIONS,
|
|
3
|
+
* feeding the verify gate's prompt.
|
|
4
|
+
*
|
|
5
|
+
* The failure class (mx5 run 7, "New Listing page" task): the spec's CONSTRAINTS
|
|
6
|
+
* said "**Do NOT modify** any server-side code: `src/server/index.ts`, …"; the
|
|
7
|
+
* implementation modified `src/server/index.ts` anyway; the verify child SAW it
|
|
8
|
+
* ("VIOLATES 'Do NOT modify server-side code'"), waived it ("BUT: this is
|
|
9
|
+
* additive, tests pass with it"), and PASSed. Worse, the reproduction fixture
|
|
10
|
+
* showed the baseline child usually never LOOKS: 5/5 baseline runs consulted no
|
|
11
|
+
* diff at all and several affirmatively claimed the forbidden file was untouched.
|
|
12
|
+
*
|
|
13
|
+
* So — like the substitution probe (see substitution-probe.ts, whose A/B proved
|
|
14
|
+
* prompt language alone gets ~40% attention while a concrete deterministic
|
|
15
|
+
* finding gets 100%) — the fix is a deterministic pre-check whose finding is
|
|
16
|
+
* injected into the prompt: extract the concrete paths the spec forbids
|
|
17
|
+
* modifying, intersect with the task's changed files (pure git shape, already
|
|
18
|
+
* collected for the substitution probe), and hand the child each hit with the
|
|
19
|
+
* exact constraint wording.
|
|
20
|
+
*
|
|
21
|
+
* The finding is advisory, not an auto-FAIL, for one reason: prohibitions in
|
|
22
|
+
* real specs are prose and can be CONDITIONAL ("Do NOT modify `api.ts` beyond
|
|
23
|
+
* what is needed for the new endpoint") — a hard gate on prose extraction would
|
|
24
|
+
* false-FAIL legitimate work. The finding therefore carries the constraint line
|
|
25
|
+
* verbatim and the prompt's no-waiver rule (4b in verify-work.ts) forbids
|
|
26
|
+
* excusing an ABSOLUTE prohibition while directing conditional ones to be judged
|
|
27
|
+
* against their own stated exception. A violation that was fully reverted before
|
|
28
|
+
* verify produces no diff entry, so it never fires — reverted = not violated.
|
|
29
|
+
*/
|
|
30
|
+
import type { ChangedFile } from './substitution-probe.js';
|
|
31
|
+
/** One "do not modify X" constraint extracted from the spec text. */
|
|
32
|
+
export interface Prohibition {
|
|
33
|
+
/** The forbidden path exactly as the spec spells it (file or directory). */
|
|
34
|
+
path: string;
|
|
35
|
+
/** The full spec line carrying the prohibition, so the verify child judges
|
|
36
|
+
* against the EXACT wording — including any exception clause it states. */
|
|
37
|
+
constraint: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Extract the concrete paths the spec explicitly forbids modifying: every
|
|
41
|
+
* backtick-quoted path-like token on a line that expresses a modification ban.
|
|
42
|
+
* Prose-only prohibitions ("do not modify server-side code" with no path named)
|
|
43
|
+
* extract nothing — the prompt-level rule still covers them.
|
|
44
|
+
*/
|
|
45
|
+
export declare function extractProhibitions(spec: string): Prohibition[];
|
|
46
|
+
/**
|
|
47
|
+
* Intersect the spec's prohibitions with the task's changed files (git shape —
|
|
48
|
+
* the same collector the substitution probe uses). A prohibition matches a
|
|
49
|
+
* changed file exactly, or as a directory prefix (`src/server` covers
|
|
50
|
+
* `src/server/index.ts`). One finding line per violated file, carrying the
|
|
51
|
+
* constraint verbatim; empty array → no block in the prompt.
|
|
52
|
+
*/
|
|
53
|
+
export declare function findProhibitionViolations(prohibitions: Prohibition[], files: ChangedFile[]): string[];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does this line express a modification ban? Matches the active forms ("do not
|
|
3
|
+
* modify", "must not touch", "never edit", "don't change") and the passive form
|
|
4
|
+
* ("must not be modified"). Deliberately verb-scoped to modification — a "do not
|
|
5
|
+
* add a dependency" style rule names no path and is the prompt rule's job.
|
|
6
|
+
*/
|
|
7
|
+
const PROHIBITION_RE = /\b(?:do\s+not|don'?t|must\s+not|never)\s+(?:be\s+)?(?:modify|modified|touch|touched|edit|edited|change|changed|alter|altered|rewrite|rewritten|overwrite|overwritten|delete|deleted|remove|removed)\b/i;
|
|
8
|
+
/**
|
|
9
|
+
* A backtick token counts as a path only when it is whitespace-free, uses path
|
|
10
|
+
* characters, and either contains a directory separator, has a file extension,
|
|
11
|
+
* or is a dotfile. Bare identifiers (`getOrder`), API routes with spaces
|
|
12
|
+
* (`POST /api/listings`), and code snippets are rejected — the probe would
|
|
13
|
+
* rather miss a `Makefile` than fire on prose.
|
|
14
|
+
*/
|
|
15
|
+
function looksLikePath(token) {
|
|
16
|
+
if (!/^[\w.@~/-]+$/.test(token))
|
|
17
|
+
return false;
|
|
18
|
+
return token.includes('/') || /\.[A-Za-z0-9]+$/.test(token) || token.startsWith('.');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Extract the concrete paths the spec explicitly forbids modifying: every
|
|
22
|
+
* backtick-quoted path-like token on a line that expresses a modification ban.
|
|
23
|
+
* Prose-only prohibitions ("do not modify server-side code" with no path named)
|
|
24
|
+
* extract nothing — the prompt-level rule still covers them.
|
|
25
|
+
*/
|
|
26
|
+
export function extractProhibitions(spec) {
|
|
27
|
+
const out = [];
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
for (const line of spec.split('\n')) {
|
|
30
|
+
if (!PROHIBITION_RE.test(line))
|
|
31
|
+
continue;
|
|
32
|
+
for (const m of line.matchAll(/`([^`]+)`/g)) {
|
|
33
|
+
const token = m[1].trim();
|
|
34
|
+
if (!looksLikePath(token) || seen.has(token))
|
|
35
|
+
continue;
|
|
36
|
+
seen.add(token);
|
|
37
|
+
out.push({ path: token, constraint: line.trim() });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/** Normalise a path for comparison: strip leading ./ and trailing /. */
|
|
43
|
+
const norm = (p) => p.replace(/^\.\//, '').replace(/\/+$/, '');
|
|
44
|
+
/**
|
|
45
|
+
* Intersect the spec's prohibitions with the task's changed files (git shape —
|
|
46
|
+
* the same collector the substitution probe uses). A prohibition matches a
|
|
47
|
+
* changed file exactly, or as a directory prefix (`src/server` covers
|
|
48
|
+
* `src/server/index.ts`). One finding line per violated file, carrying the
|
|
49
|
+
* constraint verbatim; empty array → no block in the prompt.
|
|
50
|
+
*/
|
|
51
|
+
export function findProhibitionViolations(prohibitions, files) {
|
|
52
|
+
const findings = [];
|
|
53
|
+
for (const f of files) {
|
|
54
|
+
const fp = norm(f.path);
|
|
55
|
+
const hit = prohibitions.find(p => {
|
|
56
|
+
const pp = norm(p.path);
|
|
57
|
+
return fp === pp || fp.startsWith(`${pp}/`);
|
|
58
|
+
});
|
|
59
|
+
if (!hit)
|
|
60
|
+
continue;
|
|
61
|
+
findings.push(`${f.path} — modified by this task, but the spec forbids it: "${hit.constraint}"`);
|
|
62
|
+
}
|
|
63
|
+
return findings;
|
|
64
|
+
}
|
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}
|
|
@@ -38,11 +38,24 @@ export declare function extractSpecForVerification(taskBody: string): string | n
|
|
|
38
38
|
* `probeFindings` are the deterministic self-verification probe results (see
|
|
39
39
|
* substitution-probe.ts): the TEST-THE-COPY class is caught 5/5 only when the
|
|
40
40
|
* prompt carries both the rule (3b) AND a concrete finding naming the suspect
|
|
41
|
-
* file — the rule alone got 2/5 attention on the
|
|
41
|
+
* file — the rule alone got 2/5 attention on the local model. The findings are
|
|
42
42
|
* pure git shape (test files the task itself changed), so the mandate is
|
|
43
43
|
* language- and framework-agnostic.
|
|
44
|
+
*
|
|
45
|
+
* `prohibitionFindings` are the deterministic prohibition probe results (see
|
|
46
|
+
* prohibition-probe.ts): spec-forbidden paths the task's diff modified anyway.
|
|
47
|
+
* Same probe+rule design, same reason: the VIOLATION-EXCUSAL class (mx5 run 7:
|
|
48
|
+
* child saw "Do NOT modify server-side code" violated, waived it as "additive,
|
|
49
|
+
* tests pass", PASSed) needs both the no-waiver rule (4b) AND the concrete diff
|
|
50
|
+
* fact — the baseline child usually never runs `git diff` at all, so without the
|
|
51
|
+
* finding it cannot even SEE the violation. A/B on the live local model
|
|
52
|
+
* (violated-but-working fixture, everything green, forbidden file modified
|
|
53
|
+
* additively): old prompt 5/5 false-PASS (several runs affirmatively claimed the
|
|
54
|
+
* forbidden file was untouched); rule+finding 5/5 FAIL naming the constraint.
|
|
55
|
+
* Guard: honest-clean fixture (prohibition in spec, probe silent) 5/5 PASS — no
|
|
56
|
+
* paranoia. Reverted-violation ≡ clean at the diff level (no entry → no finding).
|
|
44
57
|
*/
|
|
45
|
-
export declare function buildVerifyPrompt(spec: string, probeFindings?: string[]): string;
|
|
58
|
+
export declare function buildVerifyPrompt(spec: string, probeFindings?: string[], envNotes?: string, prohibitionFindings?: string[]): string;
|
|
46
59
|
/**
|
|
47
60
|
* Parse the child's verdict. Scans for the LAST `WORK-VERIFIED: PASS|FAIL` marker
|
|
48
61
|
* (the model discusses before concluding, and bash output may echo the word
|
|
@@ -82,6 +95,12 @@ export interface VerificationDeps {
|
|
|
82
95
|
* into the child's prompt. A/B-proven load-bearing: the prompt rule alone caught
|
|
83
96
|
* the class 2/5, rule + probe finding 5/5. ABSENT or empty → no probe block. */
|
|
84
97
|
probe?: () => Promise<string[]>;
|
|
98
|
+
/**
|
|
99
|
+
* DETERMINISTIC prohibition probe (see prohibition-probe.ts): spec-forbidden
|
|
100
|
+
* paths the task's diff modified anyway, injected as prompt findings under the
|
|
101
|
+
* no-waiver rule (4b). Advisory, never auto-FAIL — real prohibitions can be
|
|
102
|
+
* conditional prose. ABSENT or empty → no prohibition block. */
|
|
103
|
+
prohibitionProbe?: () => Promise<string[]>;
|
|
85
104
|
/**
|
|
86
105
|
* Result of the git-state guard for the MOST RECENT runChild call (see
|
|
87
106
|
* git-state-guard.ts): did the child mutate repo state (stash/checkout/file
|
|
@@ -94,6 +113,17 @@ export interface VerificationDeps {
|
|
|
94
113
|
mutated: boolean;
|
|
95
114
|
detail: string;
|
|
96
115
|
};
|
|
116
|
+
/**
|
|
117
|
+
* Per-run environment-facts cache (see env-notes.ts): `read` supplies the
|
|
118
|
+
* facts earlier gate children discovered (inlined into the prompt with the
|
|
119
|
+
* no-waiver caveat); `append` stores the `ENV-NOTE:` lines this child
|
|
120
|
+
* emitted, host-side. ABSENT → no block, no capture (tests unchanged).
|
|
121
|
+
* Facts only cut re-discovery time; verdict rules are unaffected.
|
|
122
|
+
*/
|
|
123
|
+
envNotes?: {
|
|
124
|
+
read: () => Promise<string>;
|
|
125
|
+
append: (notes: string[]) => Promise<void>;
|
|
126
|
+
};
|
|
97
127
|
}
|
|
98
128
|
/**
|
|
99
129
|
* 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
|
*
|
|
@@ -119,11 +120,24 @@ export function extractSpecForVerification(taskBody) {
|
|
|
119
120
|
* `probeFindings` are the deterministic self-verification probe results (see
|
|
120
121
|
* substitution-probe.ts): the TEST-THE-COPY class is caught 5/5 only when the
|
|
121
122
|
* prompt carries both the rule (3b) AND a concrete finding naming the suspect
|
|
122
|
-
* file — the rule alone got 2/5 attention on the
|
|
123
|
+
* file — the rule alone got 2/5 attention on the local model. The findings are
|
|
123
124
|
* pure git shape (test files the task itself changed), so the mandate is
|
|
124
125
|
* language- and framework-agnostic.
|
|
126
|
+
*
|
|
127
|
+
* `prohibitionFindings` are the deterministic prohibition probe results (see
|
|
128
|
+
* prohibition-probe.ts): spec-forbidden paths the task's diff modified anyway.
|
|
129
|
+
* Same probe+rule design, same reason: the VIOLATION-EXCUSAL class (mx5 run 7:
|
|
130
|
+
* child saw "Do NOT modify server-side code" violated, waived it as "additive,
|
|
131
|
+
* tests pass", PASSed) needs both the no-waiver rule (4b) AND the concrete diff
|
|
132
|
+
* fact — the baseline child usually never runs `git diff` at all, so without the
|
|
133
|
+
* finding it cannot even SEE the violation. A/B on the live local model
|
|
134
|
+
* (violated-but-working fixture, everything green, forbidden file modified
|
|
135
|
+
* additively): old prompt 5/5 false-PASS (several runs affirmatively claimed the
|
|
136
|
+
* forbidden file was untouched); rule+finding 5/5 FAIL naming the constraint.
|
|
137
|
+
* Guard: honest-clean fixture (prohibition in spec, probe silent) 5/5 PASS — no
|
|
138
|
+
* paranoia. Reverted-violation ≡ clean at the diff level (no entry → no finding).
|
|
125
139
|
*/
|
|
126
|
-
export function buildVerifyPrompt(spec, probeFindings) {
|
|
140
|
+
export function buildVerifyPrompt(spec, probeFindings, envNotes, prohibitionFindings) {
|
|
127
141
|
const probeBlock = probeFindings && probeFindings.length > 0 ?
|
|
128
142
|
[
|
|
129
143
|
'SELF-VERIFICATION NOTICE (deterministic, computed by the orchestrator from the diff):',
|
|
@@ -137,6 +151,20 @@ export function buildVerifyPrompt(spec, probeFindings) {
|
|
|
137
151
|
''
|
|
138
152
|
]
|
|
139
153
|
: [];
|
|
154
|
+
const prohibitionBlock = prohibitionFindings && prohibitionFindings.length > 0 ?
|
|
155
|
+
[
|
|
156
|
+
'PROHIBITION NOTICE (deterministic, computed by the orchestrator from the',
|
|
157
|
+
"spec's own constraint lines and the task's diff): this task MODIFIED paths",
|
|
158
|
+
'the spec explicitly forbids modifying:',
|
|
159
|
+
...prohibitionFindings.map(f => `- ${f}`),
|
|
160
|
+
'Read the exact constraint wording in the spec. Unless that wording itself',
|
|
161
|
+
'states an exception that covers this change, this is a violated prohibition:',
|
|
162
|
+
'rule 4b applies and the verdict is FAIL naming the forbidden path — even if',
|
|
163
|
+
'every test passes and the change looks harmless.',
|
|
164
|
+
''
|
|
165
|
+
]
|
|
166
|
+
: [];
|
|
167
|
+
const envBlock = envNotes && envNotes.trim().length > 0 ? [buildEnvNotesBlock(envNotes)] : [];
|
|
140
168
|
return [
|
|
141
169
|
'You are a strict verification pass running right after an AI coding agent',
|
|
142
170
|
'finished a task and committed it. The agent is known to mark work "done"',
|
|
@@ -150,7 +178,9 @@ export function buildVerifyPrompt(spec, probeFindings) {
|
|
|
150
178
|
'THE TASK SPEC (its ACCEPTANCE criteria and VERIFY block are the contract):',
|
|
151
179
|
spec.trim(),
|
|
152
180
|
'',
|
|
181
|
+
...envBlock,
|
|
153
182
|
...probeBlock,
|
|
183
|
+
...prohibitionBlock,
|
|
154
184
|
'How to verify — verify the REAL, shipped deliverable exactly as an unaided fresh',
|
|
155
185
|
'checkout (or CI run) would experience it:',
|
|
156
186
|
'',
|
|
@@ -215,6 +245,19 @@ export function buildVerifyPrompt(spec, probeFindings) {
|
|
|
215
245
|
'4. Treat the ACCEPTANCE criteria as the bar. If a command fails, or its real output',
|
|
216
246
|
' contradicts an ACCEPTANCE criterion, the work has NOT verified.',
|
|
217
247
|
'',
|
|
248
|
+
'4b. SPEC PROHIBITIONS ARE PART OF THE BAR — YOU HAVE NO WAIVER AUTHORITY: when the',
|
|
249
|
+
' spec explicitly forbids something ("Do NOT modify X", "MUST NOT touch Y") and the',
|
|
250
|
+
' shipped work does it anyway, that is a FAIL naming the violated constraint. You',
|
|
251
|
+
' may not excuse a violation because it is additive, small, harmless, an improvement,',
|
|
252
|
+
' or because every test still passes — "it works anyway" is exactly the waiver you do',
|
|
253
|
+
" not have; relaxing a constraint is the spec owner's call, not yours. Check the",
|
|
254
|
+
" task's own diff (git) against the spec's prohibitions — a forbidden file can be",
|
|
255
|
+
' modified without any test noticing. Only two outcomes are not a FAIL: the',
|
|
256
|
+
' violation was fully REVERTED (the shipped tree no longer violates), or the',
|
|
257
|
+
' prohibition\'s own wording states an exception ("except…", "beyond what is needed',
|
|
258
|
+
' for…") that covers the change — judged against that stated exception, not against',
|
|
259
|
+
' your view of harmlessness.',
|
|
260
|
+
'',
|
|
218
261
|
'5. The ONLY thing you may assume is already provided is a genuinely EXTERNAL running',
|
|
219
262
|
' service or network resource (a database server, an API host) that the project',
|
|
220
263
|
' documents as a prerequisite. Before you rely on that assumption, PROBE for the',
|
|
@@ -249,6 +292,8 @@ export function buildVerifyPrompt(spec, probeFindings) {
|
|
|
249
292
|
'a required behavior missing — the verdict is FAIL, even if typecheck and lint are green',
|
|
250
293
|
'and even if the gap seems minor. Never downgrade an unmet criterion to a warning note.',
|
|
251
294
|
'',
|
|
295
|
+
ENV_NOTE_EMIT_INSTRUCTION,
|
|
296
|
+
'',
|
|
252
297
|
'When you are done, output EXACTLY ONE of these as the final line:',
|
|
253
298
|
" WORK-VERIFIED: PASS (the project's own command, run unaided, met the spec)",
|
|
254
299
|
' WORK-VERIFIED: FAIL <text> (the shipped command failed or did not meet the spec; say what failed)',
|
|
@@ -305,6 +350,26 @@ export async function runWorkVerification(deps) {
|
|
|
305
350
|
findings = [];
|
|
306
351
|
}
|
|
307
352
|
}
|
|
353
|
+
let prohibitions = [];
|
|
354
|
+
if (deps.prohibitionProbe) {
|
|
355
|
+
try {
|
|
356
|
+
prohibitions = await deps.prohibitionProbe();
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
prohibitions = [];
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// Environment facts from earlier gate children (best-effort; a cache failure
|
|
363
|
+
// must never block verification).
|
|
364
|
+
let envNotes = '';
|
|
365
|
+
if (deps.envNotes) {
|
|
366
|
+
try {
|
|
367
|
+
envNotes = await deps.envNotes.read();
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
envNotes = '';
|
|
371
|
+
}
|
|
372
|
+
}
|
|
308
373
|
// A child that emits NO verdict never judged the work (budget/context death mid-
|
|
309
374
|
// investigation — seen live: an 11-minute verify wandered, died verdict-less, and
|
|
310
375
|
// the resulting FAIL burned a full implementation re-run on an unjudged artifact).
|
|
@@ -312,7 +377,7 @@ export async function runWorkVerification(deps) {
|
|
|
312
377
|
for (let attempt = 1;; attempt++) {
|
|
313
378
|
let text;
|
|
314
379
|
try {
|
|
315
|
-
text = await deps.runChild(VERIFY_TOOLS, buildVerifyPrompt(deps.spec, findings), deps.signal);
|
|
380
|
+
text = await deps.runChild(VERIFY_TOOLS, buildVerifyPrompt(deps.spec, findings, envNotes, prohibitions), deps.signal);
|
|
316
381
|
}
|
|
317
382
|
catch (err) {
|
|
318
383
|
if (err instanceof Error && err.message === USER_CANCELLED)
|
|
@@ -320,6 +385,16 @@ export async function runWorkVerification(deps) {
|
|
|
320
385
|
const msg = err instanceof Error ? err.message : String(err);
|
|
321
386
|
return { ok: false, reason: `verification pass could not run: ${msg}` };
|
|
322
387
|
}
|
|
388
|
+
// Capture the environment facts the child shared — regardless of verdict
|
|
389
|
+
// (a FAIL run's discoveries are just as reusable).
|
|
390
|
+
if (deps.envNotes) {
|
|
391
|
+
try {
|
|
392
|
+
await deps.envNotes.append(extractEnvNotes(text));
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
// best-effort cache
|
|
396
|
+
}
|
|
397
|
+
}
|
|
323
398
|
// A child that mutated the repo (git-state guard fired) judged a tree it had
|
|
324
399
|
// itself changed — its verdict is meaningless in both directions, so discard
|
|
325
400
|
// 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.1",
|
|
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",
|