@mjasnikovs/pi-task 0.13.34 → 0.13.35

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,12 +313,14 @@ 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
@@ -329,12 +331,11 @@ function defaultDeps(ctx, cwd, signal, title) {
329
331
  contextUsage = undefined;
330
332
  const startedAt = Date.now();
331
333
  // 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
334
+ // unobservable (it has no per-task file like the impl runner).
335
+ // One rolling file under .pi-tasks/; each pass brackets its
335
336
  // 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.
337
+ // the terminal outcome, so a stop (a real failure or a found
338
+ // violation) is diagnosable. Fire-and-forget.
338
339
  const enforceLogPath = path.join(tasksDir(cwd2), 'enforce-debug.log');
339
340
  const logEnforce = (msg) => {
340
341
  void fsp
@@ -358,8 +359,8 @@ function defaultDeps(ctx, cwd, signal, title) {
358
359
  cwd: cwd2,
359
360
  signal: sig,
360
361
  tools,
361
- timeoutMs: ENFORCE_TIMEOUT_MS,
362
- loop: ENFORCE_LOOP,
362
+ timeoutMs: 0, // no wall-clock timeout — run to completion
363
+ loop: false, // no loop guard — revisiting one file IS the job
363
364
  onLine: line => {
364
365
  lastLine = line;
365
366
  logEnforce(line);
@@ -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 };
@@ -11,19 +11,24 @@ 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
+ * Per-worker wall-clock timeout in ms. Defaults to RESEARCH_WORKER_TIMEOUT_MS.
16
+ * Pass 0 to disable the timeout entirely (run until the child exits on its
17
+ * own) — for a pass that must be allowed to finish however long it takes.
18
+ */
15
19
  timeoutMs?: number;
16
20
  /**
17
21
  * Per-worker loop-detector tuning. Defaults to the read-only research/impl
18
22
  * guard (LOOP_WINDOW / LOOP_THRESHOLD, path threshold = exact threshold). An
19
23
  * edit/fix pass legitimately revisits one file, so it can raise (or disable
20
- * via Infinity) `pathThreshold` to keep only the exact-match guard.
24
+ * via Infinity) `pathThreshold`. Pass `false` to turn the detector OFF
25
+ * entirely — no tool-call pattern will ever kill the worker.
21
26
  */
22
27
  loop?: {
23
28
  window?: number;
24
29
  threshold?: number;
25
30
  pathThreshold?: number;
26
- };
31
+ } | false;
27
32
  }
28
33
  export interface RunWorkerResult {
29
34
  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,6 +97,8 @@ 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;
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.35",
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",