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.
- package/CHANGELOG.md +40 -0
- package/README.md +153 -3
- package/package.json +1 -1
- package/src/coordinator.ts +861 -62
- package/src/goal_discovery.ts +2 -0
- package/src/goal_loop.ts +76 -0
- package/src/goal_orchestrator.ts +98 -1
- package/src/goal_review.ts +206 -15
- package/src/goal_todo_execution.ts +7 -0
- package/src/goal_todo_generation.ts +108 -13
- package/src/index.ts +15 -1
- package/src/network_failure.ts +574 -0
- package/src/network_recovery.ts +395 -0
- package/src/network_recovery_config.ts +89 -0
- package/src/planner_config.ts +214 -0
- package/src/planner_progress.ts +156 -0
- package/src/render.ts +38 -0
- package/src/session_guard.ts +120 -7
- package/src/todo_generator.ts +84 -7
- package/src/types.ts +68 -0
- package/src/worker_capabilities.ts +103 -0
- package/src/worker_config.ts +148 -7
- package/src/worker_session.ts +33 -1
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
- [ ]
|
|
203
|
-
- [ ]
|
|
204
|
-
- [ ]
|
|
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
|
|
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
|
-
|
|
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":
|