@mjasnikovs/pi-task 0.38.10 → 0.38.12

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.
Files changed (78) hide show
  1. package/README.md +7 -3
  2. package/dist/shared/child-process.d.ts +8 -0
  3. package/dist/shared/command-watchdog.d.ts +1 -1
  4. package/dist/shared/command-watchdog.js +1 -1
  5. package/dist/task/accept-debt.d.ts +47 -0
  6. package/dist/task/accept-debt.js +127 -28
  7. package/dist/task/auto-orchestrator.js +93 -116
  8. package/dist/task/boot-probe.d.ts +298 -0
  9. package/dist/task/boot-probe.js +806 -0
  10. package/dist/task/child-runner.d.ts +56 -25
  11. package/dist/task/child-runner.js +65 -31
  12. package/dist/task/child-status.d.ts +95 -0
  13. package/dist/task/child-status.js +99 -0
  14. package/dist/task/command-run.d.ts +36 -0
  15. package/dist/task/command-run.js +48 -1
  16. package/dist/task/command-watchdog.js +1 -1
  17. package/dist/task/context-usage.d.ts +4 -3
  18. package/dist/task/context-usage.js +4 -3
  19. package/dist/task/contracts.js +18 -35
  20. package/dist/task/deep-render-check.d.ts +47 -0
  21. package/dist/task/deep-render-check.js +110 -65
  22. package/dist/task/env-notes.d.ts +3 -3
  23. package/dist/task/env-notes.js +24 -35
  24. package/dist/task/final-gate-fix.d.ts +1 -1
  25. package/dist/task/final-gate-fix.js +1 -1
  26. package/dist/task/final-gate.d.ts +38 -432
  27. package/dist/task/final-gate.js +105 -1213
  28. package/dist/task/gate-child.d.ts +8 -10
  29. package/dist/task/gate-child.js +15 -19
  30. package/dist/task/gate-deps.d.ts +29 -0
  31. package/dist/task/gate-deps.js +192 -206
  32. package/dist/task/gate-tally.d.ts +189 -0
  33. package/dist/task/gate-tally.js +249 -0
  34. package/dist/task/implementation-turn.d.ts +201 -0
  35. package/dist/task/implementation-turn.js +263 -0
  36. package/dist/task/launch-contract.js +27 -43
  37. package/dist/task/launch-manifest.d.ts +5 -0
  38. package/dist/task/launch-manifest.js +21 -0
  39. package/dist/task/ledger.d.ts +38 -0
  40. package/dist/task/ledger.js +83 -0
  41. package/dist/task/loop-detector.d.ts +14 -8
  42. package/dist/task/loop-detector.js +36 -12
  43. package/dist/task/orchestrator.d.ts +61 -126
  44. package/dist/task/orchestrator.js +70 -297
  45. package/dist/task/phases.d.ts +18 -0
  46. package/dist/task/phases.js +4 -3
  47. package/dist/task/plan-orchestrator.js +34 -33
  48. package/dist/task/requirements.d.ts +1 -1
  49. package/dist/task/requirements.js +50 -66
  50. package/dist/task/root-cause-repair.js +20 -32
  51. package/dist/task/run-bracket.d.ts +75 -0
  52. package/dist/task/run-bracket.js +41 -0
  53. package/dist/task/stall-detector.d.ts +110 -0
  54. package/dist/task/stall-detector.js +159 -0
  55. package/dist/task/task-gates.d.ts +69 -0
  56. package/dist/task/task-gates.js +114 -90
  57. package/dist/task/verify-work.d.ts +53 -67
  58. package/dist/task/verify-work.js +15 -11
  59. package/dist/workers/docs-core.d.ts +0 -4
  60. package/dist/workers/docs-core.js +10 -34
  61. package/dist/workers/docs-project.js +3 -3
  62. package/dist/workers/docs-resolve.d.ts +18 -0
  63. package/dist/workers/docs-resolve.js +39 -0
  64. package/dist/workers/docs-retrieve.d.ts +13 -0
  65. package/dist/workers/docs-retrieve.js +17 -2
  66. package/dist/workers/fetch-core.d.ts +0 -4
  67. package/dist/workers/fetch-core.js +2 -5
  68. package/dist/workers/phantom-imports.d.ts +3 -3
  69. package/dist/workers/phantom-imports.js +16 -29
  70. package/dist/workers/pi-worker-docs.d.ts +49 -0
  71. package/dist/workers/pi-worker-docs.js +33 -9
  72. package/dist/workers/pi-worker-fetch.d.ts +18 -0
  73. package/dist/workers/pi-worker-fetch.js +19 -4
  74. package/dist/workers/single-read-extension.d.ts +1 -1
  75. package/dist/workers/single-read-extension.js +5 -4
  76. package/dist/workers/single-read-guard.d.ts +32 -10
  77. package/dist/workers/single-read-guard.js +67 -16
  78. package/package.json +1 -1
