@mjasnikovs/pi-task 0.13.25 → 0.13.27

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.
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  [![npm](https://img.shields.io/npm/v/@mjasnikovs/pi-task?color=cb3837&logo=npm)](https://www.npmjs.com/package/@mjasnikovs/pi-task)
10
10
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
11
11
  [![pi extension](https://img.shields.io/badge/pi-extension-7c3aed)](https://www.npmjs.com/package/@earendil-works/pi-coding-agent)
12
- [![tests](https://img.shields.io/badge/tests-559%20passing-3fb950)](#development)
12
+ [![tests](https://img.shields.io/badge/tests-731%20passing-3fb950)](#development)
13
13
  [![types](https://img.shields.io/badge/TypeScript-strict-3178c6?logo=typescript&logoColor=white)](./tsconfig.json)
14
14
 
15
15
  </div>
@@ -49,7 +49,8 @@ pi install npm:@mjasnikovs/pi-task
49
49
  | `/task-auto <feature>` | Plan a feature into a task list and run each title through `/task` in order (resumable). |
50
50
  | `/task-auto-resume` | Resume the active `/task-auto` run at the next unfinished task. |
51
51
  | `/task-auto-cancel` | Stop the `/task-auto` loop after the current task (still resumable). |
52
- | `/remote` | Show the QR code & URLs for the always-on web view (`/remote stop` to stop). Answer grill questions, start tasks, and watch progress from your phone. |
52
+ | `/task-config` | Toggle pi-task settings in an editor dialog: remote server, compress reasoning, and auto-commit. |
53
+ | `/remote` | Show the QR code & URLs for the web view (`/remote stop` to stop). Answer grill questions, start tasks, and watch progress from your phone. |
53
54
 
54
55
  ## The pipeline
55
56
 
@@ -77,10 +78,11 @@ A real feature is usually several tasks, not one. `/task-auto` is a thin planner
77
78
  - **Clarify first.** It asks the few clarifying questions whose answers change how the feature splits, then decomposes the answers into an ordered list of task titles written to `.pi-tasks/TASK_AUTO_NNNN.md`.
78
79
  - **Sequential, blocking.** Each title runs through `/task` to a spec, the spec is implemented, and the loop waits for that to finish before starting the next title. No overlap.
79
80
  - **Crash- and cancel-safe.** Progress is the markdown checkboxes in the AUTO file. `/task-auto-resume` (no id) automatically picks up the active run at the first unchecked title. If a title's `/task` run fails, the loop stops and leaves the run resumable.
81
+ - **One commit per task.** When **auto-commit** is on (the default) and you're in a git repo, the working tree is snapshotted into a single commit after each title passes, so the run produces a clean per-task history. It's best-effort: outside a repo, with nothing to commit, or on any git error, the loop reports the reason and keeps going. Toggle it in `/task-config`.
80
82
 
81
83
  ## Remote — drive a task from your phone
82
84
 
83
- The remote server is **always on** — it starts automatically with each session, with nothing taking up screen space. Run `/remote` any time to pop a QR code and the connection URLs: a **Tailscale** line and a **LAN** line when both are available (the QR encodes the Tailscale-preferred one). Open the URL on any device that can reach the host and you get a live view of the session: streaming output, tool calls, and the `/task` status block (phase, elapsed, context). It's bidirectional — the browser can:
85
+ The remote server is **on by default** — it starts automatically with each session, with nothing taking up screen space (disable it in `/task-config`). Run `/remote` any time to pop a QR code and the connection URLs: a **Tailscale** line and a **LAN** line when both are available (the QR encodes the Tailscale-preferred one). Open the URL on any device that can reach the host and you get a live view of the session: streaming output, tool calls, and the `/task` status block (phase, elapsed, context). It's bidirectional — the browser can:
84
86
 
85
87
  - **Answer grill / `/task-auto` clarify questions.** Each question appears as a card with the recommended default pre-filled (Accept), a free-text box (Submit), Skip, or Cancel task.
86
88
  - **Start and control tasks.** Type `/task …`, `/task-auto …`, `/task-cancel`, `/task-resume`, etc. — they run on the host.
@@ -136,6 +138,16 @@ Resolves an installed npm package, indexes its `.d.ts` files and README into a l
136
138
  - The first call for a `(package, version)` pair pays a one-time ingestion cost; later calls are FTS-only.
137
139
  - Cache lives at `${XDG_CACHE_HOME:-~/.cache}/pi-worker/docs.sqlite` — delete it to reset.
138
140
 
141
+ ## Settings — `/task-config`
142
+
143
+ Run `/task-config` to toggle pi-task's behavior in an editor dialog. Settings persist to `~/.config/pi-task/config.json` and all default to **on**:
144
+
145
+ | Setting | What it does |
146
+ | --- | --- |
147
+ | **remote** | The remote UI server (QR code, phone access). Turn off to never start it. |
148
+ | **compress reasoning** | After each message, compresses the model's `<think>` blocks down to the decisions/constraints/facts that matter later — keeping long local-model runs from drowning their own context in self-talk. |
149
+ | **auto-commit** | Snapshots the working tree into one git commit per `/task-auto` sub-task (see above). |
150
+
139
151
  ## Configuration
140
152
 
141
153
  | Variable | Used by | Notes |
@@ -153,7 +165,7 @@ Tasks are persisted to `<cwd>/.pi-tasks/TASK_NNNN.md`. Add `.pi-tasks/` to your
153
165
 
154
166
  ```sh
155
167
  bun install
156
- bun test src/ # 559 tests across 46 files
168
+ bun test src/ # 731 tests across 57 files
157
169
  bun run lint # prettier + eslint + tsc --noEmit
158
170
  bun run build # tsc → dist/
159
171
  ```
Binary file
@@ -56,6 +56,14 @@ export interface PhaseResearchDeps extends ExternalContextDeps {
56
56
  * mentions; the rest are unchanged or only lightly trimmed.
57
57
  */
58
58
  export declare function scopedToolingGoal(refined: string): string;
59
+ /**
60
+ * Build a degraded section body for a runaway worker: a one-line marker naming
61
+ * the failure (so downstream phases and a human reading the task file know this
62
+ * section is incomplete) followed by whatever partial answer the worker streamed
63
+ * before it was killed. The marker is always present even when there is no
64
+ * partial text, so an empty degrade is never mistaken for a real finding.
65
+ */
66
+ export declare function degradedSectionBody(name: string, reason: string, partial: string): string;
59
67
  export declare function phaseResearch(deps: PhaseDeps, refined: string, researchDeps?: PhaseResearchDeps): Promise<string>;
60
68
  export interface PhaseAutoAnswerDeps {
61
69
  docsFocused?: typeof docsFocused;
@@ -122,34 +122,67 @@ export function scopedToolingGoal(refined) {
122
122
  return firstBullet === -1 ? goal : goal.slice(0, firstBullet).trim();
123
123
  }
124
124
  /**
125
- * Throw a descriptive error if a research worker's result is not trustworthy
126
- * output, so only good output is ever cached and assembled. Precedence mirrors
127
- * the original inline checks: loop/timeout exhaustion first (a loop-kill is a
128
- * SIGTERM/exit 143 OR a clean exit 0 with partial text, so exitCode alone is
129
- * ambiguous the worker already burned its MAX_LOOP_RESTARTS restarts before
130
- * setting these), then exit code, empty output, and finally a leaked
131
- * (never-executed) tool call.
125
+ * Classify a research worker's result so the phase can react per-worker instead
126
+ * of treating every failure the same. Two distinct failure shapes:
127
+ *
128
+ * - 'runaway' (loop-kill OR per-worker wall-clock timeout): the worker explored
129
+ * too long and was killed *after* burning its MAX_LOOP_RESTARTS restarts. It
130
+ * did real work and left partial text; the other three workers are unaffected.
131
+ * Failing the whole task here would throw away every already-good worker AND
132
+ * abort the entire auto-run over the weakest section — and because the loop is
133
+ * deterministic, a resume just re-loops and re-fails. So this DEGRADES: keep
134
+ * the partial answer (marked), cache it, move on. A loop-kill is a SIGTERM
135
+ * (exit 143) OR a clean exit 0 with truncated text, so loopHit/timedOut — not
136
+ * exitCode — are the reliable signal and are checked first.
137
+ *
138
+ * - 'fatal' (non-zero exit that isn't a loop-kill, empty output, or a leaked
139
+ * never-executed tool call): the output is untrustworthy in a way partial text
140
+ * can't paper over (broken env, model disconnect, wrong tool-call dialect).
141
+ * These still throw — degrading them would launder a real breakage into a
142
+ * plausible-looking section. Returns null when the result is trustworthy.
132
143
  */
133
- function assertResearchWorkerOk(name, result) {
144
+ function classifyResearchWorker(name, result) {
134
145
  if (result.loopHit) {
135
146
  const argsStr = JSON.stringify(result.loopHit.call.args);
136
- throw new Error(`Research ${name} worker stuck in a loop — called `
137
- + `${result.loopHit.call.name}(${argsStr}) ×${result.loopHit.count} in the last `
138
- + `${result.loopHit.windowSize} calls and still looped after restarts`);
147
+ return {
148
+ kind: 'runaway',
149
+ reason: `stuck in a loop — called ${result.loopHit.call.name}(${argsStr}) `
150
+ + `×${result.loopHit.count} in the last ${result.loopHit.windowSize} calls `
151
+ + `and still looped after restarts`
152
+ };
139
153
  }
140
154
  if (result.timedOut) {
141
- throw new Error(`Research ${name} worker timed out after restarts`);
155
+ return { kind: 'runaway', reason: 'timed out after restarts' };
142
156
  }
143
157
  if (result.exitCode !== 0) {
144
- throw new Error(`Research ${name} worker failed (exit ${result.exitCode}): ${result.stderr.slice(-500)}`);
158
+ return {
159
+ kind: 'fatal',
160
+ error: new Error(`Research ${name} worker failed (exit ${result.exitCode}): ${result.stderr.slice(-500)}`)
161
+ };
145
162
  }
146
163
  if (result.text.trim().length === 0) {
147
- throw new Error(`Research ${name} worker produced no output`);
164
+ return { kind: 'fatal', error: new Error(`Research ${name} worker produced no output`) };
148
165
  }
149
166
  if (result.leakedToolCall) {
150
- throw new Error(`Research ${name} worker wrote a tool call as text instead of invoking it `
151
- + `(${result.leakedToolCall.trim()}) — it never ran`);
167
+ return {
168
+ kind: 'fatal',
169
+ error: new Error(`Research ${name} worker wrote a tool call as text instead of invoking it `
170
+ + `(${result.leakedToolCall.trim()}) — it never ran`)
171
+ };
152
172
  }
173
+ return null;
174
+ }
175
+ /**
176
+ * Build a degraded section body for a runaway worker: a one-line marker naming
177
+ * the failure (so downstream phases and a human reading the task file know this
178
+ * section is incomplete) followed by whatever partial answer the worker streamed
179
+ * before it was killed. The marker is always present even when there is no
180
+ * partial text, so an empty degrade is never mistaken for a real finding.
181
+ */
182
+ export function degradedSectionBody(name, reason, partial) {
183
+ const marker = `(degraded: research ${name} worker ${reason}; this section may be incomplete)`;
184
+ const body = partial.trim();
185
+ return body.length > 0 ? `${marker}\n\n${body}` : marker;
153
186
  }
154
187
  export async function phaseResearch(deps, refined, researchDeps = {}) {
155
188
  const fileInventoryFn = researchDeps.getFileInventory ?? getFileInventory;
@@ -265,11 +298,21 @@ export async function phaseResearch(deps, refined, researchDeps = {}) {
265
298
  + (r.stderr ? ` stderr=${r.stderr.slice(0, 300)}` : '')
266
299
  + (r.leakedToolCall ? ` leaked=${r.leakedToolCall.trim().slice(0, 80)}` : ''));
267
300
  updateProgress();
268
- // Throws (failing the phase) on a loop/timeout/exit/empty/leak — so the
269
- // workers that already succeeded above stay cached for the resume.
270
- assertResearchWorkerOk(spec.section, r);
271
- await setTaskSection(deps.cwd, deps.taskId, cacheHeading, r.text);
272
- sections.push({ name: spec.section, text: r.text.trim() });
301
+ // A fatal failure (crash/empty/leak) still throws the already-cached
302
+ // workers survive for the resume. A runaway (loop/timeout) degrades to its
303
+ // partial output instead, so one weak worker can't abort a whole auto-run;
304
+ // the degraded section is cached too, so a resume doesn't re-loop it.
305
+ const failure = classifyResearchWorker(spec.section, r);
306
+ if (failure?.kind === 'fatal')
307
+ throw failure.error;
308
+ const sectionText = failure?.kind === 'runaway' ?
309
+ degradedSectionBody(spec.section, failure.reason, r.text)
310
+ : r.text.trim();
311
+ if (failure?.kind === 'runaway') {
312
+ deps.logDebug?.(`${spec.label}: degraded — ${failure.reason}`);
313
+ }
314
+ await setTaskSection(deps.cwd, deps.taskId, cacheHeading, sectionText);
315
+ sections.push({ name: spec.section, text: sectionText });
273
316
  }
274
317
  // All workers succeeded — the assembled output below becomes the canonical
275
318
  // 'research' section (written by the orchestrator). The per-worker caches
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.13.25",
3
+ "version": "0.13.27",
4
4
  "description": "Deterministic spec-orchestration for local models, with a bundled real-time remote web view and web/docs/fetch/worker subagent tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -66,6 +66,7 @@
66
66
  "pi": {
67
67
  "extensions": [
68
68
  "dist/index.js"
69
- ]
69
+ ],
70
+ "image": "https://raw.githubusercontent.com/mjasnikovs/pi-task/main/assets/pipeline.png"
70
71
  }
71
72
  }