@mjasnikovs/pi-task 0.13.34 → 0.13.36
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.
|
@@ -15,7 +15,7 @@ import { isDuplicateQuestion, MAX_DUP_STRIKES, DUP_REPROMPT_HINT } from './quest
|
|
|
15
15
|
import { allocateAutoId, buildAutoBody, parseDecomposeList, parseTaskList, checkOffTask, stampTaskInProgress, findResumableAuto } from './auto-io.js';
|
|
16
16
|
import { writeTaskFile, readTaskFile, updateTaskFrontMatter, taskFilePath, tasksDir } from './task-io.js';
|
|
17
17
|
import { gitCommitAll } from './auto-commit.js';
|
|
18
|
-
import { runGuidelineEnforcement, classifyEnforceChildFailure
|
|
18
|
+
import { runGuidelineEnforcement, classifyEnforceChildFailure } from './enforce-guidelines.js';
|
|
19
19
|
import { runWorker } from '../workers/pi-worker-core.js';
|
|
20
20
|
import { runPhaseChild, prependHint, USER_CANCELLED } from './child-runner.js';
|
|
21
21
|
import { SessionUI, registerBridgeCommand } from '../remote/bridge.js';
|
|
@@ -313,28 +313,31 @@ function defaultDeps(ctx, cwd, signal, title) {
|
|
|
313
313
|
return runGuidelineEnforcement({
|
|
314
314
|
cwd: cwd2,
|
|
315
315
|
signal,
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
316
|
+
// Run the worker child UNGUARDED: no loop detector, no wall-clock
|
|
317
|
+
// timeout. This pass's job is to rework files in place until every
|
|
318
|
+
// violation is fixed, and it legitimately reads/edits/re-greps the
|
|
319
|
+
// same file many times — the research-worker guards mislabel that
|
|
320
|
+
// as a runaway and kill good work (proven on mx5 TASK_0002). Let it
|
|
321
|
+
// run until the model finishes; classifyEnforceChildFailure still
|
|
322
|
+
// blocks the commit on a real failure (non-zero exit, leaked tool
|
|
323
|
+
// call) or a user cancel.
|
|
322
324
|
runChild: async (tools, prompt, sig) => {
|
|
323
325
|
// The enforcement child is a slow local-model pass with edit
|
|
324
326
|
// tools; show the same /task-auto status block as planning so
|
|
325
|
-
// it isn't silent — head · enforcing guidelines/elapsed ·
|
|
326
|
-
// last line.
|
|
327
|
-
//
|
|
327
|
+
// it isn't silent — head · enforcing guidelines/elapsed ·
|
|
328
|
+
// tokens/window [bar] · ↳ last line. The worker runs the same
|
|
329
|
+
// `--mode json` stream as the phase children, so it emits
|
|
330
|
+
// context_usage; forward it (onContextUsage below) so the bar
|
|
331
|
+
// renders here exactly like every other phase widget.
|
|
328
332
|
lastLine = undefined;
|
|
329
333
|
contextUsage = undefined;
|
|
330
334
|
const startedAt = Date.now();
|
|
331
335
|
// Per-pass debug log. The enforce child is otherwise
|
|
332
|
-
// unobservable (it has no per-task file like the impl runner)
|
|
333
|
-
//
|
|
334
|
-
// rolling file under .pi-tasks/; each pass brackets its
|
|
336
|
+
// unobservable (it has no per-task file like the impl runner).
|
|
337
|
+
// One rolling file under .pi-tasks/; each pass brackets its
|
|
335
338
|
// tool/text lines with start/end markers naming the task and
|
|
336
|
-
// the terminal outcome, so a
|
|
337
|
-
//
|
|
339
|
+
// the terminal outcome, so a stop (a real failure or a found
|
|
340
|
+
// violation) is diagnosable. Fire-and-forget.
|
|
338
341
|
const enforceLogPath = path.join(tasksDir(cwd2), 'enforce-debug.log');
|
|
339
342
|
const logEnforce = (msg) => {
|
|
340
343
|
void fsp
|
|
@@ -358,11 +361,14 @@ function defaultDeps(ctx, cwd, signal, title) {
|
|
|
358
361
|
cwd: cwd2,
|
|
359
362
|
signal: sig,
|
|
360
363
|
tools,
|
|
361
|
-
timeoutMs:
|
|
362
|
-
loop:
|
|
364
|
+
timeoutMs: 0, // no wall-clock timeout — run to completion
|
|
365
|
+
loop: false, // no loop guard — revisiting one file IS the job
|
|
363
366
|
onLine: line => {
|
|
364
367
|
lastLine = line;
|
|
365
368
|
logEnforce(line);
|
|
369
|
+
},
|
|
370
|
+
onContextUsage: snapshot => {
|
|
371
|
+
contextUsage = resolveContextUsage(snapshot, contextUsage, parentContextWindow);
|
|
366
372
|
}
|
|
367
373
|
});
|
|
368
374
|
const failure = classifyEnforceChildFailure(r);
|
|
@@ -3,25 +3,6 @@ import { type SpawnFn } from '../shared/child-process.js';
|
|
|
3
3
|
export declare const GUIDELINE_FILENAMES: readonly ["AGENTS.md", "CLAUDE.md"];
|
|
4
4
|
/** Tools the fix pass is allowed: read/search to inspect, edit/write to fix. */
|
|
5
5
|
declare const ENFORCE_TOOLS = "read,grep,find,ls,edit,write";
|
|
6
|
-
/** Wall-clock ceiling for the enforcement child. Local models are slow and the
|
|
7
|
-
* fix pass reads several files and may rewrite a few; give it room but never
|
|
8
|
-
* let a wedged child hang the whole run. */
|
|
9
|
-
declare const ENFORCE_TIMEOUT_MS = 600000;
|
|
10
|
-
/**
|
|
11
|
-
* Loop-guard tuning for the enforcement child. Unlike a read-only research
|
|
12
|
-
* worker, this pass's JOB is to rework files in place, so it legitimately reads,
|
|
13
|
-
* edits, and re-greps the SAME file many times — the default path-revisit
|
|
14
|
-
* heuristic (5 ops on one path) mislabels that as a runaway. It is disabled here
|
|
15
|
-
* (pathThreshold = Infinity); only the exact-match guard remains (a byte-for-byte
|
|
16
|
-
* identical call repeated, raised to 12 over a 40-call window), with the 10-min
|
|
17
|
-
* wall-clock timeout as the real runaway backstop. Validated against a real
|
|
18
|
-
* loop-kill on mx5 TASK_0002, where a single-file fix pass on queries.ts tripped
|
|
19
|
-
* the default detector at 5 revisits. */
|
|
20
|
-
export declare const ENFORCE_LOOP: {
|
|
21
|
-
readonly window: 40;
|
|
22
|
-
readonly threshold: 12;
|
|
23
|
-
readonly pathThreshold: number;
|
|
24
|
-
};
|
|
25
6
|
export interface GuidelineDoc {
|
|
26
7
|
/** Filenames found, in discovery order (e.g. ['AGENTS.md', 'CLAUDE.md']). */
|
|
27
8
|
files: string[];
|
|
@@ -106,4 +87,4 @@ export interface EnforcementDeps {
|
|
|
106
87
|
* blocking outcome (ok: false) so an unverifiable task is not committed.
|
|
107
88
|
*/
|
|
108
89
|
export declare function runGuidelineEnforcement(deps: EnforcementDeps): Promise<EnforceOutcome>;
|
|
109
|
-
export { ENFORCE_TOOLS
|
|
90
|
+
export { ENFORCE_TOOLS };
|
|
@@ -24,25 +24,6 @@ import { USER_CANCELLED } from './child-runner.js';
|
|
|
24
24
|
export const GUIDELINE_FILENAMES = ['AGENTS.md', 'CLAUDE.md'];
|
|
25
25
|
/** Tools the fix pass is allowed: read/search to inspect, edit/write to fix. */
|
|
26
26
|
const ENFORCE_TOOLS = 'read,grep,find,ls,edit,write';
|
|
27
|
-
/** Wall-clock ceiling for the enforcement child. Local models are slow and the
|
|
28
|
-
* fix pass reads several files and may rewrite a few; give it room but never
|
|
29
|
-
* let a wedged child hang the whole run. */
|
|
30
|
-
const ENFORCE_TIMEOUT_MS = 600_000;
|
|
31
|
-
/**
|
|
32
|
-
* Loop-guard tuning for the enforcement child. Unlike a read-only research
|
|
33
|
-
* worker, this pass's JOB is to rework files in place, so it legitimately reads,
|
|
34
|
-
* edits, and re-greps the SAME file many times — the default path-revisit
|
|
35
|
-
* heuristic (5 ops on one path) mislabels that as a runaway. It is disabled here
|
|
36
|
-
* (pathThreshold = Infinity); only the exact-match guard remains (a byte-for-byte
|
|
37
|
-
* identical call repeated, raised to 12 over a 40-call window), with the 10-min
|
|
38
|
-
* wall-clock timeout as the real runaway backstop. Validated against a real
|
|
39
|
-
* loop-kill on mx5 TASK_0002, where a single-file fix pass on queries.ts tripped
|
|
40
|
-
* the default detector at 5 revisits. */
|
|
41
|
-
export const ENFORCE_LOOP = {
|
|
42
|
-
window: 40,
|
|
43
|
-
threshold: 12,
|
|
44
|
-
pathThreshold: Number.POSITIVE_INFINITY
|
|
45
|
-
};
|
|
46
27
|
/**
|
|
47
28
|
* Read the guideline files that live directly in `cwd` (AGENTS.md, CLAUDE.md).
|
|
48
29
|
* Returns null when none exist or all are empty — the caller treats that as a
|
|
@@ -187,4 +168,4 @@ export async function runGuidelineEnforcement(deps) {
|
|
|
187
168
|
return { ok: true };
|
|
188
169
|
return { ok: false, reason: `guideline violation: ${verdict.detail}` };
|
|
189
170
|
}
|
|
190
|
-
export { ENFORCE_TOOLS
|
|
171
|
+
export { ENFORCE_TOOLS };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type SpawnFn } from '../shared/child-process.js';
|
|
1
|
+
import { type ContextSnapshot, type SpawnFn } from '../shared/child-process.js';
|
|
2
2
|
import { type LoopHit } from '../task/loop-detector.js';
|
|
3
3
|
export interface RunWorkerInput {
|
|
4
4
|
prompt: string;
|
|
@@ -11,19 +11,31 @@ export interface RunWorkerInput {
|
|
|
11
11
|
extensions?: string[];
|
|
12
12
|
/** Called for each tool execution start and text-writing event inside the worker. */
|
|
13
13
|
onLine?: (line: string) => void;
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Called for each context_usage snapshot the child emits (same `--mode json`
|
|
16
|
+
* stream the phase children parse). Lets a caller's status widget show the
|
|
17
|
+
* tokens/window progress bar for the worker exactly like the phase widget,
|
|
18
|
+
* instead of omitting it.
|
|
19
|
+
*/
|
|
20
|
+
onContextUsage?: (snapshot: ContextSnapshot) => void;
|
|
21
|
+
/**
|
|
22
|
+
* Per-worker wall-clock timeout in ms. Defaults to RESEARCH_WORKER_TIMEOUT_MS.
|
|
23
|
+
* Pass 0 to disable the timeout entirely (run until the child exits on its
|
|
24
|
+
* own) — for a pass that must be allowed to finish however long it takes.
|
|
25
|
+
*/
|
|
15
26
|
timeoutMs?: number;
|
|
16
27
|
/**
|
|
17
28
|
* Per-worker loop-detector tuning. Defaults to the read-only research/impl
|
|
18
29
|
* guard (LOOP_WINDOW / LOOP_THRESHOLD, path threshold = exact threshold). An
|
|
19
30
|
* edit/fix pass legitimately revisits one file, so it can raise (or disable
|
|
20
|
-
* via Infinity) `pathThreshold` to
|
|
31
|
+
* via Infinity) `pathThreshold`. Pass `false` to turn the detector OFF
|
|
32
|
+
* entirely — no tool-call pattern will ever kill the worker.
|
|
21
33
|
*/
|
|
22
34
|
loop?: {
|
|
23
35
|
window?: number;
|
|
24
36
|
threshold?: number;
|
|
25
37
|
pathThreshold?: number;
|
|
26
|
-
};
|
|
38
|
+
} | false;
|
|
27
39
|
}
|
|
28
40
|
export interface RunWorkerResult {
|
|
29
41
|
text: string;
|
|
@@ -34,10 +34,14 @@ const WORKER_TIMEOUT_HINT = '[SYSTEM NOTE: Your previous attempt ran out of time
|
|
|
34
34
|
function workerTimeout(external, ms) {
|
|
35
35
|
const ctrl = new AbortController();
|
|
36
36
|
let timedOut = false;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
// ms <= 0 (or non-finite) disables the wall-clock timeout: no timer is armed,
|
|
38
|
+
// so only the external signal can abort and timedOut() stays false forever.
|
|
39
|
+
const timer = ms > 0 && Number.isFinite(ms) ?
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
timedOut = true;
|
|
42
|
+
ctrl.abort();
|
|
43
|
+
}, ms)
|
|
44
|
+
: undefined;
|
|
41
45
|
const onExternal = () => ctrl.abort();
|
|
42
46
|
if (external) {
|
|
43
47
|
if (external.aborted)
|
|
@@ -71,9 +75,16 @@ export async function runWorker(input) {
|
|
|
71
75
|
const invocation = getPiInvocation([...baseArgs, prompt]);
|
|
72
76
|
const tStart = Date.now();
|
|
73
77
|
let tFirstByte = null;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
// loop === false turns the guard off entirely (detector is null and no
|
|
79
|
+
// tool call is ever flagged); otherwise build a detector from the override
|
|
80
|
+
// or the default research/impl thresholds.
|
|
81
|
+
const loopDetector = input.loop === false ?
|
|
82
|
+
null
|
|
83
|
+
: (() => {
|
|
84
|
+
const window = input.loop?.window ?? LOOP_WINDOW;
|
|
85
|
+
const threshold = input.loop?.threshold ?? LOOP_THRESHOLD;
|
|
86
|
+
return new LoopDetector(window, threshold, input.loop?.pathThreshold ?? threshold);
|
|
87
|
+
})();
|
|
77
88
|
// Capture the hit the detector reports (it also returns it to the unified
|
|
78
89
|
// runner, which kills the child on a hit). Without capturing it here the
|
|
79
90
|
// SIGTERM that kill produces would surface as a bare non-zero exit the
|
|
@@ -86,12 +97,15 @@ export async function runWorker(input) {
|
|
|
86
97
|
mode: 'json-events',
|
|
87
98
|
onFirstByte: () => (tFirstByte = Date.now()),
|
|
88
99
|
onToolCall: call => {
|
|
100
|
+
if (!loopDetector)
|
|
101
|
+
return null;
|
|
89
102
|
const hit = loopDetector.record(call);
|
|
90
103
|
if (hit && !loopHit)
|
|
91
104
|
loopHit = hit;
|
|
92
105
|
return hit;
|
|
93
106
|
},
|
|
94
|
-
onLine: input.onLine
|
|
107
|
+
onLine: input.onLine,
|
|
108
|
+
onContextUsage: input.onContextUsage
|
|
95
109
|
}, input.spawn);
|
|
96
110
|
}
|
|
97
111
|
finally {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.36",
|
|
4
4
|
"description": "Deterministic spec-orchestration for local models, with a bundled real-time remote web view and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|