@mjasnikovs/pi-task 0.38.30 → 0.38.32

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 (73) hide show
  1. package/README.md +2 -1
  2. package/dist/config/config.d.ts +16 -2
  3. package/dist/config/config.js +7 -2
  4. package/dist/config/group-args.d.ts +52 -0
  5. package/dist/config/group-args.js +110 -0
  6. package/dist/config/group-models.d.ts +88 -0
  7. package/dist/config/group-models.js +117 -0
  8. package/dist/config/groups.d.ts +76 -0
  9. package/dist/config/groups.js +110 -0
  10. package/dist/config/option-picker.d.ts +42 -0
  11. package/dist/config/option-picker.js +73 -0
  12. package/dist/config/reasoning.d.ts +22 -63
  13. package/dist/config/reasoning.js +37 -108
  14. package/dist/config/register.d.ts +98 -12
  15. package/dist/config/register.js +228 -23
  16. package/dist/index.js +4 -0
  17. package/dist/remote/push.js +1 -7
  18. package/dist/shared/command-watchdog.d.ts +63 -0
  19. package/dist/shared/command-watchdog.js +87 -0
  20. package/dist/shared/data-home.d.ts +8 -0
  21. package/dist/shared/data-home.js +14 -0
  22. package/dist/shared/model-endpoint.d.ts +53 -0
  23. package/dist/shared/model-endpoint.js +98 -2
  24. package/dist/shared/reasoning-capability.d.ts +25 -5
  25. package/dist/shared/reasoning-capability.js +18 -9
  26. package/dist/task/auto-orchestrator.js +14 -3
  27. package/dist/task/child-runner.d.ts +92 -15
  28. package/dist/task/child-runner.js +303 -66
  29. package/dist/task/context-usage.d.ts +46 -0
  30. package/dist/task/context-usage.js +41 -0
  31. package/dist/task/failure-classifier.js +24 -1
  32. package/dist/task/gate-child.d.ts +15 -4
  33. package/dist/task/gate-child.js +2 -2
  34. package/dist/task/gate-deps.js +7 -2
  35. package/dist/task/implementation-guards.d.ts +26 -0
  36. package/dist/task/implementation-guards.js +177 -0
  37. package/dist/task/implementation-hold.d.ts +118 -0
  38. package/dist/task/implementation-hold.js +165 -0
  39. package/dist/task/implementation-turn.d.ts +5 -0
  40. package/dist/task/implementation-turn.js +12 -1
  41. package/dist/task/loop-detector.d.ts +18 -0
  42. package/dist/task/loop-detector.js +22 -2
  43. package/dist/task/model-hold-stash.d.ts +43 -0
  44. package/dist/task/model-hold-stash.js +70 -0
  45. package/dist/task/orchestrator.d.ts +18 -5
  46. package/dist/task/orchestrator.js +63 -6
  47. package/dist/task/phases.js +18 -5
  48. package/dist/task/research-worker.d.ts +2 -2
  49. package/dist/task/research-worker.js +1 -1
  50. package/dist/workers/docs-core.js +2 -2
  51. package/dist/workers/docs-lookup.d.ts +4 -3
  52. package/dist/workers/docs-lookup.js +1 -1
  53. package/dist/workers/fetch-core.js +2 -2
  54. package/dist/workers/focused-extractor.d.ts +6 -4
  55. package/dist/workers/focused-extractor.js +17 -5
  56. package/dist/workers/index.js +2 -0
  57. package/dist/workers/model-warning.d.ts +69 -0
  58. package/dist/workers/model-warning.js +113 -0
  59. package/dist/workers/pi-worker-core.d.ts +9 -38
  60. package/dist/workers/pi-worker-core.js +8 -86
  61. package/dist/workers/pi-worker-docs.js +2 -2
  62. package/dist/workers/pi-worker.js +4 -4
  63. package/dist/workers/reasoning-warning.d.ts +17 -9
  64. package/dist/workers/reasoning-warning.js +69 -22
  65. package/dist/workers/single-read-guard.d.ts +6 -6
  66. package/dist/workers/single-read-guard.js +8 -8
  67. package/dist/workers/worker-profiles.d.ts +11 -3
  68. package/dist/workers/worker-profiles.js +33 -1
  69. package/package.json +1 -1
  70. package/dist/config/reasoning-args.d.ts +0 -23
  71. package/dist/config/reasoning-args.js +0 -28
  72. package/dist/task/implementation-thinking.d.ts +0 -56
  73. package/dist/task/implementation-thinking.js +0 -32
@@ -21,14 +21,14 @@
21
21
  *
22
22
  * - RepeatedCallGuard: "no identical search twice", for grep/find/ls — the
23
23
  * shapes the read guard cannot see, such as the same grep pattern re-run