@@ -1,7 +1,8 @@
1
1
  import { Type } from '@sinclair/typebox';
2
2
  import { Text } from '@earendil-works/pi-tui';
3
3
  import { FetchAndCleanError } from './html-clean.js';
4
- import { fetchFocused, formatResultText } from './fetch-core.js';
4
+ import { fetchFocused } from './fetch-core.js';
5
+ import { formatResultText } from '../shared/child-output.js';
5
6
  import { makeWorkerTool } from './shared.js';
6
7
  import { normalizeQuery } from './research-cache.js';
7
8
  import { isAbstention } from './abstention.js';
@@ -51,7 +52,8 @@ export function registerPiWorkerFetch(pi, internals = {}) {
51
52
  if (result.failure !== undefined) {
52
53
  return { text: result.failure, details: { childExitCode: result.childExitCode } };
53
54
  }
54
- const body = formatResultText({ answer: result.answer, excerpt: result.excerpt }, result.excerptVerified) || '(no output)';
55
+ const body = formatResultText('', // a fetched page answer carries no package header
56
+ { answer: result.answer, excerpt: result.excerpt }, result.excerptVerified) || '(no output)';
55
57
  // The coverage miss is the one outcome that carries an instruction. It goes
56
58
  // in the TEXT, not only in details: details are for the harness, and the
57
59
  // worker acts on what it reads.
@@ -90,7 +92,7 @@ export function registerPiWorkerFetch(pi, internals = {}) {
90
92
  // otherwise). The URL is kept verbatim (path case can matter); the query is
91
93
  // normalised. Both parts key the entry — same page, different question is a
92
94
  // different answer.
93
- cacheKey: params => `${params.url.trim()}::${normalizeQuery(params.query)}`,
95
+ cacheKey: fetchCacheKey,
94
96
  // Only a completed fetch (child exited 0) is a real answer; invalid-URL,
95
97
  // fetch failures, and aborts omit childExitCode:0 and fall through.
96
98
  // F-2(e), on the fetch channel. A child that ran fine and answered
@@ -99,6 +101,19 @@ export function registerPiWorkerFetch(pi, internals = {}) {
99
101
  // the same dead-end-paid-many-times shape pi-worker-docs already closed
100
102
  // for packages, with escalation unable to re-fire because the miss never
101
103
  // recurred. One predicate now covers every corpus (workers/abstention.ts).
102
- cacheable: (d, text) => d.childExitCode === 0 && !isAbstention(text)
104
+ cacheable: fetchCacheable
103
105
  });
104
106
  }
107
+ /**
108
+ * The F-2(e) cache rule for the fetch channel, named for the same reason as
109
+ * `docsCacheable`: pi-worker-fetch.test.ts carried a hand-retyped copy driving four
110
+ * tests, which a change to the shipped rule would leave green.
111
+ */
112
+ export function fetchCacheable(d, text) {
113
+ return d.childExitCode === 0 && !isAbstention(text);
114
+ }
115
+ /** The fetch cache key. URL verbatim (path case can matter), question normalised —
116
+ * same page, different question is a different answer. */
117
+ export function fetchCacheKey(params) {
118
+ return `${params.url.trim()}::${normalizeQuery(params.query)}`;
119
+ }
@@ -4,7 +4,7 @@ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
4
4
  * blocks, each returning a reason the model receives as an error tool result
