pi-long-task 0.3.11 → 0.3.13
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 +19 -0
- package/README.md +14 -6
- package/package.json +9 -5
- package/src/coordinator.ts +68 -20
- package/src/goal_loop.ts +30 -0
- package/src/goal_orchestrator.ts +219 -128
- package/src/goal_review.ts +91 -73
- package/src/goal_state.ts +25 -0
- package/src/goal_todo_execution.ts +25 -8
- package/src/goal_todo_generation.ts +15 -8
- package/src/result_writer.ts +90 -24
- package/src/todo_generator.ts +74 -23
- package/src/todo_parser.ts +35 -11
- package/src/worker_session.ts +163 -44
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Notable changes to Pi Long Task are recorded here. This project follows semantic versioning.
|
|
4
|
+
|
|
5
|
+
## 0.3.13 - 2026-07-22
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Require complete, machine-readable worker results and reject false completion after session errors, timeouts, or cancellation.
|
|
10
|
+
- Bound worker and reviewer cancellation even when an SDK prompt or graceful follow-up does not settle.
|
|
11
|
+
- Preserve checked TODO progress, durable attempt ordering, retry commit baselines, and accurate failure evidence.
|
|
12
|
+
- Resume persisted goal-loop phases without repeating completed generation or execution work.
|
|
13
|
+
- Enforce overall and iteration deadlines consistently across generation, execution, and review.
|
|
14
|
+
- Retain structured terminal goal results and persisted cost totals across cancellation, failure, and resume paths.
|
|
15
|
+
|
|
16
|
+
### Compatibility and maintenance
|
|
17
|
+
|
|
18
|
+
- 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.
|
|
19
|
+
- Refresh the controllable transitive development lockfile resolutions for `brace-expansion` and `protobufjs`.
|
package/README.md
CHANGED
|
@@ -194,7 +194,7 @@ Use `with commits` or `commit true` only when you want Pi Long Task to create el
|
|
|
194
194
|
|
|
195
195
|
### 3. Monitor progress and completion
|
|
196
196
|
|
|
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.
|
|
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. 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
198
|
|
|
199
199
|
## What it looks like
|
|
200
200
|
|
|
@@ -331,16 +331,16 @@ When a `pi_goal_task` goal is already concrete, existing direct behavior is pres
|
|
|
331
331
|
|
|
332
332
|
`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
333
|
|
|
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.
|
|
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. 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
335
|
|
|
336
336
|
Safety controls:
|
|
337
337
|
|
|
338
338
|
- `minIterations` prevents early success before the requested number of loops; default is `1`.
|
|
339
339
|
- `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
340
|
- `timeoutMs` caps the overall goal loop; default is `172800000` ms (48 hours).
|
|
341
|
-
- `iterationTimeoutMs` caps each
|
|
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.
|
|
341
|
+
- `iterationTimeoutMs` caps each generation, execution, and review sequence; default is `10800000` ms (3 hours).
|
|
342
|
+
- `reviewerTimeoutMs` caps each reviewer session within the remaining overall and iteration budgets; default is `1800000` ms (30 minutes).
|
|
343
|
+
- 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
344
|
- `maxAttemptsPerTask` and `maxBashTimeoutMs` are forwarded to worker long-task runs.
|
|
345
345
|
- `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
346
|
|
|
@@ -414,12 +414,18 @@ When `commit` is `true`, it may commit eligible task changes after a task report
|
|
|
414
414
|
|
|
415
415
|
- generated run files under `tmp/pi-long-task/`
|
|
416
416
|
- generated `TASK_RESULT.md` files
|
|
417
|
-
- files that were already dirty before the task
|
|
417
|
+
- files that were already dirty before the task's first attempt (the same protected baseline is retained across retries)
|
|
418
418
|
|
|
419
419
|
This lets you keep existing local work separate from Pi Long Task changes.
|
|
420
420
|
|
|
421
421
|
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
422
|
|
|
423
|
+
## Runtime compatibility
|
|
424
|
+
|
|
425
|
+
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.
|
|
426
|
+
|
|
427
|
+
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.
|
|
428
|
+
|
|
423
429
|
## Development and validation
|
|
424
430
|
|
|
425
431
|
Run the local development checks:
|
|
@@ -429,6 +435,8 @@ cd /path/to/pi-long-task
|
|
|
429
435
|
npm run check
|
|
430
436
|
```
|
|
431
437
|
|
|
438
|
+
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.
|
|
439
|
+
|
|
432
440
|
Check that Pi can load the extension:
|
|
433
441
|
|
|
434
442
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-long-task",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pi extension for breaking down and running long coding tasks safely.",
|
|
6
6
|
"keywords": [
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"pi-extension"
|
|
12
12
|
],
|
|
13
13
|
"files": [
|
|
14
|
+
"CHANGELOG.md",
|
|
14
15
|
"LICENSE",
|
|
15
16
|
"README.md",
|
|
16
17
|
"scripts",
|
|
@@ -38,9 +39,9 @@
|
|
|
38
39
|
"typebox": "*"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
|
-
"@earendil-works/pi-ai": "^0.
|
|
42
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
43
|
-
"@earendil-works/pi-tui": "^0.
|
|
42
|
+
"@earendil-works/pi-ai": "^0.81.1",
|
|
43
|
+
"@earendil-works/pi-coding-agent": "^0.81.1",
|
|
44
|
+
"@earendil-works/pi-tui": "^0.81.1",
|
|
44
45
|
"@eslint/js": "^10.0.1",
|
|
45
46
|
"@types/node": "^25.9.3",
|
|
46
47
|
"eslint": "^10.5.0",
|
|
@@ -60,5 +61,8 @@
|
|
|
60
61
|
"bugs": {
|
|
61
62
|
"url": "https://github.com/thestuntcoder/pi-long-task/issues"
|
|
62
63
|
},
|
|
63
|
-
"homepage": "https://github.com/thestuntcoder/pi-long-task#readme"
|
|
64
|
+
"homepage": "https://github.com/thestuntcoder/pi-long-task#readme",
|
|
65
|
+
"engines": {
|
|
66
|
+
"node": ">=22.19.0"
|
|
67
|
+
}
|
|
64
68
|
}
|
package/src/coordinator.ts
CHANGED
|
@@ -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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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,
|
|
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:
|
|
446
|
-
completedTasks
|
|
447
|
-
failedTasks
|
|
448
|
-
blockedTasks
|
|
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
|
|
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
|
-
|
|
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.",
|