@mjasnikovs/pi-task 0.38.7 → 0.38.8

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.
@@ -25,7 +25,7 @@ import { requestCancel, resetCancel, isCancelRequested, cancelCheckpoint } from
25
25
  import { armCancelListener, disarmCancelListener } from './cancel-input.js';
26
26
  import { beginRun, endRun } from './mid-run-input.js';
27
27
  import { reportDroppedInput } from './dropped-input.js';
28
- import { refineExistingFilesBlock } from './phases.js';
28
+ import { refineExistingFilesBlock, SINGLE_READ_EXTENSION_PATH } from './phases.js';
29
29
  import { SessionUI, registerBridgeCommand, publishLifecycleNotice } from '../remote/bridge.js';
30
30
  import { pushNotify } from '../remote/push.js';
31
31
  import { startAutoLoader } from './widget.js';
@@ -1079,6 +1079,13 @@ function defaultDeps(ctx, cwd, signal, title) {
1079
1079
  cwd,
1080
1080
  taskId: '',
1081
1081
  signal,
1082
+ // IN-RUN thrash guard for the planning children (mx5-n 2026-08-14: a
1083
+ // decompose child re-read DESIGN/marketplace.html until it filled a
1084
+ // 120k window, and ran 16m23s without returning). Every planning child
1085
+ // gets its source doc INLINED in its prompt, so a second read of a file
1086
+ // it has already opened can only be thrash — which makes the read-once
1087
+ // block safe here in a way it is not for a phase that must explore.
1088
+ childExtensions: [SINGLE_READ_EXTENSION_PATH],
1082
1089
  onChildOutput: (line) => {
1083
1090
  lastLine = line;
1084
1091
  },
@@ -58,14 +58,16 @@ export interface PhaseRunResult {
58
58
  /** Set when the child's final turn failed with stopReason "error" (model/provider failure). */
59
59
  modelError?: string;
60
60
  }
61
- export declare function childArgs(tools: string): string[];
61
+ export declare function childArgs(tools: string, extensions?: readonly string[]): string[];
62
62
  export declare const USER_CANCELLED = "__user_cancelled__";
63
63
  /**
64
64
  * Run a child pi process with JSON event-stream output, loop detection, and
65
65
  * context-usage tracking. This is the typed convenience wrapper used by
66
66
  * phase-level code.
67
67
  */
68
- export declare function runChild(cwd: string, tools: string, prompt: string, signal: AbortSignal, onLine?: (line: string) => void, onContextUsage?: (snapshot: ContextSnapshot) => void, onToolCall?: (call: ToolCall) => LoopHit | null, spawnFn?: SpawnFn): Promise<PhaseRunResult>;
68
+ export declare function runChild(cwd: string, tools: string, prompt: string, signal: AbortSignal, onLine?: (line: string) => void, onContextUsage?: (snapshot: ContextSnapshot) => void, onToolCall?: (call: ToolCall) => LoopHit | null, spawnFn?: SpawnFn,
69
+ /** Internal `-e` extension paths for in-run guards (see childArgs). */
70
+ extensions?: readonly string[]): Promise<PhaseRunResult>;
69
71
  interface PhaseDeps {
70
72
  cwd: string;
71
73
  taskId: string;
@@ -79,6 +81,17 @@ interface PhaseDeps {
79
81
  */
80
82
  recordSubStep?: (label: string, ms: number) => void;
81
83
  spawn?: SpawnFn;
84
+ /**
85
+ * Internal `-e` extension paths loaded into this child for IN-RUN guards.
86
+ *
87
+ * The point of a guard that runs inside the child is that it does not have
88
+ * to kill it: pi turns a `tool_call` handler's `{block, reason}` into an
89
+ * error tool result, so the model reads the reason as its own tool output
90
+ * and continues with its context intact. The host's only alternative is to
91
+ * kill and re-spawn from nothing, which just re-runs a model that
92
+ * deterministically re-thrashes (workers/single-read-guard.ts).
93
+ */
94
+ childExtensions?: readonly string[];
82
95
  /**
83
96
  * Wall-clock budget for ONE spawn of this child, in ms. Defaults to
84
97
  * PHASE_CHILD_TIMEOUT_MS; `0` disables the cap. Mirrors runWorker's
@@ -127,7 +127,7 @@ export function connectionRetryBackoffMs(attempt) {
127
127
  }
128
128
  const defaultSleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
129
129
  // ─── Spawn helpers ───────────────────────────────────────────────────────────
130
- export function childArgs(tools) {
130
+ export function childArgs(tools, extensions = []) {
131
131
  // `--mode json` puts the child into the structured event stream the
132
132
  // unified runner parses in `mode: 'json-events'`. Without it the child
133
133
  // emits plain text, every line fails JSON.parse, finalText stays empty,
@@ -142,8 +142,13 @@ export function childArgs(tools) {
142
142
  // The prompt is NOT an argv element: it goes to the child over stdin (see
143
143
  // runChild below / getPiInvocation), so a large inlined-design prompt can't
144
144
  // overflow the OS command-line limit (Windows `spawn ENAMETOOLONG`).
145
+ //
146
+ // `extensions` are internal `-e` loads for in-run guards (the caller supplies
147
+ // the path). A no-tools child cannot make a tool call, so it never carries
148
+ // one — the guards all hang off pi's `tool_call` hook.
145
149
  const toolFlags = tools === '' ? ['--no-tools'] : ['--tools', tools];
146
- return [...childBaseArgs(), '--mode', 'json', ...toolFlags];
150
+ const internal = tools === '' ? [] : extensions;
151
+ return [...childBaseArgs(internal), '--mode', 'json', ...toolFlags];
147
152
  }
148
153
  // Sentinel error thrown when the user dismisses a grill-me dialog.
149
154
  // Defined here (not in failure-classifier.ts) to avoid circular dependency.
@@ -154,8 +159,10 @@ export const USER_CANCELLED = '__user_cancelled__';
154
159
  * context-usage tracking. This is the typed convenience wrapper used by
155
160
  * phase-level code.
156
161
  */
157
- export async function runChild(cwd, tools, prompt, signal, onLine, onContextUsage, onToolCall, spawnFn) {
158
- const invocation = getPiInvocation(childArgs(tools), prompt);
162
+ export async function runChild(cwd, tools, prompt, signal, onLine, onContextUsage, onToolCall, spawnFn,
163
+ /** Internal `-e` extension paths for in-run guards (see childArgs). */
164
+ extensions) {
165
+ const invocation = getPiInvocation(childArgs(tools, extensions), prompt);
159
166
  let loopHit;
160
167
  const result = await runChildUnified(spawnFn ?? spawn, invocation, cwd, signal, {
161
168
  mode: 'json-events',
@@ -289,7 +296,7 @@ export async function runPhaseChild(deps, name, tools, prompt) {
289
296
  const clock = phaseTimeout(deps.signal, budgetMs);
290
297
  let r;
291
298
  try {
292
- r = await runChild(deps.cwd, tools, prependHint(hint, prompt), clock.signal, deps.onChildOutput, deps.onContextUsage, call => detector.record(call), deps.spawn);
299
+ r = await runChild(deps.cwd, tools, prependHint(hint, prompt), clock.signal, deps.onChildOutput, deps.onContextUsage, call => detector.record(call), deps.spawn, deps.childExtensions);
293
300
  }
294
301
  finally {
295
302
  clock.cleanup();
@@ -111,6 +111,15 @@ export declare function searchConfigured(getEnv?: (k: string) => string | undefi
111
111
  /** Extra prompt block for the APIS worker when search is available — trigger-framed
112
112
  * (the validated shape for getting a local model to actually reach for search). */
113
113
  export declare const RESEARCH_SEARCH_HINT: string;
114
+ /**
115
+ * In-process guards loaded into the TOOLING worker only: block a re-read of any
116
+ * file already read, and block any byte-identical grep/find/ls repeat, feeding
117
+ * the model "you already have this, answer now" instead of letting it re-run.
118
+ * TOOLING reads each file once and never needs an identical search twice in any
119
+ * healthy recorded run, so neither rule has a legitimate false positive here.
120
+ * See single-read-guard.ts.
121
+ */
122
+ export declare const SINGLE_READ_EXTENSION_PATH: string;
114
123
  /**
115
124
  * The TOOLING worker only needs to know which verification commands the task
116
125
  * cares about — never the per-file edit list. Big refined prompts embed a long
@@ -332,7 +332,7 @@ export const RESEARCH_SEARCH_HINT = '\n\nLIVE WEB — use pi-worker-search for e
332
332
  * healthy recorded run, so neither rule has a legitimate false positive here.
333
333
  * See single-read-guard.ts.
334
334
  */
335
- const SINGLE_READ_EXTENSION_PATH = fileURLToPath(new URL('../workers/single-read-extension.js', import.meta.url));
335
+ export const SINGLE_READ_EXTENSION_PATH = fileURLToPath(new URL('../workers/single-read-extension.js', import.meta.url));
336
336
  /**
337
337
  * Task-file heading under which a research worker's validated output is cached.
338
338
  * A resumed research phase reads these to skip workers that already succeeded,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.38.7",
3
+ "version": "0.38.8",
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",