5
5
  * (agent-loop: block → createErrorToolResult) so the worker continues instead of
6
6
  * looping:
7
- * - read: blocks any re-read of a file already read this run (path-keyed).
7
+ * - read: blocks a re-read of LINES already delivered; forward paging passes.
8
8
  * - grep/find/ls: blocks an identical repeat of the same call (args-keyed).
9
9
  * See single-read-guard.ts for why this is scoped to TOOLING.
10
10
  */
@@ -7,7 +7,7 @@ const DEDUP_TOOLS = new Set(['grep', 'find', 'ls']);
7
7
  * blocks, each returning a reason the model receives as an error tool result
8
8
  * (agent-loop: block → createErrorToolResult) so the worker continues instead of
9
9
  * looping:
10
- * - read: blocks any re-read of a file already read this run (path-keyed).
10
+ * - read: blocks a re-read of LINES already delivered; forward paging passes.
11
11
  * - grep/find/ls: blocks an identical repeat of the same call (args-keyed).
12
12
  * See single-read-guard.ts for why this is scoped to TOOLING.
13
13
  */
@@ -16,10 +16,11 @@ export default function (pi) {
16
16
  const calls = new RepeatedCallGuard();
17
17
  pi.on('tool_call', event => {
18
18
  if (event.toolName === 'read') {
19
- const path = event.input.path;
20
- if (typeof path !== 'string')
19
+ const input = event.input;
20
+ if (typeof input.path !== 'string')
21
21
  return;
22
- return reads.check(resolve(process.cwd(), path)) ?? undefined;
22
+ return (reads.check(resolve(process.cwd(), input.path), input.offset, input.limit)
23
+ ?? undefined);
23
24
  }
24
25
  if (DEDUP_TOOLS.has(event.toolName)) {
25
26
  return calls.check(event.toolName, event.input) ?? undefined;
@@ -6,10 +6,22 @@
6
6
  * as an error tool result, the worker continues). No kill, no restart: detect-
7
7
  * and-kill only re-spawns a model that deterministically re-thrashes.
8
8
  *
9
- * - SingleReadGuard: "read each file once". Validated against every recorded
10
- * mx5 run — a healthy TOOLING worker reads each file exactly once (max
11
- * same-file reads = 1 across 7 tasks). TASK_0017 re-read one file up to 50×.
12
- * Keyed on the resolved path so any re-read is blocked regardless of offset.
9
+ * - SingleReadGuard: "read each LINE of a file once". Validated against every
10
+ * recorded mx5 run — a healthy TOOLING worker reads each file exactly once
11
+ * (max same-file reads = 1 across 7 tasks). TASK_0017 re-read one file 50×.
12
+ *
13
+ * It used to key on the resolved path alone, blocking any second read
14
+ * "regardless of offset". That made a file bigger than one read into a trap.
15
+ * Measured 2026-08-17 on a captured auto-decompose request: the planner asked
16
+ * for `DESIGN/marketplace.html` with `limit: 80` — the first 80 lines of 743,
17
+ * deliberately paging — and its request for offset 80 was refused. It never
18
+ * reached the end of the file, and spent the rest of the run asking for the
19
+ * remainder: 197 of 200 tool calls were this guard's own refusal. The guard
20
+ * did not stop a thrash, it CAUSED one.
21
+ *
22
+ * So the unit is the line range, not the file. A request that extends past
23
+ * the furthest line already delivered is forward paging and passes; one that
24
+ * lies entirely within ground already delivered is a re-read and is blocked.
13
25
  *
14
26
  * - RepeatedCallGuard: "no identical search twice", for grep/find/ls. TASK_0017
15
27
  * also looped on grep({pattern:"^\\s*}",path:".../index.ts"}) ×5 — a path the
@@ -24,17 +36,27 @@ export interface ReadBlock {
24
36
  block: true;
25
37
  reason: string;
26
38
  }
27
- /** The error text the model receives in place of the re-read's contents. */
28
- export declare function singleReadReason(path: string): string;
39
+ /**
40
+ * The error text the model receives in place of the re-read's contents.
41
+ *
42
+ * It must say what to do NEXT, and the honest next move depends on whether there
43
+ * is any of the file left: with `covered` lines already delivered, asking for
44
+ * line `covered + 1` is always allowed, so the message says so. The old wording
45
+ * ("Do not read it again") was a dead end for a model that was mid-way through a
46
+ * file — it had nowhere legal to go and kept asking anyway.
47
+ */
48
+ export declare function singleReadReason(path: string, covered: number): string;
29
49
  export declare class SingleReadGuard {
30
- private readonly seen;
50
+ /** Furthest line already delivered per path; Infinity once a read hit EOF. */
51
+ private readonly covered;
31
52
  /**
32
- * Record a read of `resolvedPath`. Returns a ReadBlock the first time a path
33
- * is seen a second time (and every time after), else null on the first read.
53
+ * Record a read of `resolvedPath` over an optional line range. Returns a
54
+ * ReadBlock when the request lies entirely within lines already delivered,
55
+ * else null — which includes every first read and every forward page.
34
56
  * Callers pass an already-resolved/normalized path so `a.ts` and `./a.ts`
35
57
  * dedupe to one entry.
36
58
  */
37
- check(resolvedPath: string): ReadBlock | null;
59
+ check(resolvedPath: string, offset?: unknown, limit?: unknown): ReadBlock | null;
38
60
  }
39
61
  /** The error text the model receives in place of a repeated grep/find/ls call. */
40
62
  export declare function repeatedCallReason(toolName: string): string;
@@ -6,10 +6,22 @@
6
6
  * as an error tool result, the worker continues). No kill, no restart: detect-
7
7
  * and-kill only re-spawns a model that deterministically re-thrashes.
8
8
  *
9
- * - SingleReadGuard: "read each file once". Validated against every recorded
10
- * mx5 run — a healthy TOOLING worker reads each file exactly once (max
11
- * same-file reads = 1 across 7 tasks). TASK_0017 re-read one file up to 50×.
12
- * Keyed on the resolved path so any re-read is blocked regardless of offset.
9
+ * - SingleReadGuard: "read each LINE of a file once". Validated against every
10
+ * recorded mx5 run — a healthy TOOLING worker reads each file exactly once
11
+ * (max same-file reads = 1 across 7 tasks). TASK_0017 re-read one file 50×.
12
+ *
13
+ * It used to key on the resolved path alone, blocking any second read
14
+ * "regardless of offset". That made a file bigger than one read into a trap.
15
+ * Measured 2026-08-17 on a captured auto-decompose request: the planner asked
16
+ * for `DESIGN/marketplace.html` with `limit: 80` — the first 80 lines of 743,
17
+ * deliberately paging — and its request for offset 80 was refused. It never
18
+ * reached the end of the file, and spent the rest of the run asking for the
19
+ * remainder: 197 of 200 tool calls were this guard's own refusal. The guard
20
+ * did not stop a thrash, it CAUSED one.
21
+ *
22
+ * So the unit is the line range, not the file. A request that extends past
23
+ * the furthest line already delivered is forward paging and passes; one that
24
+ * lies entirely within ground already delivered is a re-read and is blocked.
13
25
  *
14
26
  * - RepeatedCallGuard: "no identical search twice", for grep/find/ls. TASK_0017
15
27
  * also looped on grep({pattern:"^\\s*}",path:".../index.ts"}) ×5 — a path the
@@ -21,25 +33,64 @@
21
33
  * Pure logic, no I/O — the extension does path resolution and tool routing.
22
34
  */
23
35
  import { stableStringify } from '../task/loop-detector.js';
24
- /** The error text the model receives in place of the re-read's contents. */
25
- export function singleReadReason(path) {
26
- return (`You already read ${path} earlier in this run — its contents are in your context. `
27
- + `Re-reading the same file is blocked. Do not read it again: use what you have already `
28
- + `gathered and write your final answer now.`);
36
+ /**
37
+ * The error text the model receives in place of the re-read's contents.
38
+ *
39
+ * It must say what to do NEXT, and the honest next move depends on whether there
40
+ * is any of the file left: with `covered` lines already delivered, asking for
41
+ * line `covered + 1` is always allowed, so the message says so. The old wording
42
+ * ("Do not read it again") was a dead end for a model that was mid-way through a
43
+ * file — it had nowhere legal to go and kept asking anyway.
44
+ */
45
+ export function singleReadReason(path, covered) {
46
+ if (!Number.isFinite(covered)) {
47
+ return (`You already read all of ${path} earlier in this run — its contents are in your `
48
+ + `context. Re-reading it is blocked. Use what you have already gathered and write `
49
+ + `your final answer now.`);
50
+ }
51
+ return (`You already read ${path} through line ${covered} earlier in this run — those lines are `
52
+ + `in your context. Re-reading them is blocked. To see more of this file, read it again `
53
+ + `starting at line ${covered + 1}; otherwise use what you have already gathered and `
54
+ + `write your final answer now.`);
55
+ }
56
+ /** Default `limit` pi's read tool applies when the call names none. */
57
+ const DEFAULT_READ_LIMIT = 2000;
58
+ /** The 1-based line a read starts at (`offset` absent or junk means line 1). */
59
+ function startLine(offset) {
60
+ return typeof offset === 'number' && Number.isFinite(offset) && offset >= 1 ?
61
+ Math.floor(offset)
62
+ : 1;
63
+ }
64
+ /**
65
+ * The last line a read reaches. A `limit` of exactly the tool default is treated
66
+ * as "no limit given" — indistinguishable at this layer, and the safe reading is
67
+ * the generous one, since blocking honest paging is the failure this guard had.
68
+ */
69
+ function endLine(start, limit) {
70
+ if (typeof limit !== 'number' || !Number.isFinite(limit) || limit <= 0)
71
+ return Infinity;
72
+ if (limit >= DEFAULT_READ_LIMIT)
73
+ return Infinity;
74
+ return start + Math.floor(limit) - 1;
29
75
  }
30
76
  export class SingleReadGuard {
31
- seen = new Set();
77
+ /** Furthest line already delivered per path; Infinity once a read hit EOF. */
78
+ covered = new Map();
32
79
  /**
33
- * Record a read of `resolvedPath`. Returns a ReadBlock the first time a path
34
- * is seen a second time (and every time after), else null on the first read.
80
+ * Record a read of `resolvedPath` over an optional line range. Returns a
81
+ * ReadBlock when the request lies entirely within lines already delivered,
82
+ * else null — which includes every first read and every forward page.
35
83
  * Callers pass an already-resolved/normalized path so `a.ts` and `./a.ts`
36
84
  * dedupe to one entry.
37
85
  */
38
- check(resolvedPath) {
39
- if (this.seen.has(resolvedPath)) {
40
- return { block: true, reason: singleReadReason(resolvedPath) };
86
+ check(resolvedPath, offset, limit) {
87
+ const seen = this.covered.get(resolvedPath);
88
+ const start = startLine(offset);
89
+ const end = endLine(start, limit);
90
+ if (seen !== undefined && end <= seen) {
91
+ return { block: true, reason: singleReadReason(resolvedPath, seen) };
41
92
  }
42
- this.seen.add(resolvedPath);
93
+ this.covered.set(resolvedPath, Math.max(seen ?? 0, end));
43
94
  return null;
44
95
  }
45
96
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.38.10",
3
+ "version": "0.38.12",
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",