24
- * against the same path. Keyed on `${toolName}\0${stableStringify(args)}`,
25
- * byte-identical to the key `LoopDetector.record` builds, so argument key
26
- * order never causes a miss and only an identical repeat trips. A different
27
- * pattern on the same file still passes.
24
+ * against the same path. Keyed with `loopKey`, the same identity
25
+ * `LoopDetector.record` uses, so argument key order never causes a miss and
26
+ * only an identical repeat trips. A different pattern on the same file still
27
+ * passes.
28
28
  *
29
29
  * Pure logic, no I/O — the extension does path resolution and tool routing.
30
30
  */
31
- import { stableStringify } from '../task/loop-detector.js';
31
+ import { loopKey } from '../task/loop-detector.js';
32
32
  /**
33
33
  * The error text the model receives in place of the re-read's contents.
34
34
  *
@@ -105,11 +105,11 @@ export class RepeatedCallGuard {
105
105
  /**
106
106
  * Record a `toolName` call with `args`. Returns a ReadBlock the second time
107
107
  * the same (toolName, stable-stringified args) pair is seen (and every time
108
- * after), else null on the first. Uses the LoopDetector's stableStringify so
109
- * argument key-order never causes a miss; only byte-identical calls collapse.
108
+ * after), else null on the first. Shares the LoopDetector's key, so argument
109
+ * key-order never causes a miss; only byte-identical calls collapse.
110
110
  */
111
111
  check(toolName, args) {
112
- const key = `${toolName}\x00${stableStringify(args)}`;
112
+ const key = loopKey({ name: toolName, args });
113
113
  if (this.seen.has(key)) {
114
114
  return { block: true, reason: repeatedCallReason(toolName) };
115
115
  }
@@ -242,15 +242,15 @@ export declare const DEFAULT_LOOP_PROGRESS: {
242
242
  readonly limit: 8;
243
243
  readonly churnFactor: 2;
244
244
  };
245
- export type WorkerProfileId = 'research' | 'gate' | 'adhoc';
245
+ export type WorkerProfileId = 'research' | 'gate' | 'adhoc' | 'phase';
246
246
  /**
247
247
  * The facts a profile needs that are NOT policy: user config, and which of the
248
248
  * four research workers is the docs-capable one.
249
249
  */
250
250
  export interface WorkerPolicyInputs {
251
- /** gate: `config.requestTimeoutMs`. */
251
+ /** gate, phase: `config.requestTimeoutMs`. */
252
252
  commandTimeoutMs?: number;
253
- /** gate: `config.streamInactivityMs`. */
253
+ /** gate, phase: `config.streamInactivityMs`. */
254
254
  streamInactivityMs?: number;
255
255
  /** research: only `worker:apis` fans out, so only it can be scaled. */
256
256
  fanoutBounded?: boolean;
@@ -289,6 +289,14 @@ export declare const WORKER_PROFILES: {
289
289
  carryForward: false;
290
290
  };
291
291
  };
292
+ readonly phase: {
293
+ readonly id: "phase";
294
+ readonly why: string;
295
+ readonly resolve: (inputs: WorkerPolicyInputs) => {
296
+ guards: WorkerGuards;
297
+ carryForward: false;
298
+ };
299
+ };
292
300
  };
293
301
  /** Resolve one profile. The only way a caller should obtain a policy. */
294
302
  export declare function workerPolicy(id: WorkerProfileId, inputs?: WorkerPolicyInputs): WorkerGuardPolicy;
@@ -50,7 +50,10 @@
50
50
  * `RESTART_ORDER` and `FAILURE_ORDER` are untouched. This is a third view of
51
51
  * the same key, not a merge of the two orderings.
52
52
  */
53
- import { LOOP_THRESHOLD, LOOP_WINDOW, MAX_LOOP_RESTARTS } from '../task/child-runner.js';
53
+ // From loop-detector.ts, NOT child-runner.ts: child-runner reads this table, and
54
+ // these are evaluated at module top level below, so that import would close a
55
+ // cycle whose only symptom is a TDZ ReferenceError on import order.
56
+ import { LOOP_THRESHOLD, LOOP_WINDOW, MAX_LOOP_RESTARTS } from '../task/loop-detector.js';
54
57
  import { CONTEXT_CHURN_FACTOR, NO_PROGRESS_LIMIT } from '../task/stall-detector.js';
55
58
  import { fanoutTimeoutPolicy, workerCarryForward, workerProgressCeilingMs } from '../task/research-fanout-budget.js';
