pi-long-task 0.3.12 → 0.3.14

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/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ Notable changes to Pi Long Task are recorded here. This project follows semantic versioning.
4
+
5
+ ## 0.3.14 - 2026-08-20
6
+
7
+ ### Documentation and metadata
8
+
9
+ - Improve npm and GitHub discoverability with a richer package description, expanded keywords, and added `repository`, `homepage`, `bugs`, and `engines` fields.
10
+ - Add npm version, Node.js, and license badges, a keyword-rich introduction, and the embedded package preview image to the README.
11
+
12
+ ## 0.3.13 - 2026-07-22
13
+
14
+ ### Fixed
15
+
16
+ - Require complete, machine-readable worker results and reject false completion after session errors, timeouts, or cancellation.
17
+ - Bound worker and reviewer cancellation even when an SDK prompt or graceful follow-up does not settle.
18
+ - Preserve checked TODO progress, durable attempt ordering, retry commit baselines, and accurate failure evidence.
19
+ - Resume persisted goal-loop phases without repeating completed generation or execution work.
20
+ - Enforce overall and iteration deadlines consistently across generation, execution, and review.
21
+ - Retain structured terminal goal results and persisted cost totals across cancellation, failure, and resume paths.
22
+
23
+ ### Compatibility and maintenance
24
+
25
+ - Declare the supported Node.js floor as 22.19.0 and document validation against Pi 0.80.7, 0.80.8, and 0.81.1.
26
+ - Refresh the controllable transitive development lockfile resolutions for `brace-expansion` and `protobufjs`.
package/README.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # Pi Long Task
2
2
 
