@mjasnikovs/pi-task 0.38.22 → 0.38.24

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 (42) hide show
  1. package/README.md +2 -2
  2. package/dist/config/reasoning-args.d.ts +12 -1
  3. package/dist/config/reasoning-args.js +5 -2
  4. package/dist/config/reasoning.d.ts +60 -2
  5. package/dist/config/reasoning.js +344 -37
  6. package/dist/config/register.d.ts +76 -32
  7. package/dist/config/register.js +124 -82
  8. package/dist/shared/reasoning-capability.d.ts +2 -5
  9. package/dist/shared/reasoning-capability.js +31 -4
  10. package/dist/task/auto-orchestrator.d.ts +2 -0
  11. package/dist/task/auto-orchestrator.js +28 -41
  12. package/dist/task/child-runner.d.ts +89 -24
  13. package/dist/task/child-runner.js +67 -46
  14. package/dist/task/orchestrator.d.ts +14 -20
  15. package/dist/task/orchestrator.js +12 -9
  16. package/dist/task/phases.d.ts +16 -23
  17. package/dist/task/phases.js +47 -452
  18. package/dist/task/question-dialog.d.ts +56 -0
  19. package/dist/task/question-dialog.js +53 -0
  20. package/dist/task/research-worker.d.ts +180 -0
  21. package/dist/task/research-worker.js +432 -0
  22. package/dist/workers/brave-warning.js +4 -30
  23. package/dist/workers/docs-core.d.ts +8 -4
  24. package/dist/workers/docs-core.js +30 -21
  25. package/dist/workers/docs-lookup.d.ts +72 -0
  26. package/dist/workers/docs-lookup.js +53 -0
  27. package/dist/workers/docs-project.d.ts +9 -0
  28. package/dist/workers/docs-project.js +15 -0
  29. package/dist/workers/pi-worker-core.d.ts +87 -1
  30. package/dist/workers/pi-worker-core.js +3 -7
  31. package/dist/workers/pi-worker-docs.js +27 -31
  32. package/dist/workers/reasoning-warning.d.ts +10 -16
  33. package/dist/workers/reasoning-warning.js +25 -57
  34. package/dist/workers/session-hint.d.ts +37 -0
  35. package/dist/workers/session-hint.js +82 -0
  36. package/dist/workers/worker-failure.d.ts +34 -0
  37. package/dist/workers/worker-failure.js +27 -16
  38. package/dist/workers/worker-kill.d.ts +84 -0
  39. package/dist/workers/worker-kill.js +124 -0
  40. package/package.json +1 -1
  41. package/dist/task/reasoning-groups.d.ts +0 -36
  42. package/dist/task/reasoning-groups.js +0 -36
@@ -28,6 +28,7 @@
28
28
  * widget line, clarify's plan-shape and triage pre-emption, plan's control
29
29
  * actions. Those genuinely differ.
30
30
  */
31
+ import { stripInlineMarkdown } from './inline-markdown.js';
31
32
  /** True when the question is a binary fork rather than a single recommendation. */
32
33
  export function isTwoOption(p) {
33
34
  return p.suggested !== undefined && p.alt !== undefined;
@@ -87,3 +88,55 @@ export function resolveAnswer(p, raw) {
87
88
  }
88
89
  return { answer: typed, source: 'typed' };
89
90
  }
