@mjasnikovs/pi-task 0.38.2 → 0.38.3

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 (46) hide show
  1. package/dist/config/config.d.ts +7 -0
  2. package/dist/config/config.js +10 -4
  3. package/dist/config/register.d.ts +37 -0
  4. package/dist/config/register.js +89 -114
  5. package/dist/remote/events.js +0 -3
  6. package/dist/remote/register.js +12 -3
  7. package/dist/task/auto-orchestrator.js +119 -94
  8. package/dist/task/command-run.d.ts +104 -0
  9. package/dist/task/command-run.js +138 -0
  10. package/dist/task/coverage-loop.d.ts +45 -0
  11. package/dist/task/critique-probes.d.ts +82 -0
  12. package/dist/task/critique-probes.js +156 -0
  13. package/dist/task/enforce-guidelines.d.ts +14 -17
  14. package/dist/task/enforce-guidelines.js +44 -31
  15. package/dist/task/final-gate.d.ts +8 -10
  16. package/dist/task/final-gate.js +36 -74
  17. package/dist/task/gate-child.d.ts +104 -0
  18. package/dist/task/gate-child.js +177 -0
  19. package/dist/task/gate-deps.js +57 -205
  20. package/dist/task/orchestrator.js +13 -22
  21. package/dist/task/phases.js +109 -182
  22. package/dist/task/plan-session.d.ts +4 -22
  23. package/dist/task/plan-session.js +4 -33
  24. package/dist/task/question-dialog.d.ts +71 -0
  25. package/dist/task/question-dialog.js +89 -0
  26. package/dist/task/terminal-outcome.d.ts +67 -0
  27. package/dist/task/terminal-outcome.js +76 -0
  28. package/dist/task/type-only-answer.js +2 -3
  29. package/dist/workers/abstention.d.ts +71 -0
  30. package/dist/workers/abstention.js +108 -0
  31. package/dist/workers/docs-chunk.d.ts +74 -0
  32. package/dist/workers/docs-chunk.js +143 -0
  33. package/dist/workers/docs-core.d.ts +10 -1
  34. package/dist/workers/docs-core.js +22 -19
  35. package/dist/workers/docs-index.js +2 -69
  36. package/dist/workers/docs-project.d.ts +15 -1
  37. package/dist/workers/docs-project.js +27 -66
  38. package/dist/workers/fetch-core.d.ts +1 -1
  39. package/dist/workers/fetch-core.js +2 -1
  40. package/dist/workers/pi-worker-core.js +157 -86
  41. package/dist/workers/pi-worker-docs.js +5 -10
  42. package/dist/workers/pi-worker-fetch.js +8 -1
  43. package/dist/workers/typeonly-log.js +2 -10
  44. package/dist/workers/worker-failure.d.ts +91 -0
  45. package/dist/workers/worker-failure.js +82 -0
  46. package/package.json +1 -1
@@ -3,7 +3,7 @@ import { Type } from '@sinclair/typebox';
3
3
  import { Text } from '@earendil-works/pi-tui';
4
4
  import { openCache as defaultOpenCache } from './docs-cache.js';
5
5
  import { retrieveChunks as defaultRetrieveChunks } from './docs-retrieve.js';
6
- import { docsRaw, formatResultText, buildPrompt, buildVersionBanner } from './docs-core.js';
6
+ import { docsRaw, formatResultText, packageHeader, buildPrompt, buildVersionBanner } from './docs-core.js';
7
7
  import { formatNpmVersionSection } from './npm-version.js';
8
8
  import { runFocusedExtraction } from './focused-extractor.js';
9
9
  import { makeWorkerTool } from './shared.js';
@@ -12,6 +12,7 @@ import { logDocsAnswer } from './typeonly-log.js';
12
12
  import { normalizeQuery } from './research-cache.js';
13
13
  import { projectDocsRaw, buildProjectPrompt } from './docs-project.js';
14
14
  import { projectDocsBudget, projectDocsBudgetExhausted } from '../task/research-fanout-budget.js';
15
+ import { isAbstention } from './abstention.js';
15
16
  const RENDER_QUERY_MAX = 100;