3
- Pi Long Task is a Pi extension that breaks large coding requests into tracked TODOs, executes them in isolated worker sessions, registers a real Pi TUI progress sidebar while a run is active, and optionally commits completed work.
3
+ [![npm version](https://img.shields.io/npm/v/pi-long-task.svg)](https://www.npmjs.com/package/pi-long-task)
4
+ [![Node.js >= 22.19](https://img.shields.io/badge/node-%3E%3D22.19-brightgreen)](https://nodejs.org/)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+
7
+ **Pi Long Task** is a long-running task runner and subagent orchestrator for the [Pi coding agent](https://github.com/earendil-works/pi). It is a Pi extension that breaks large coding requests into tracked TODOs, executes them in isolated AI worker sessions, registers a real Pi TUI progress sidebar while a run is active, and optionally commits completed work.
8
+
9
+ If you are looking for a way to run long-running, multi-step, autonomous coding tasks with Pi — refactors, test coverage pushes, full feature builds, or entire product goals — this extension handles the planning, delegation, progress tracking, retries, and safe git commits for you.
4
10
 
5
11
  Use it when a coding request is bigger than one focused interaction. Pi Long Task creates or cleans up the TODO plan, hands each TODO to a fresh worker session, tracks every attempt, and keeps the run artifacts so you can inspect what happened later.
6
12
 
@@ -194,10 +200,12 @@ Use `with commits` or `commit true` only when you want Pi Long Task to create el
194
200
 
195
201
  ### 3. Monitor progress and completion
196
202
 
197
- During execution, Pi Long Task creates `tmp/pi-long-task/<run-id>/TODO.md` and `TASK_RESULT.md`, runs one isolated worker session per unfinished TODO in order, and retries unfinished tasks up to the configured attempt limit. In Pi TUI, watch the Long Task sidebar/widget for the active task, subtask checklist, task timeline, counts, and worker spend when available. In headless or non-UI runs, watch the partial tool-result updates in the main output. When the run finishes, the final response lists completed, failed, blocked, and remaining task counts plus the result and TODO file paths.
203
+ During execution, Pi Long Task creates `tmp/pi-long-task/<run-id>/TODO.md` and `TASK_RESULT.md`, runs one isolated worker session per unfinished TODO in order, and retries unfinished tasks up to the configured attempt limit. Checked progress in pasted TODO markdown is preserved, so completed tasks are skipped when that artifact is supplied again. A task is marked complete only after the worker returns every required `TASK_RESULT` field without a session error, timeout, or cancellation, and the attempt evidence is appended before the TODO completion marker. In Pi TUI, watch the Long Task sidebar/widget for the active task, subtask checklist, task timeline, counts, and worker spend when available. In headless or non-UI runs, watch the partial tool-result updates in the main output. When the run finishes, the final response lists completed, failed, blocked, and remaining task counts plus the result and TODO file paths.
198
204
 
199
205
  ## What it looks like
200
206
 
207
+ ![Pi Long Task running in the Pi TUI with a live progress sidebar showing the task timeline, subtasks, and worker spend](docs/assets/package-preview.png)
208
+
201
209
  In Pi TUI, Pi Long Task keeps worker activity in the main tool result flow and registers a real right-side TUI sidebar for the run timeline:
202
210
 
203
211
  ```text
@@ -331,16 +339,16 @@ When a `pi_goal_task` goal is already concrete, existing direct behavior is pres
331
339
 
332
340
  `pi_long_task` behavior is unchanged. Discovery is only enabled by default for `pi_goal_task`; direct long-task planning, TODO normalization, worker execution, progress display, retries, artifacts, and commit behavior continue to work as before.
333
341
 
334
- Goal-loop artifacts are stored under `tmp/pi-goal-task/<goal-run-id>/`, including `GOAL_STATE.json`, `GOAL_TRACE.jsonl`, `GOAL_RESULT.md`, optional `GOAL_SPEC.json` for discovered goals, and per-iteration generated TODO, worker, and reviewer files. Child TODO execution still writes normal `tmp/pi-long-task/<run-id>/` artifacts.
342
+ Goal-loop artifacts are stored under `tmp/pi-goal-task/<goal-run-id>/`, including `GOAL_STATE.json`, `GOAL_TRACE.jsonl`, `GOAL_RESULT.md`, optional `GOAL_SPEC.json` for discovered goals, and per-iteration generated TODO, worker, and reviewer files. Persisted `pending`, `todo_generated`, `todo_executed`, `failed`, and reviewed boundaries can be resumed by SDK callers without rerunning completed phases or rewriting existing result/trace history. Child TODO execution still writes normal `tmp/pi-long-task/<run-id>/` artifacts.
335
343
 
336
344
  Safety controls:
337
345
 
338
346
  - `minIterations` prevents early success before the requested number of loops; default is `1`.
339
347
  - `maxIterations` stops retry loops when the reviewer keeps finding remaining work; default is `50`. If explicitly provided without `minIterations`, it is also used as the minimum target.
340
348
  - `timeoutMs` caps the overall goal loop; default is `172800000` ms (48 hours).
341
- - `iterationTimeoutMs` caps each generated TODO worker iteration; default is `10800000` ms (3 hours).
342
- - `reviewerTimeoutMs` caps each reviewer session; default is `1800000` ms (30 minutes).
343
- - tool cancellation is passed through and stops the loop with `cancelled` status.
349
+ - `iterationTimeoutMs` caps each generation, execution, and review sequence; default is `10800000` ms (3 hours).
350
+ - `reviewerTimeoutMs` caps each reviewer session within the remaining overall and iteration budgets; default is `1800000` ms (30 minutes).
351
+ - tool cancellation is passed through, bounded locally even if an SDK prompt does not settle after abort, and stops the loop with `cancelled` status.
344
352
  - `maxAttemptsPerTask` and `maxBashTimeoutMs` are forwarded to worker long-task runs.
345
353
  - `commit` controls whether implementation workers may commit; goal loops default to `commit true`, so pass `commit false` when you want to review all changes first.
346
354
 
@@ -414,12 +422,18 @@ When `commit` is `true`, it may commit eligible task changes after a task report
414
422
 
415
423
  - generated run files under `tmp/pi-long-task/`
416
424
  - generated `TASK_RESULT.md` files
417
- - files that were already dirty before the task started
425
+ - files that were already dirty before the task's first attempt (the same protected baseline is retained across retries)
418
426
 
419
427
  This lets you keep existing local work separate from Pi Long Task changes.
420
428
 
421
429
  Commit messages are generated from the task title and adjusted to resemble recent commit-message style in the repository. Pi Long Task does not prefix commits with generated labels like `Complete TODO 1 — ...`.
422
430
 
431
+ ## Runtime compatibility
432
+
433
+ Pi Long Task requires Node.js 22.19 or newer, matching the supported runtime of the Pi SDK versions used by the package. The extension and isolated-worker setup are validated against Pi 0.80.7 (legacy `AuthStorage`/`ModelRegistry`), Pi 0.80.8 (the `ModelRuntime` transition), and Pi 0.81.1. Older Pi releases may use the legacy fallback but are not part of the validated matrix.
434
+
435
+ The Pi TUI is optional: JSON and print modes run without sidebar registration, and RPC mode uses the non-component widget path. Git is required only when commit mode is enabled. Model credentials are not required to load the extension, but they are required to execute planner, worker, or reviewer model sessions.
436
+
423
437
  ## Development and validation
424
438
 
425
439
  Run the local development checks:
@@ -429,7 +443,7 @@ cd /path/to/pi-long-task
429
443
  npm run check
430
444
  ```
431
445
 
432
- The development SDK tracks current Pi releases. Pi Long Task uses Pi's `ModelRuntime` worker API on Pi 0.80.8 and newer, with a legacy worker-model fallback for older compatible Pi releases.
446
+ The development SDK tracks current Pi releases. There is no separate build command: Pi loads the TypeScript extension directly through its supported package loader, while `npm run typecheck` validates the source without emitting JavaScript.
433
447
 
434
448
  Check that Pi can load the extension:
435
449
 
@@ -454,6 +468,10 @@ That smoke test creates disposable git repos and verifies both `commit: false` a
454
468
  - Worker spend is added to the main Pi `$ spent` total as cost-only usage. Token counts are not merged into the main thread because worker sessions have separate context windows, and merging their token usage would corrupt the main conversation's context statistics.
455
469
  - Run artifacts are written under `tmp/pi-long-task/<run-id>/`.
456
470
 
471
+ ## Keywords
472
+
473
+ Pi extension, Pi package, Pi coding agent, AI coding agent, AI coding assistant, LLM agent, agentic coding, subagent orchestration, long-running tasks, task runner, task orchestration, TODO planner, autonomous coding, background coding agent, isolated worker sessions, multi-step coding tasks.
474
+
457
475
  ## License
458
476
 
459
477
  MIT. See [LICENSE](LICENSE).
package/package.json CHANGED
@@ -1,16 +1,43 @@
1
1
  {
2
2
  "name": "pi-long-task",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "type": "module",
5
- "description": "Pi extension for breaking down and running long coding tasks safely.",
5
+ "description": "Pi coding agent extension that breaks large coding requests into tracked TODOs and runs them in isolated AI worker sessions. A long-running task runner and subagent orchestrator for Pi, with a live TUI progress sidebar, retries, goal loops, and optional per-task git commits.",
6
6
  "keywords": [
7
+ "pi",
7
8
  "pi-package",
9
+ "pi-extension",
10
+ "pi-coding-agent",
11
+ "coding-agent",
12
+ "ai-coding-agent",
13
+ "ai-coding-assistant",
14
+ "llm-agent",
15
+ "agentic-coding",
16
+ "subagent",
8
17
  "long-task",
18
+ "long-running-tasks",
9
19
  "task-runner",
10
- "coding-agent",
11
- "pi-extension"
20
+ "task-orchestration",
21
+ "todo-planner",
22
+ "autonomous-coding",
23
+ "background-agent",
24
+ "worker-sessions",
25
+ "ai-automation",
26
+ "developer-tools"
12
27
  ],
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/thestuntcoder/pi-long-task.git"
31
+ },
32
+ "homepage": "https://github.com/thestuntcoder/pi-long-task#readme",
33
+ "bugs": {
34
+ "url": "https://github.com/thestuntcoder/pi-long-task/issues"
35
+ },
36
+ "engines": {
37
+ "node": ">=22.19.0"
38
+ },
13
39
  "files": [
40
+ "CHANGELOG.md",
14
41
  "LICENSE",
15
42
  "README.md",
16
43
  "scripts",
@@ -52,13 +79,5 @@
52
79
  "typescript-eslint": "^8.61.1"
53
80
  },
54
81
  "license": "MIT",
55
- "author": "Dejan Jacimovic <dejan@stuntcoders.com>",
56
- "repository": {
57
- "type": "git",
58
- "url": "git+https://github.com/thestuntcoder/pi-long-task.git"
59
- },
60
- "bugs": {
61
- "url": "https://github.com/thestuntcoder/pi-long-task/issues"
62
- },
63
- "homepage": "https://github.com/thestuntcoder/pi-long-task#readme"
82
+ "author": "Dejan Jacimovic <dejan@stuntcoders.com>"
64
83
  }
@@ -228,14 +228,21 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
228
228
  await mkdir(runtime.runDir, { recursive: true });
229
229
  await writeFile(runtime.taskResultPath, initialTaskResultMarkdown(runtime.runId), "utf8");
230
230
  let planningComplete = false;
231
+ let latestTodoMarkdown: string | undefined;
232
+ let latestTasks: Task[] = [];
233
+ let activeTask: Task | undefined;
234
+ let activeAttempt: number | undefined;
235
+ const protectedDirtyPathsByTask = new Map<string, Set<string>>();
231
236
 
232
237
  try {
233
238
  emitProgress(runtime, "Creating TODO plan...", { phase: "planning" });
234
239
  let todoMarkdown = await generateOrNormalizeTodoMarkdown(inputText, runtime);
235
240
  validateTodoMarkdown(todoMarkdown);
236
241
  planningComplete = true;
242
+ latestTodoMarkdown = todoMarkdown;
237
243
  await writeFile(runtime.todoPath, todoMarkdown, "utf8");
238
244
  const initialTasks = parseTasks(todoMarkdown);
245
+ latestTasks = initialTasks;
239
246
  emitProgress(runtime, `Created TODO plan with ${initialTasks.length} task(s).`, {
240
247
  phase: "planned",
241
248
  totalTasks: initialTasks.length,
@@ -247,6 +254,7 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
247
254
 
248
255
  while (!runtime.abortSignal?.aborted) {
249
256
  const tasksBeforeAttempt = parseTasks(todoMarkdown);
257
+ latestTasks = tasksBeforeAttempt;
250
258
  const nextTask = tasksBeforeAttempt.find((task) => !task.done);
251
259
  if (!nextTask) {
252
260
  break;
@@ -269,9 +277,15 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
269
277
  }),
270
278
  },
271
279
  );
272
- const preExistingDirtyPaths = options.commit
273
- ? await gitDirtyPaths(runtime.cwd, runtime.taskResultPath, runtime.todoPath, runtime.runDir)
274
- : new Set<string>();
280
+ let preExistingDirtyPaths = protectedDirtyPathsByTask.get(nextTask.taskId);
281
+ if (!preExistingDirtyPaths) {
282
+ preExistingDirtyPaths = options.commit
283
+ ? await gitDirtyPaths(runtime.cwd, runtime.taskResultPath, runtime.todoPath, runtime.runDir)
284
+ : new Set<string>();
285
+ protectedDirtyPathsByTask.set(nextTask.taskId, preExistingDirtyPaths);
286
+ }
287
+ activeTask = nextTask;
288
+ activeAttempt = attempt;
275
289
  const outcome = await runtime.workerRunner({
276
290
  cwd: runtime.cwd,
277
291
  todoPath: runtime.todoPath,
@@ -294,11 +308,6 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
294
308
  outcomes.push(outcome);
295
309
  finalizeWorkerCost(runtime.workerCostState, outcome);
296
310
 
297
- if (outcome.done) {
298
- todoMarkdown = markTaskDone(todoMarkdown, nextTask.taskId);
299
- await writeFile(runtime.todoPath, todoMarkdown, "utf8");
300
- }
301
-
302
311
  const attemptDetails: TaskAttemptSummary = {
303
312
  taskId: nextTask.taskId,
304
313
  title: nextTask.title,
@@ -310,6 +319,14 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
310
319
  attempts.push(attemptDetails);
311
320
  await appendTaskResult(runtime.taskResultPath, nextTask, outcome);
312
321
 
322
+ if (outcome.done) {
323
+ todoMarkdown = markTaskDone(todoMarkdown, nextTask.taskId);
324
+ latestTodoMarkdown = todoMarkdown;
325
+ await writeFile(runtime.todoPath, todoMarkdown, "utf8");
326
+ }
327
+ activeTask = undefined;
328
+ activeAttempt = undefined;
329
+
313
330
  let taskCommitHash: string | undefined;
314
331
  let taskCommitError: string | undefined;
315
332
  let taskCommitSkipped: string | undefined;
@@ -360,12 +377,13 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
360
377
  }
361
378
  }
362
379
 
363
- if (runtime.abortSignal?.aborted && !failure) {
364
- failure = "Pi Long Task run aborted.";
365
- }
366
-
367
380
  const finalTodoMarkdown = await readFile(runtime.todoPath, "utf8");
368
381
  const finalTasks = parseTasks(finalTodoMarkdown);
382
+ latestTodoMarkdown = finalTodoMarkdown;
383
+ latestTasks = finalTasks;
384
+ if (runtime.abortSignal?.aborted && !failure && finalTasks.some((task) => !task.done)) {
385
+ failure = "Pi Long Task run aborted.";
386
+ }
369
387
  const completedTasks = finalTasks.filter((task) => task.done).length;
370
388
  const remainingTasks = remainingTaskSummaries(finalTasks, attempts);
371
389
  const blockedTasks = remainingTasks.filter((task) => task.status === "blocked").length;
@@ -427,12 +445,41 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
427
445
  ? `${message} See ${runtime.taskResultPath} for planner diagnostics.`
428
446
  : message;
429
447
  const summary = `Pi Long Task failed: ${resultError}`;
448
+ const failureNote =
449
+ planningComplete && activeTask
450
+ ? `TODO ${activeTask.taskId} — ${activeTask.title} (attempt ${activeAttempt ?? "unknown"}) failed: ${message}`
451
+ : message;
430
452
  try {
431
- await appendFailureNote(runtime.taskResultPath, message, !planningComplete ? runtime.plannerDiagnostics : []);
453
+ await appendFailureNote(runtime.taskResultPath, failureNote, !planningComplete ? runtime.plannerDiagnostics : []);
432
454
  } catch {
433
455
  // Best effort only; the original error is returned below.
434
456
  }
435
457
 
458
+ if (planningComplete && activeTask && activeAttempt !== undefined) {
459
+ attempts.push({
460
+ taskId: activeTask.taskId,
461
+ title: activeTask.title,
462
+ attempt: activeAttempt,
463
+ reportedStatus: "failed",
464
+ done: false,
465
+ error: message,
466
+ });
467
+ }
468
+ if (planningComplete) {
469
+ try {
470
+ latestTodoMarkdown = await readFile(runtime.todoPath, "utf8");
471
+ latestTasks = parseTasks(latestTodoMarkdown);
472
+ } catch {
473
+ // Retain the last valid in-memory task snapshot.
474
+ }
475
+ }
476
+ const completedTasks = latestTasks.filter((task) => task.done).length;
477
+ const remainingTasks = remainingTaskSummaries(latestTasks, attempts);
478
+ const blockedTasks = remainingTasks.filter((task) => task.status === "blocked").length;
479
+ const failedTasks = remainingTasks.filter(
480
+ (task) => task.status !== "blocked" && task.status !== "not_started",
481
+ ).length;
482
+ const taskProgress = buildCompletionTaskProgressModel(latestTasks, attempts, "failed");
436
483
  const result: CoordinatorResult = {
437
484
  status: "failed",
438
485
  summary,
@@ -442,16 +489,16 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
442
489
  todoPath: runtime.todoPath,
443
490
  resultPath: runtime.taskResultPath,
444
491
  taskResultPath: runtime.taskResultPath,
445
- totalTasks: 0,
446
- completedTasks: 0,
447
- failedTasks: 0,
448
- blockedTasks: 0,
492
+ totalTasks: latestTasks.length,
493
+ completedTasks,
494
+ failedTasks,
495
+ blockedTasks,
449
496
  attemptedTasks: attempts.length,
450
- remainingTasks: [],
497
+ remainingTasks,
451
498
  outcomes,
452
499
  commits,
453
500
  attempts,
454
- taskProgress: buildTaskProgressModel({ tasks: [], attempts }),
501
+ taskProgress,
455
502
  workerCostTotal: runtime.workerCostState.total,
456
503
  commit: options.commit,
457
504
  goal: runtime.goal,
@@ -461,7 +508,8 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
461
508
  emitProgress(runtime, "Pi Long Task failed.", {
462
509
  phase: "complete",
463
510
  status: "failed",
464
- taskProgress: buildTaskProgressModel({ tasks: [], attempts }),
511
+ totalTasks: latestTasks.length,
512
+ taskProgress,
465
513
  });
466
514
  return result;
467
515
  }
package/src/goal_loop.ts CHANGED
@@ -68,6 +68,7 @@ export interface GeneratedTodoState {
68
68
  generatorRunDir?: string;
69
69
  generatorResultPath?: string;
70
70
  generatorTaskResultPath?: string;
71
+ generatorWorkerCostTotal?: number;
71
72
  }
72
73
 
73
74
  export interface GoalWorkerResultState {
@@ -401,6 +402,35 @@ export function recordReviewerResult(
401
402
  });
402
403
  }
403
404
 
405
+ export function failGoalLoop(
406
+ state: GoalLoopState,
407
+ reason: string,
408
+ options: { now?: Date; status?: "failed" | "partial" } = {},
409
+ ): GoalLoopState {
410
+ if (isTerminalGoalLoopStatus(state.status)) {
411
+ return state;
412
+ }
413
+ const timestamp = (options.now ?? new Date()).toISOString();
414
+ const status = options.status ?? "failed";
415
+ const phase: GoalLoopPhase = status === "failed" ? "failed" : "complete";
416
+ const completion: GoalCompletionState = { status, reason, completedAt: timestamp };
417
+ const iterations = state.iterations.map((iteration) =>
418
+ iteration.iteration === state.currentIteration && !isTerminalIterationStatus(iteration.status)
419
+ ? { ...iteration, status: "failed" as const, updatedAt: timestamp, completion }
420
+ : iteration,
421
+ );
422
+ return withTrace(
423
+ { ...state, status, phase, completion, iterations, updatedAt: timestamp },
424
+ {
425
+ timestamp,
426
+ phase,
427
+ event: status === "partial" ? "timeout" : "failed",
428
+ message: reason,
429
+ iteration: state.currentIteration || undefined,
430
+ },
431
+ );
432
+ }
433
+
404
434
  export function cancelGoalLoop(
405
435
  state: GoalLoopState,
406
436
  reason = "Goal loop cancellation requested.",