91
+ /**
92
+ * Settle one question: YOLO short-circuit, cards, ask, record.
93
+ *
94
+ * Returns `'cancelled'` rather than throwing, because that is the one thing the
95
+ * two callers genuinely disagree about — grill throws `USER_CANCELLED` into the
96
+ * phase ladder, clarify announces and returns null from the plan. Everything
97
+ * before it was written out twice at ~50 lines each and had already drifted on
98
+ * `recommended2`, harmless today only because the bridge re-guards it.
99
+ */
100
+ export async function settleQuestion(input) {
101
+ const { ui, transcript, plain, shown, render, yolo } = input;
102
+ const plainSuggested = input.suggested === undefined ? undefined : stripInlineMarkdown(input.suggested);
103
+ const plainAlt = input.alt === undefined ? undefined : stripInlineMarkdown(input.alt);
104
+ if (yolo !== null) {
105
+ // The stamp is a RECORD fact, and which kinds are stamped is the
106
+ // transcript policy's call — never this call site's.
107
+ if (yolo.kind === 'answer') {
108
+ transcript.add('yolo', plain, stripInlineMarkdown(yolo.answer));
109
+ }
110
+ else {
111
+ transcript.add('yolo-skip', plain, `(skipped — ${yolo.note})`);
112
+ }
113
+ return 'settled';
114
+ }
115
+ const pending = {
116
+ plain,
117
+ shown,
118
+ ...(plainSuggested !== undefined && {
119
+ suggested: plainSuggested,
120
+ shownSuggested: render(input.suggested)
121
+ }),
122
+ ...(plainAlt !== undefined && { alt: plainAlt, shownAlt: render(input.alt) })
123
+ };
124
+ const options = buildOptionCards(pending);
125
+ input.onAsk?.();
126
+ const a = await ui.ask({
127
+ localTitle: shown,
128
+ displayQuestion: shown,
129
+ question: plain,
130
+ recommended: plainSuggested,
131
+ ...(plainAlt !== undefined && { recommended2: plainAlt }),
132
+ allowSkip: plainSuggested === undefined && plainAlt === undefined,
133
+ ...(options && { options })
134
+ });
135
+ if (a === undefined)
136
+ return 'cancelled';
137
+ // An accept covers both routes to it: submitting empty, and pressing the
138
+ // single green card.
139
+ const resolved = resolveAnswer(pending, a);
140
+ transcript.add(resolved.source === 'accepted' ? 'accepted' : 'typed', plain, resolved.answer);
141
+ return 'settled';
142
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * ONE research worker, cache-skip to persist.
3
+ *
4
+ * WHY IT IS A MODULE. This was a 228-line closure inside `phaseResearch` over
5
+ * eleven locals, and inside it live the three RETRY GATES and their precedence:
6
+ * the EMPTY-SECTION gate (the only one that can fail the phase), the
7
+ * ZERO-RETRIEVAL gate and the SILENT gate (both of which discard a failed retry
8
+ * and ship the original). Getting that order wrong is how a run either dies on a
9
+ * legitimately empty section or ships one written from memory.
10
+ *
11
+ * The cost was in the TESTS. Reaching the gates meant a temp dir, a real task
12
+ * file, and a fake spawn routed on prose lifted out of `prompts.ts` — plus, for
13
+ * attempt-1-vs-attempt-2, a second sentence lifted out of a module-private
14
+ * preamble constant. In a codebase whose whole workflow is re-wording prompts and
15
+ * measuring what changed, that means a reworded preamble silently stops the gate
16
+ * tests from testing the gate. Behind this interface a test scripts
17
+ * `runWorker(label, attempt)` and states the `RunWorkerResult` fields a gate
18
+ * reads.
19
+ *
20
+ * THE INVARIANT, stated once: the empty gate runs FIRST and is the only one that
21
+ * can throw; the other two keep the original when their retry does not improve a
22
+ * named measure; and `confirmedEmpty` suppresses the silent gate, because that
23
+ * retry already asked "you wrote nothing" and got the same answer.
24
+ */
25
+ import type { RunWorkerInput, RunWorkerResult } from '../workers/pi-worker-core.js';
26
+ import type { SpawnFn } from '../shared/child-process.js';
27
+ import type { DebugLine } from './debug-log.js';
28
+ /**
29
+ * One research worker's row. `section` is the heading its output is assembled
30
+ * and cached under; `label` is its child NAME — what the loader, the debug trail
31
+ * and the A/B ledgers print, and the key into `REASONING_GROUP_BY_CHILD`.
32
+ */
33
+ export interface ResearchWorkerSpec {
34
+ section: string;
35
+ label: string;
36
+ /** Static, or built from the sections completed so far (serial mode hands
37
+ * APIS the finished FILES map; parallel mode hands it nothing). */
38
+ prompt: string | ((prior: ReadonlyArray<{
39
+ name: string;
40
+ text: string;
41
+ }>) => string);
42
+ tools?: string;
43
+ extensions?: string[];
44
+ /** Deterministic gate over the worker's own output, run BEFORE the section is
45
+ * persisted, so nothing it rejects survives into the cache or into compose. */
46
+ postProcess?: (text: string) => string;
47
+ /** When set, a non-empty section produced with ZERO grounding-retrieval calls is
48
+ * re-run ONCE with this preamble prepended, forcing a retrieval-first pass. The
49
+ * retry replaces the original only if it actually retrieved. */
50
+ zeroRetrievalRetry?: string;
51
+ /** When set, a section that comes out SILENT — zero parseable bullets from a
52
+ * loop-degrade banner or a hallucinated non-bullet fragment — is re-run ONCE
53
+ * with this preamble. A legitimately-empty section is NOT retried. */
54
+ retryIfSilent?: string;
55
+ /** This worker can issue project-source docs lookups, so the 5B fan-out bounds
56
+ * apply to it (see task/research-fanout-budget.ts). */
57
+ fanoutBounded?: true;
58
+ }
59
+ /**
60
+ * The small record the driver needs: one way to run a worker, and where to put
61
+ * what comes back.
62
+ *
63
+ * Everything here is a fact about THIS RUN, not about the worker — which is what
64
+ * lets the four rows be plain data and the driver be a function of two arguments.
65
+ */
66
+ export interface ResearchWorkerRun {
67
+ runWorker: (label: string, input: RunWorkerInput) => Promise<RunWorkerResult>;
68
+ cwd: string;
69
+ taskId: string;
70
+ signal: AbortSignal;
71
+ spawn?: SpawnFn;
72
+ /** The `--thinking` fragment for a named child. */
73
+ thinkingFor: (label: string) => string[];
74
+ logDebug?: (msg: string, kind?: DebugLine) => void;
75
+ onChildOutput?: (line: string) => void;
76
+ /** Record one finished worker's timing splits. */
77
+ record: (label: string, p: Promise<RunWorkerResult>) => Promise<RunWorkerResult>;
78
+ /** Called once per worker that reaches an outcome, cached ones included. */
79
+ onDone: () => void;
80
+ /**
81
+ * Read one worker's cached output back, or '' when there is none.
82
+ *
83
+ * A SEAM, and the symmetric half of `persistSection`. The cache skip is one
84
+ * of the four outcomes this driver has, and reaching it used to require a
85
+ * real task file on disk — which is most of why the gate tests needed a temp
86
+ * dir at all.
87
+ */
88
+ readCached: (heading: string) => Promise<string>;
89
+ /** Write one validated section to the task file. Serialised by the caller. */
90
+ persistSection: (heading: string, text: string) => Promise<void>;
91
+ /** 5B RESCUE / SCALE knobs, resolved once for the phase. */
92
+ carryForward: boolean;
93
+ fanoutTimeout: {
94
+ perLookupMs: number;
95
+ ceilingMs: number;
96
+ } | null;
97
+ progressCeilingMs: number | null;
98
+ }
99
+ /**
100
+ * Task-file heading under which a research worker's validated output is cached.
101
+ * A resumed research phase reads these to skip workers that already succeeded,
102
+ * instead of re-running all four from scratch when one of them fails — the
103
+ * expensive case (e.g. 3 healthy workers thrown away because the 4th looped).
104
+ */
105
+ export declare function researchWorkerCacheHeading(section: string): string;
106
+ /**
107
+ * Classify a research worker's result so the phase can react per-worker instead
108
+ * of treating every failure the same. Two distinct failure shapes:
109
+ *
110
+ * - 'runaway' (loop-kill OR per-worker wall-clock timeout): the worker explored
111
+ * too long and was killed *after* burning its MAX_LOOP_RESTARTS restarts. It
112
+ * did real work and left partial text; the other three workers are unaffected.
113
+ * Failing the whole task here would throw away every already-good worker AND
114
+ * abort the entire auto-run over the weakest section — and because the loop is
115
+ * deterministic, a resume just re-loops and re-fails. So this DEGRADES: keep
116
+ * the partial answer (marked), cache it, move on. A loop-kill is a SIGTERM
117
+ * (exit 143) OR a clean exit 0 with truncated text, so loopHit/timedOut — not
118
+ * exitCode — are the reliable signal and are checked first.
119
+ *
120
+ * - 'fatal' (non-zero exit that isn't a loop-kill, a provider error behind an
121
+ * empty answer, or a leaked never-executed tool call): the output is
122
+ * untrustworthy in a way partial text can't paper over (broken env, model
123
+ * disconnect, wrong tool-call dialect). These still throw — degrading them
124
+ * would launder a real breakage into a plausible-looking section.
125
+ *
126
+ * - 'empty' (clean exit 0, no provider error, no loop/timeout — the model simply
127
+ * wrote nothing): NOT a failure. On an extremely simple task ("create a folder
128
+ * with an index.html in it") three of the four workers have genuinely nothing
129
+ * to report, and each worker prompt tells the model to emit ONLY what this task
130
+ * touches and to drop everything else — so silence is the CORRECT answer and
131
+ * was killing the whole task at research (issue #10). Measured live on the
132
+ * issue's own prompt (30 reps/worker, local Qwen3.6-27B): every APIS answer was
133
+ * semantically "there is nothing here", and 2/30 were literally zero bytes on a
134
+ * clean exit — the other 28 survived only because the model happened to wrap the
135
+ * same non-answer in a parenthetical, which is model style, not signal. The
136
+ * caller retries once and then accepts an explicit empty section; what stays
137
+ * fatal is silence WITH a reported cause, which is the masked-disconnect case
138
+ * this branch was written for and which `modelError` now names outright.
139
+ *
140
+ * Returns null when the result is trustworthy.
141
+ */
142
+ export declare function classifyResearchWorker(name: string, result: RunWorkerResult): {
143
+ kind: 'runaway';
144
+ reason: string;
145
+ } | {
146
+ kind: 'fatal';
147
+ error: Error;
148
+ } | {
149
+ kind: 'empty';
150
+ } | null;
151
+ /**
152
+ * Build a degraded section body for a runaway worker: a one-line marker naming
153
+ * the failure (so downstream phases and a human reading the task file know this
154
+ * section is incomplete) followed by whatever partial answer the worker streamed
155
+ * before it was killed. The marker is always present even when there is no
156
+ * partial text, so an empty degrade is never mistaken for a real finding.
157
+ */
158
+ export declare function degradedSectionBody(name: string, reason: string, partial: string): string;
159
+ /**
160
+ * The body written for a research section the worker confirmed has no entries.
161
+ *
162
+ * Three states have to stay distinguishable to anyone — human or later phase —
163
+ * reading a research section, so each carries its own marker:
164
+ * `(none — …)` the worker RAN and answered "nothing applies" (this)
165
+ * `(degraded: …)` the worker was killed mid-answer, text may be partial
166
+ * (degradedSectionBody)
167
+ * section absent the worker never got that far — the phase threw
168
+ *
169
+ * Naming the worker inside the marker keeps it true after assembly, where the
170
+ * section headings are all that separate the four workers' output.
171
+ */
172
+ export declare function emptySectionBody(name: string): string;
173
+ export declare function isBareNoneAnswer(text: string): boolean;
174
+ export declare function runResearchWorker(spec: ResearchWorkerSpec, run: ResearchWorkerRun, prior?: ReadonlyArray<{
175
+ name: string;
176
+ text: string;
177
+ }>): Promise<{
178
+ name: string;
179
+ text: string;
180
+ }>;