16
17
  const Params = Type.Object({
17
18
  module: Type.String({
@@ -189,13 +190,7 @@ export function registerPiWorkerDocs(pi, internals = {}) {
189
190
  if (!extraction.ok)
190
191
  return docsFailureResult(extraction, baseDetails, '');
191
192
  const verified = extraction.excerptVerified;
192
- const text = formatResultText({
193
- name: projectName,
194
- version: 'local',
195
- root: ctx.cwd,
196
- entryDts: null,
197
- readme: null
198
- }, extraction, verified);
193
+ const text = formatResultText(`Per ${projectName} (project source):`, extraction, verified);
199
194
  // SAME instrumentation channel as the package path below, extended to the
200
195
  // project-source branch because that branch is the MAJORITY of what
201
196
  // worker:apis asks — 13 of 17 docs calls in run 15's fatal task, 7 of 12 in
@@ -302,7 +297,7 @@ export function registerPiWorkerDocs(pi, internals = {}) {
302
297
  return docsFailureResult(extraction, baseDetails, versionBanner + npmHeader);
303
298
  }
304
299
  const verified = extraction.excerptVerified;
305
- const body = formatResultText(pkg, extraction, verified);
300
+ const body = formatResultText(packageHeader(pkg), extraction, verified);
306
301
  // F-2: a TYPE-ONLY answer is the dangerous failure. "unclear from this package"
307
302
  // is honest and already escalates; a signature is a well-formed, confident,
308
303
  // on-topic answer that names the very parameter asked about, so the worker
@@ -404,6 +399,6 @@ export function registerPiWorkerDocs(pi, internals = {}) {
404
399
  cacheable: (d, text) => d.childExitCode === 0
405
400
  && d.typeOnly !== true
406
401
  && d.excerptVerified !== false
407
- && !/unclear from this package/i.test(text)
402
+ && !isAbstention(text)
408
403
  });
409
404
  }
@@ -4,6 +4,7 @@ import { FetchAndCleanError } from './html-clean.js';
4
4
  import { fetchFocused, formatResultText } from './fetch-core.js';
5
5
  import { makeWorkerTool } from './shared.js';
6
6
  import { normalizeQuery } from './research-cache.js';
7
+ import { isAbstention } from './abstention.js';
7
8
  const RENDER_QUERY_MAX = 100;
8
9
  const Params = Type.Object({
9
10
  url: Type.String({ description: 'URL to fetch. Must be http or https.' }),
@@ -92,6 +93,12 @@ export function registerPiWorkerFetch(pi, internals = {}) {
92
93
  cacheKey: params => `${params.url.trim()}::${normalizeQuery(params.query)}`,
93
94
  // Only a completed fetch (child exited 0) is a real answer; invalid-URL,
94
95
  // fetch failures, and aborts omit childExitCode:0 and fall through.
95
- cacheable: d => d.childExitCode === 0
96
+ // F-2(e), on the fetch channel. A child that ran fine and answered
97
+ // "unclear from this page" exits 0, so caching on process health alone
98
+ // memoised the NON-ANSWER and re-served it to every later sibling task —
99
+ // the same dead-end-paid-many-times shape pi-worker-docs already closed
100
+ // for packages, with escalation unable to re-fire because the miss never
101
+ // recurred. One predicate now covers every corpus (workers/abstention.ts).
102
+ cacheable: (d, text) => d.childExitCode === 0 && !isAbstention(text)
96
103
  });
97
104
  }
@@ -37,17 +37,9 @@
37
37
  * reconstruct later.
38
38
  */
39
39
  import * as fs from 'node:fs';
40
+ import { isAbstention } from './abstention.js';
40
41
  /** Env var naming the JSONL sink. Unset (or empty) ⇒ instrumentation is entirely off. */
41
42
  export const TYPEONLY_LOG_ENV = 'PI_TASK_TYPEONLY_LOG';
42
- /**
43
- * The honest non-answer, in BOTH wordings the tool can emit. A package lookup is told to
44
- * write "unclear from this package" (docs-core.ts:622); a project-source lookup is told
45
- * "unclear from this project" (docs-project.ts:310). Matching only the first silently scored
46
- * every project-source abstention as a valid answer — and project-source is the MAJORITY of
47
- * what worker:apis asks (13 of 17 calls in run 15's fatal task), so that one missing word
48
- * would have put the wrong denominator under the whole termination diagnostic.
49
- */
50
- const UNCLEAR = /unclear from this (package|project)/i;
51
43
  /**
52
44
  * Append one record to the JSONL sink named by `PI_TASK_TYPEONLY_LOG`, if set.
53
45
  *
@@ -61,7 +53,7 @@ export function logDocsAnswer(rec, getEnv = k => process.env[k]) {
61
53
  const full = {
62
54
  at: new Date().toISOString(),
63
55
  ...rec,
64
- unclear: UNCLEAR.test(rec.answer)
56
+ unclear: isAbstention(rec.answer)
65
57
  };
66
58
  try {
67
59
  fs.appendFileSync(sink, `${JSON.stringify(full)}\n`, 'utf8');
@@ -0,0 +1,91 @@
1
+ /**
2
+ * How a worker child DIED — the one classification of a finished `runWorker`
3
+ * result, and the single place its precedence is written down.
4
+ *
5
+ * Why this module exists. `RunWorkerResult` reports each kill cause as its own
6
+ * optional field (`loopHit`, `timedOut`, `stalled`, `commandTimedOut`,
7
+ * `streamStalled`, `leakedToolCall`, `aborted`, `exitCode`), and every kill path
8
+ * ALSO sets `aborted` and a non-zero exit — killProc flips those on every route
9
+ * out. So a consumer cannot read the fields in any order it likes: the specific
10
+ * causes must be matched before the generic `aborted`/`exitCode` ones, or a dead
11
+ * backend is reported to the user as "you cancelled".
12
+ *
13
+ * That rule used to live as prose in three doc comments and as the source order
14
+ * of three hand-written ladders (`classifyEnforceChildFailure`,
15
+ * `classifyResearchWorker`, and the gate's own reading). Three copies of an
16
+ * ordering is three chances to drift, and it had already drifted: `streamStalled`
17
+ * was added to the result and to `finalAttemptFailed`, but the enforce ladder
18
+ * never grew an arm for it, so an enforcement child killed for a hung model
19
+ * stream fell all the way through to `if (aborted) return USER_CANCELLED` — the
20
+ * exact mislabel the comment above that line warns against.
21
+ *
22
+ * The fix is to make the order data. `FAILURE_RULES` is ordered, the first
23
+ * matching row wins, and consumers `switch` on the resulting `kind` instead of
24
+ * re-deriving the ladder. A new kill cause is one row here plus a compile error
25
+ * in every consumer that has not handled it — which is what makes the drift that
26
+ * caused the bug impossible rather than merely fixed.
27
+ */
28
+ import type { LoopHit } from '../task/loop-detector.js';
29
+ /**
30
+ * The subset of a finished child result this classification reads.
31
+ *
32
+ * Structural on purpose: `runWorker` returns a superset, and
33
+ * `EnforceChildResult` is a hand-written narrowing of the same shape. Typing the
34
+ * input as what is actually READ lets both pass without either importing the
35
+ * other's interface.
36
+ */
37
+ export interface WorkerFailureInput {
38
+ exitCode: number;
39
+ aborted: boolean;
40
+ timedOut?: boolean;
41
+ stalled?: boolean;
42
+ loopHit?: unknown;
43
+ leakedToolCall?: unknown;
44
+ commandTimedOut?: {
45
+ toolName: string;
46
+ timeoutMs: number;
47
+ };
48
+ streamStalled?: {
49
+ idleMs: number;
50
+ };
51
+ }
52
+ /**
53
+ * Why the child died, or `undefined` when it finished under its own power.
54
+ *
55
+ * Note what is NOT here: an empty answer, and a reported `modelError` on a run
56
+ * that still produced text. Neither is a kill — whether they count as a failure
57
+ * is the consumer's policy (research accepts a genuinely empty section; the gate
58
+ * does not), so folding them in would move a decision out of the module that
59
+ * owns it.
60
+ */
61
+ export type WorkerFailure = {
62
+ kind: 'stalled';
63
+ } | {
64
+ kind: 'command-timeout';
65
+ toolName: string;
66
+ timeoutMs: number;
67
+ } | {
68
+ kind: 'stream-stall';
69
+ idleMs: number;
70
+ } | {
71
+ kind: 'worker-timeout';
72
+ } | {
73
+ kind: 'loop';
74
+ hit: LoopHit;
75
+ } | {
76
+ kind: 'leaked-tool-call';
77
+ text: string;
78
+ } | {
79
+ kind: 'aborted';
80
+ } | {
81
+ kind: 'exit';
82
+ code: number;
83
+ };
84
+ /** The `kind` of every row, for exhaustiveness checks in consumers. */
85
+ export type WorkerFailureKind = WorkerFailure['kind'];
86
+ /**
87
+ * Classify a finished child. Returns `undefined` when nothing killed it —
88
+ * which is not the same as "it answered": the text may still be empty, and that
89
+ * judgement belongs to the caller.
90
+ */
91
+ export declare function classifyWorkerFailure(r: WorkerFailureInput): WorkerFailure | undefined;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * How a worker child DIED — the one classification of a finished `runWorker`
3
+ * result, and the single place its precedence is written down.
4
+ *
5
+ * Why this module exists. `RunWorkerResult` reports each kill cause as its own
6
+ * optional field (`loopHit`, `timedOut`, `stalled`, `commandTimedOut`,
7
+ * `streamStalled`, `leakedToolCall`, `aborted`, `exitCode`), and every kill path
8
+ * ALSO sets `aborted` and a non-zero exit — killProc flips those on every route
9
+ * out. So a consumer cannot read the fields in any order it likes: the specific
10
+ * causes must be matched before the generic `aborted`/`exitCode` ones, or a dead
11
+ * backend is reported to the user as "you cancelled".
12
+ *
13
+ * That rule used to live as prose in three doc comments and as the source order
14
+ * of three hand-written ladders (`classifyEnforceChildFailure`,
15
+ * `classifyResearchWorker`, and the gate's own reading). Three copies of an
16
+ * ordering is three chances to drift, and it had already drifted: `streamStalled`
17
+ * was added to the result and to `finalAttemptFailed`, but the enforce ladder
18
+ * never grew an arm for it, so an enforcement child killed for a hung model
19
+ * stream fell all the way through to `if (aborted) return USER_CANCELLED` — the
20
+ * exact mislabel the comment above that line warns against.
21
+ *
22
+ * The fix is to make the order data. `FAILURE_RULES` is ordered, the first
23
+ * matching row wins, and consumers `switch` on the resulting `kind` instead of
24
+ * re-deriving the ladder. A new kill cause is one row here plus a compile error
25
+ * in every consumer that has not handled it — which is what makes the drift that
26
+ * caused the bug impossible rather than merely fixed.
27
+ */
28
+ /**
29
+ * The ordered ladder. FIRST MATCH WINS — row order IS the precedence, and it is
30
+ * the only statement of it in the codebase.
31
+ *
32
+ * The order, and why:
33
+ *
34
+ * 1. `stalled` — no output AND the model endpoint did not answer a probe. The
35
+ * most specific diagnosis there is, and the one most easily lost: the kill
36
+ * aborts, so anything checked after `aborted` never sees it.
37
+ * 2. `command-timeout` — a watchdog kill naming the tool call that hung. Also
38
+ * aborts. Before the wall-clock timeout because it is the narrower cause
39
+ * (the two cannot be confused: a watchdog kill leaves the worker's own
40
+ * timeout flag false).
41
+ * 3. `stream-stall` — a watchdog kill for a model stream that went silent while
42
+ * no tool was running. Sits next to `command-timeout` because it is the same
43
+ * class of event: a watchdog, not the model, ended the attempt.
44
+ * 4. `worker-timeout` — the wall-clock backstop.
45
+ * 5. `loop` — killed for repeating one tool call past threshold. After the
46
+ * timeouts, matching the enforce ladder this replaces; in practice the two
47
+ * cannot both fire, since a loop kill stops the attempt before its own timer
48
+ * can expire.
49
+ * 6. `leaked-tool-call` — the model wrote a call as prose instead of invoking
50
+ * it. Only ever set on an otherwise clean run.
51
+ * 7. `aborted` — no specific cause survived, so this really is a cancel.
52
+ * 8. `exit` — a plain non-zero exit with no kill behind it: a crash.
53
+ */
54
+ const FAILURE_RULES = [
55
+ r => (r.stalled === true ? { kind: 'stalled' } : null),
56
+ r => r.commandTimedOut ?
57
+ {
58
+ kind: 'command-timeout',
59
+ toolName: r.commandTimedOut.toolName,
60
+ timeoutMs: r.commandTimedOut.timeoutMs
61
+ }
62
+ : null,
63
+ r => (r.streamStalled ? { kind: 'stream-stall', idleMs: r.streamStalled.idleMs } : null),
64
+ r => (r.timedOut === true ? { kind: 'worker-timeout' } : null),
65
+ r => (r.loopHit ? { kind: 'loop', hit: r.loopHit } : null),
66
+ r => (r.leakedToolCall ? { kind: 'leaked-tool-call', text: String(r.leakedToolCall) } : null),
67
+ r => (r.aborted ? { kind: 'aborted' } : null),
68
+ r => (r.exitCode !== 0 ? { kind: 'exit', code: r.exitCode } : null)
69
+ ];
70
+ /**
71
+ * Classify a finished child. Returns `undefined` when nothing killed it —
72
+ * which is not the same as "it answered": the text may still be empty, and that
73
+ * judgement belongs to the caller.
74
+ */
75
+ export function classifyWorkerFailure(r) {
76
+ for (const rule of FAILURE_RULES) {
77
+ const hit = rule(r);
78
+ if (hit)
79
+ return hit;
80
+ }
81
+ return undefined;
82
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.38.2",
3
+ "version": "0.38.3",
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",