@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, ENFORCE_TIMEOUT_MS, ENFORCE_LOOP } from './enforce-guidelines.js';
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
- // Reuse the timeout- and loop-guarded worker child. It runs the
317
- // same local model with edit tools so it can fix violations in
318
- // place. classifyEnforceChildFailure turns any non-clean exit
319
- // (loop kill, timeout, leaked tool call, non-zero) into a thrown
320
- // error so the pass becomes a blocking outcome rather than
321
- // trusting a partial transcript.
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. (runWorker surfaces no context usage, so that
327
- // line is just omitted.)
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
- // so a loop/timeout in the verifier was undiagnosable. One
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 kill shows the exact repeated
337
- // calls that tripped the loop detector. Fire-and-forget.
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: ENFORCE_TIMEOUT_MS,
362
- loop: ENFORCE_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, ENFORCE_TIMEOUT_MS };
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, ENFORCE_TIMEOUT_MS };
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
- /** Per-worker wall-clock timeout in ms. Defaults to RESEARCH_WORKER_TIMEOUT_MS. */
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 keep only the exact-match guard.
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
- const timer = setTimeout(() => {
38
- timedOut = true;
39
- ctrl.abort();
40
- }, ms);
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
- const loopWindow = input.loop?.window ?? LOOP_WINDOW;
75
- const loopThreshold = input.loop?.threshold ?? LOOP_THRESHOLD;
76
- const loopDetector = new LoopDetector(loopWindow, loopThreshold, input.loop?.pathThreshold ?? loopThreshold);
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.34",
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",