pi-long-task 0.5.0 → 0.7.0

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.
@@ -3,13 +3,23 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
 
5
5
  import { runCoordinator, type CoordinatorResult, type RunCoordinatorOptions } from "./coordinator.ts";
6
- import { type GoalIterationState, type GoalLoopState, recordGeneratedTodo, startGoalIteration } from "./goal_loop.ts";
6
+ import {
7
+ excludeNetworkOutageFromGoalDeadlines,
8
+ type GoalIterationState,
9
+ type GoalLoopState,
10
+ recordGeneratedTodo,
11
+ startGoalIteration,
12
+ } from "./goal_loop.ts";
7
13
  import { GoalStateStore } from "./goal_state.ts";
8
14
  import type { GoalSpecification } from "./goal_spec.ts";
15
+ import { classifyNetworkFailure } from "./network_failure.ts";
16
+ import { recoverNetworkOperation, type NetworkRecoveryEvent } from "./network_recovery.ts";
17
+ import type { NetworkRecoveryConfig } from "./network_recovery_config.ts";
9
18
  import { parseTasks } from "./todo_parser.ts";
10
19
  import {
11
20
  applyGoalInstructionsToTodoMarkdown,
12
21
  extractAndValidateTodoMarkdown,
22
+ todoPlanningOnlyPromptBlock,
13
23
  validateTodoMarkdown,
14
24
  } from "./todo_generator.ts";
15
25
 
@@ -29,10 +39,14 @@ export interface GoalTodoGenerationOptions {
29
39
  modelName?: string;
30
40
  thinkingLevel?: string;
31
41
  maxBashTimeoutMs?: number;
42
+ todoTimeoutMs?: number;
43
+ todoGracefulShutdownMs?: number;
44
+ networkRecovery?: Readonly<NetworkRecoveryConfig>;
32
45
  now?: () => Date;
33
46
  additionalContext?: string;
34
47
  outputPath?: string;
35
48
  goalSpecification?: GoalSpecification;
49
+ onNetworkRecovery?: (event: NetworkRecoveryEvent) => void;
36
50
  }
37
51
 
38
52
  export interface GoalTodoGenerationResult {
@@ -92,7 +106,14 @@ export async function runGoalTodoGenerationLongTask(
92
106
  );
93
107
  }
94
108
 