56
59
  /**
@@ -198,6 +201,35 @@ export const WORKER_PROFILES = {
198
201
  guards['stream-stall'] = inputs.streamInactivityMs ?? 0;
199
202
  return { guards, carryForward: false };
200
203
  }
204
+ },
205
+ phase: {
206
+ id: 'phase',
207
+ why: 'The spec-pipeline and planning children (runPhaseChild). Every call site '
208
+ + "but one passes `read` or `''`, and none of them EDITS, so the "
209
+ + 'path-revisit rule stays ON, unlike gate: re-reading one file here is '
210
+ + 'the thrash it was written for, not the job. '
211
+ + 'THE COMMAND WATCHDOG IS WHY THIS ROW EXISTS. verify-tooling holds '
212
+ + '`read,bash`, and a hung command there was unkillable: the stream '
213
+ + 'watchdog SUSPENDS for the duration of a tool call, the dead-backend '
214
+ + 'probe reads a reachable endpoint as alive, and both runaway detectors '
215
+ + 'wait on a result that never arrives. Only a user ESC could end it. '
216
+ + 'The wall clock stays OFF as PHASE_CHILD_TIMEOUT_MS decided for a FIXED '
217
+ + "cap; that does not settle research's progress-based ceiling. "
218
+ + 'PARTIALLY CONSUMED: runPhaseChild has its own strike loop and reads '
219
+ + 'only `command-timeout`, `stream-stall`, `stalled` and `loop`, so '
220
+ + 'setting `worker-timeout` or `connection-error` here does NOTHING.',
221
+ resolve: inputs => {
222
+ const guards = baseGuards();
223
+ // INERT for this profile — runPhaseChild never reads it. Zeroed anyway
224
+ // so the row cannot be mistaken for research's armed 240s cap.
225
+ guards['worker-timeout'] = { timeoutMs: 0, progressCeilingMs: null, fanout: null };
226
+ // Both ceilings are the user's own settings, as they are for gate: the
227
+ // number is theirs, the decision to arm it is this row's. 0 from a
228
+ // caller that hands none, so a harness cannot silently acquire a guard.
229
+ guards['command-timeout'] = inputs.commandTimeoutMs ?? 0;
230
+ guards['stream-stall'] = inputs.streamInactivityMs ?? 0;
231
+ return { guards, carryForward: false };
232
+ }
201
233
  // `as const satisfies`, not an annotation — the same reason RESTART_ORDER
202
234
  // gives: an annotation widens each row back to `WorkerProfile`, and the
203
235
  // `why` strings and literal ids stop being visible to a reader or a test.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.38.30",
3
+ "version": "0.38.32",
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",
@@ -1,23 +0,0 @@
1
- /**
2
- * The live-config bridge for reasoning profiles: group in, argv fragment out.
3
- *
4
- * Separate from reasoning.ts because that module must take no import with a
5
- * runtime side effect — see its header. The `getConfig()` read lives here
6
- * instead: this file imports both, and nothing in config/ imports it back, so
7
- * the graph stays a tree.
8
- *
9
- * Read PER CALL, never cached at module scope, so a /task-config change lands on
10
- * the next child without a restart. Same contract `childBaseArgs` keeps.
11
- */
12
- import { type PiTaskConfig } from './config.js';
13
- import { type ReasoningGroup } from './reasoning.js';
14
- /**
15
- * The `['--thinking', level]` fragment for a group, or `[]` when the group is
16
- * `inherit` and the child should keep falling back to settings.json.
17
- *
18
- * Every argv builder calls this rather than reading config itself. The two
19
- * callers that skip it are not argv builders: the host-session turn
20
- * (implementation-thinking.ts) and the settings UI (register.ts) both need the
21
- * level itself, not a flag, so they call `resolveReasoning` directly.
22
- */
23
- export declare function groupThinkingArgs(group: ReasoningGroup, cfg?: PiTaskConfig): string[];
@@ -1,28 +0,0 @@
1
- /**
2
- * The live-config bridge for reasoning profiles: group in, argv fragment out.
3
- *
4
- * Separate from reasoning.ts because that module must take no import with a
5
- * runtime side effect — see its header. The `getConfig()` read lives here
6
- * instead: this file imports both, and nothing in config/ imports it back, so
7
- * the graph stays a tree.
8
- *
9
- * Read PER CALL, never cached at module scope, so a /task-config change lands on
10
- * the next child without a restart. Same contract `childBaseArgs` keeps.
11
- */
12
- import { getConfig } from './config.js';
13
- import { resolveReasoning, thinkingArgs } from './reasoning.js';
14
- /**
15
- * The `['--thinking', level]` fragment for a group, or `[]` when the group is
16
- * `inherit` and the child should keep falling back to settings.json.
17
- *
18
- * Every argv builder calls this rather than reading config itself. The two
19
- * callers that skip it are not argv builders: the host-session turn
20
- * (implementation-thinking.ts) and the settings UI (register.ts) both need the
21
- * level itself, not a flag, so they call `resolveReasoning` directly.
22
- */
23
- export function groupThinkingArgs(group, cfg) {
24
- // The default is evaluated HERE, per call. Hoisting the read to module scope
25
- // would leave every test green, so the optional parameter is what makes the
26
- // per-call contract assertable.
27
- return thinkingArgs(resolveReasoning(group, cfg ?? getConfig()));
28
- }
@@ -1,56 +0,0 @@
1
- /**
2
- * Hold the host session at the `implementation` group's thinking level for the
3
- * duration of one implementation turn, then put it back.
4
- *
5
- * WHY THIS GROUP IS NOT LIKE THE OTHERS
6
- * -------------------------------------
7
- * Every other reasoning group runs in a child process, so its level is one argv
8
- * flag (`--thinking <level>`, built in reasoning-args.ts) and it dies with the
9
- * child. The implementation turn runs in the USER'S OWN session
10
- * (orchestrator.ts `sendSpec` -> `sendUserMessage` -> `superviseImplementation`),
11
- * so the only lever is `pi.setThinkingLevel`, which is session-global.
12
- *
13
- * THREE THINGS pi DOES that this has to survive:
14
- *
15
- * 1. IT PERSISTS. pi-coding-agent's agent-session `setThinkingLevel` calls
16
- * `settingsManager.setDefaultThinkingLevel(...)` whenever the effective
17
- * level actually changes, and that writes pi's global settings file
18
- * (`~/.pi/agent/settings.json`). Without the restore, running one task would
19
- * silently rewrite the user's global default. That makes `release()`
20
- * load-bearing, not tidy-up.
21
- * 2. IT CLAMPS, to the levels the model declares. A model with no reasoning
22
- * support offers only `off`, so asking for `medium` yields `off`. The
23
- * restore therefore writes back what was READ after setting, never what was
24
- * asked for — otherwise a clamp would ratchet the stored default a little
25
- * further every run.
26
- * 3. IT IS OBSERVABLE, and the user can change it mid-turn: `shift+tab` is the
27
- * default binding for `app.thinking.cycle`, and a change invalidates the
28
- * footer. Restoring blindly would clobber a choice they just made. We detect
29
- * it by comparing the live level at release against what we applied: if it
30
- * has moved, somebody else moved it, and we leave it alone.
31
- *
32
- * We compare rather than subscribe because the extension API's `on(...)` returns
33
- * `void` — there is no unsubscribe handle — so a per-turn listener could only
34
- * ever be added, never removed. The comparison answers the same question with no
35
- * accumulating state.
36
- */
37
- import type { ThinkingLevel } from '@earendil-works/pi-agent-core';
38
- import { type GroupSetting } from '../config/reasoning.js';
39
- /**
40
- * The slice of the extension API this needs, named so tests can drive the
41
- * hold-and-restore with a fake object instead of a live pi session.
42
- */
43
- export interface ThinkingControl {
44
- get(): ThinkingLevel;
45
- set(level: ThinkingLevel): void;
46
- }
47
- /**
48
- * Put the session at the implementation group's level and return the function
49
- * that puts it back. Always call the returned function — `finally`, not the
50
- * happy path.
51
- *
52
- * `inherit` makes NO call at all, not even a redundant set-to-current. It means
53
- * the same thing here as in `thinkingArgs`, which emits no `--thinking` flag for
54
- * it: leave the level wherever it already is.
55
- */
56
- export declare function holdImplementationThinking(control: ThinkingControl, setting?: GroupSetting): () => void;
@@ -1,32 +0,0 @@
1
- import { getConfig } from '../config/config.js';
2
- import { resolveReasoning } from '../config/reasoning.js';
3
- /**
4
- * Put the session at the implementation group's level and return the function
5
- * that puts it back. Always call the returned function — `finally`, not the
6
- * happy path.
7
- *
8
- * `inherit` makes NO call at all, not even a redundant set-to-current. It means
9
- * the same thing here as in `thinkingArgs`, which emits no `--thinking` flag for
10
- * it: leave the level wherever it already is.
11
- */
12
- export function holdImplementationThinking(control, setting = resolveReasoning('implementation', getConfig())) {
13
- if (setting === 'inherit')
14
- return () => { };
15
- const before = control.get();
16
- control.set(setting);
17
- // Post-clamp, so a model that cannot do `medium` does not leave us believing
18
- // it is at `medium` and treating the user's later change as our own.
19
- const applied = control.get();
20
- if (applied === before)
21
- return () => { };
22
- let released = false;
23
- return () => {
24
- // Idempotent by contract: only the first call restores. A later call
25
- // would write `before` on top of whatever the level is by then.
26
- if (released)
27
- return;
28
- released = true;
29
- if (control.get() === applied)
30
- control.set(before);
31
- };
32
- }