95
- const childResult = await (options.longTaskRunner ?? runCoordinator)({
109
+ let excludedOutageMs = 0;
110
+ const captureOutage = (event: NetworkRecoveryEvent) => {
111
+ if (event.type === "cleanup") {
112
+ excludedOutageMs += event.state.elapsedMs;
113
+ }
114
+ options.onNetworkRecovery?.(event);
115
+ };
116
+ const childOptions: RunCoordinatorOptions = {
96
117
  inputText: payload,
97
118
  commit: false,
98
119
  goal: state.goal,
@@ -104,7 +125,38 @@ export async function runGoalTodoGenerationLongTask(
104
125
  taskThinking: options.thinkingLevel,
105
126
  taskTimeoutMs: childTimeoutMs,
106
127
  maxBashTimeoutMs: options.maxBashTimeoutMs,
107
- });
128
+ todoTimeoutMs: options.todoTimeoutMs,
129
+ todoGracefulShutdownMs: options.todoGracefulShutdownMs,
130
+ networkRecovery: options.networkRecovery,
131
+ onNetworkRecovery: captureOutage,
132
+ };
133
+
134
+ let childResult: CoordinatorResult | undefined;
135
+ let childFailure: unknown;
136
+ try {
137
+ childResult = await runGoalPlannerWithNetworkRecovery(
138
+ options.longTaskRunner ?? runCoordinator,
139
+ childOptions,
140
+ options.networkRecovery,
141
+ options.abortSignal,
142
+ now,
143
+ captureOutage,
144
+ );
145
+ } catch (error) {
146
+ childFailure = error;
147
+ }
148
+
149
+ if (excludedOutageMs > 0) {
150
+ state = excludeNetworkOutageFromGoalDeadlines(state, excludedOutageMs, "planner", { now: now() });
151
+ await persistStateChange(store, previousTraceLength, state);
152
+ previousTraceLength = state.trace.length;
153
+ }
154
+ if (childFailure !== undefined) {
155
+ throw childFailure;
156
+ }
157
+ if (!childResult) {
158
+ throw new GoalTodoGenerationError("TODO-generation planner ended without a coordinator result.");
159
+ }
108
160
 
109
161
  throwIfAborted(options.abortSignal);
110
162
  const rawOutput = await readGeneratedTodo(rawTodoPath, childResult);
@@ -178,16 +230,17 @@ export function buildGoalTodoGenerationTaskPayload(options: {
178
230
 
179
231
  - Long task goal: ${oneLine(options.state.goal)}
180
232
  - This is goal-loop TODO generation iteration ${options.iteration} for goal run ${options.state.goalRunId}.
181
- - Only generate TODO markdown for future workers; do not implement, edit, test, refactor, or otherwise perform the goal work in this generation run.
182
233
  - Write the generated Pi Long Task-compatible TODO markdown to \`${options.outputPath}\`.
183
234
  - Do not wrap the generated file in a code fence and do not include commentary outside the TODO markdown in that file.
184
235
  - Keep generated tasks focused, independently assignable, and safe for separate worker sessions.
185
236
  ${iterationPolicy}
186
237
  ${
187
238
  options.goalSpecification
188
- ? "- A persisted goal specification is available; derive implementation TODOs from that specification rather than only the original vague goal.\n- Ensure generated tasks explicitly cover relevant requirement IDs, milestones, acceptance criteria, verification gates, constraints, and definition-of-done items from the specification.\n- Include spec IDs (for example REQ-*, MS-*, AC-*, VG-*) in generated task goals/status/verification/done-when guidance wherever applicable.\n"
239
+ ? "- A persisted goal specification is available; derive implementation TODOs from it rather than only the original vague goal.\n- Preserve its requirement IDs, milestones, acceptance criteria, verification gates, constraints, and definition of done in the relevant tasks. Reference each applicable spec ID (for example REQ-*, MS-*, AC-*, VG-*) where it is most useful instead of duplicating it in every task field.\n"
189
240
  : ""
190
241
  }
242
+ ${todoPlanningOnlyPromptBlock()}
243
+
191
244
  ## Progress
192
245
 
193
246
  - [ ] TODO 1 — Generate Pi Long Task TODO markdown
@@ -199,16 +252,12 @@ ${
199
252
  **Goal:** Convert the high-level goal into a valid Pi Long Task TODO plan for the next implementation long task.
200
253
 
201
254
  **Status:**
202
- - [ ] Analyze the high-level goal and any iteration context.
203
- - [ ] Create TODO markdown that starts with \`# Pi Long Task TODO\`.
204
- - [ ] Include a \`## Progress\` section with one unchecked \`- [ ] TODO N Title\` line per generated task.
205
- - [ ] Include a \`---\` separator before generated task sections.
206
- - [ ] Include sequential \`## TODO N — Title\` sections with \`**Goal:**\`, \`**Status:**\`, \`**Verify:**\`, and \`**Done when:**\` guidance.
207
- - [ ] Write only the generated TODO markdown to \`${options.outputPath}\`.
255
+ - [ ] Derive focused future-worker tasks from the goal, constraints, and iteration context without performing that work.
256
+ - [ ] Render the required Progress list and sequential task sections with Goal, Status, Verify, and Done when fields.
257
+ - [ ] Write only the concise generated TODO markdown to \`${options.outputPath}\`.
208
258
 
209
259
  **Verify:**
210
- - Confirm the file at \`${options.outputPath}\` exists.
211
- - Confirm it starts with \`# Pi Long Task TODO\`, has a \`## Progress\` section, a \`---\` separator, sequential TODO sections, unchecked status checkboxes, and concrete verification instructions.
260
+ - Confirm \`${options.outputPath}\` contains valid TODO markdown with preserved constraints, compact task sections, unchecked statuses, and concrete checks.
212
261
 
213
262
  **Done when:**
214
263
  - \`${options.outputPath}\` contains valid Pi Long Task-compatible TODO markdown for achieving the high-level goal.
@@ -259,6 +308,45 @@ async function persistStateChange(
259
308
  await store.appendNewTraceEvents(previousTraceLength, state);
260
309
  }
261
310
 
311
+ /**
312
+ * Resume the TODO-generation child under its stable run ID. This child is
313
+ * constrained to one replaceable generated-plan artifact (never implementation
314
+ * work), while the default coordinator durably resumes its TODO evidence and
315
+ * rotates provider-failed sessions. Network probes therefore neither create a
316
+ * goal iteration nor consume a normal child task attempt.
317
+ */
318
+ async function runGoalPlannerWithNetworkRecovery(
319
+ runner: GoalTodoGenerationLongTaskRunner,
320
+ childOptions: RunCoordinatorOptions,
321
+ networkRecovery: Readonly<NetworkRecoveryConfig> | undefined,
322
+ abortSignal: AbortSignal | undefined,
323
+ now: () => Date,
324
+ onRecoveryEvent: (event: NetworkRecoveryEvent) => void,
325
+ ): Promise<CoordinatorResult> {
326
+ const run = (recoverySignal?: AbortSignal) =>
327
+ runner({
328
+ ...childOptions,
329
+ abortSignal: combineAbortSignals(abortSignal, recoverySignal),
330
+ });
331
+
332
+ try {
333
+ return await run();
334
+ } catch (initialFailure) {
335
+ if (!networkRecovery?.enabled || !classifyNetworkFailure(initialFailure).recoverable) {
336
+ throw initialFailure;
337
+ }
338
+ const recovered = await recoverNetworkOperation({
339
+ initialFailure,
340
+ config: networkRecovery,
341
+ signal: abortSignal,
342
+ now: () => now().getTime(),
343
+ onEvent: onRecoveryEvent,
344
+ retry: ({ signal }) => run(signal),
345
+ });
346
+ return recovered.value;
347
+ }
348
+ }
349
+
262
350
  async function readGeneratedTodo(rawTodoPath: string, childResult: CoordinatorResult): Promise<string> {
263
351
  try {
264
352
  return await readFile(rawTodoPath, "utf8");
@@ -538,6 +626,13 @@ function sha256(value: string): string {
538
626
  return createHash("sha256").update(value).digest("hex");
539
627
  }
540
628
 
629
+ function combineAbortSignals(...signals: Array<AbortSignal | undefined>): AbortSignal | undefined {
630
+ const available = signals.filter((signal): signal is AbortSignal => Boolean(signal));
631
+ if (available.length === 0) return undefined;
632
+ if (available.length === 1) return available[0];
633
+ return AbortSignal.any(available);
634
+ }
635
+
541
636
  function throwIfAborted(signal: AbortSignal | undefined): void {
542
637
  if (signal?.aborted) {
543
638
  throw new GoalTodoGenerationError("TODO generation was aborted before producing a generated TODO.");
package/src/index.ts CHANGED
@@ -80,6 +80,8 @@ function toolDetails(result: CoordinatorResult) {
80
80
  remainingTasks: result.remainingTasks,
81
81
  taskProgress: result.taskProgress,
82
82
  workerCostTotal: result.workerCostTotal,
83
+ plannerBudget: result.plannerBudget,
84
+ capabilityWarnings: result.capabilityWarnings,
83
85
  summary: result.summary,
84
86
  goal: result.goal,
85
87
  error: result.error,
@@ -349,7 +351,15 @@ function renderSidebarRows(update: CoordinatorProgressUpdate | undefined, theme:
349
351
  rows.push(renderSidebarStateLine(update, theme));
350
352
 
351
353
  if (!progress || progress.tasks.length === 0) {
352
- rows.push("", sidebarHeading("Context", theme), theme.fg("muted", "Waiting for TODO plan"));
354
+ const planningStatus =
355
+ update.phase === "planning" ? normalizeActiveStatus(update.activeStatus ?? update.message) : undefined;
356
+ rows.push(
357
+ "",
358
+ sidebarHeading(planningStatus ? "Planning timing" : "Context", theme),
359
+ ...(planningStatus
360
+ ? wrapPlainText(planningStatus, width, 8).map((line) => theme.fg("accent", line))
361
+ : [theme.fg("muted", "Waiting for TODO plan")]),
362
+ );
353
363
  if (update.workerCostTotal > 0) {
354
364
  rows.push(theme.fg("muted", `${formatCost(update.workerCostTotal)} spent`));
355
365
  }
@@ -566,6 +576,8 @@ function sidebarUpdateStateDetails(update: CoordinatorProgressUpdate): {
566
576
  }
567
577
 
568
578
  switch (update.phase) {
579
+ case "capability_warning":
580
+ return { icon: "!", label: "Capability warning", color: "warning" };
569
581
  case "planning":
570
582
  return { icon: "+", label: "Planning", color: "warning" };
571
583
  case "planned":
@@ -576,6 +588,8 @@ function sidebarUpdateStateDetails(update: CoordinatorProgressUpdate): {
576
588
  return { icon: "↻", label: "Worker session", color: "accent" };
577
589
  case "worker_tool":
578
590
  return { icon: "+", label: "Worker tool", color: "warning" };
591
+ case "network_wait":
592
+ return { icon: "↻", label: "Waiting for connection", color: "warning" };
579
593
  case "task_done":
580
594
  return { icon: "✓", label: "Task complete", color: "success" };
581
595
  case "task_blocked":