pi-long-task 0.3.9 → 0.3.10
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 +219 -5
- package/package.json +1 -1
- package/src/coordinator.ts +33 -7
- package/src/coverage_goal.ts +90 -0
- package/src/goal_discovery.ts +739 -0
- package/src/goal_loop.ts +567 -0
- package/src/goal_orchestrator.ts +396 -0
- package/src/goal_review.ts +575 -0
- package/src/goal_spec.ts +670 -0
- package/src/goal_state.ts +227 -0
- package/src/goal_todo_execution.ts +309 -0
- package/src/goal_todo_generation.ts +539 -0
- package/src/index.ts +89 -2
- package/src/input_router.ts +124 -6
- package/src/render.ts +223 -4
- package/src/todo_generator.ts +135 -7
- package/src/types.ts +66 -3
- package/src/worker_session.ts +23 -2
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { runCoordinator, type CoordinatorResult, type RunCoordinatorOptions } from "./coordinator.ts";
|
|
6
|
+
import { type GoalIterationState, type GoalLoopState, recordGeneratedTodo, startGoalIteration } from "./goal_loop.ts";
|
|
7
|
+
import { GoalStateStore } from "./goal_state.ts";
|
|
8
|
+
import type { GoalSpecification } from "./goal_spec.ts";
|
|
9
|
+
import { parseTasks } from "./todo_parser.ts";
|
|
10
|
+
import {
|
|
11
|
+
applyGoalInstructionsToTodoMarkdown,
|
|
12
|
+
extractAndValidateTodoMarkdown,
|
|
13
|
+
validateTodoMarkdown,
|
|
14
|
+
} from "./todo_generator.ts";
|
|
15
|
+
|
|
16
|
+
export const GOAL_TODO_GENERATION_PAYLOAD_FILE = "TODO_GENERATION_TASK.md";
|
|
17
|
+
export const GOAL_TODO_GENERATION_RAW_FILE = "GENERATED_TODO_RAW.md";
|
|
18
|
+
export const GOAL_TODO_GENERATION_TODO_FILE = "TODO.md";
|
|
19
|
+
|
|
20
|
+
export type GoalTodoGenerationLongTaskRunner = (options: RunCoordinatorOptions) => Promise<CoordinatorResult>;
|
|
21
|
+
|
|
22
|
+
export interface GoalTodoGenerationOptions {
|
|
23
|
+
state: GoalLoopState;
|
|
24
|
+
cwd?: string;
|
|
25
|
+
store?: GoalStateStore;
|
|
26
|
+
longTaskRunner?: GoalTodoGenerationLongTaskRunner;
|
|
27
|
+
abortSignal?: AbortSignal;
|
|
28
|
+
model?: unknown;
|
|
29
|
+
modelName?: string;
|
|
30
|
+
thinkingLevel?: string;
|
|
31
|
+
maxBashTimeoutMs?: number;
|
|
32
|
+
now?: () => Date;
|
|
33
|
+
additionalContext?: string;
|
|
34
|
+
outputPath?: string;
|
|
35
|
+
goalSpecification?: GoalSpecification;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface GoalTodoGenerationResult {
|
|
39
|
+
state: GoalLoopState;
|
|
40
|
+
iteration: GoalIterationState;
|
|
41
|
+
todoMarkdown: string;
|
|
42
|
+
todoPath: string;
|
|
43
|
+
rawTodoPath: string;
|
|
44
|
+
payloadPath: string;
|
|
45
|
+
contentHash: string;
|
|
46
|
+
taskCount: number;
|
|
47
|
+
childResult: CoordinatorResult;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class GoalTodoGenerationError extends Error {
|
|
51
|
+
constructor(message: string) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = "GoalTodoGenerationError";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function runGoalTodoGenerationLongTask(
|
|
58
|
+
options: GoalTodoGenerationOptions,
|
|
59
|
+
): Promise<GoalTodoGenerationResult> {
|
|
60
|
+
const now = options.now ?? (() => new Date());
|
|
61
|
+
let state = options.state;
|
|
62
|
+
let previousTraceLength = state.trace.length;
|
|
63
|
+
const store =
|
|
64
|
+
options.store ?? new GoalStateStore({ cwd: options.cwd, goalRunId: state.goalRunId, goalRunDir: state.goalRunDir });
|
|
65
|
+
|
|
66
|
+
throwIfAborted(options.abortSignal);
|
|
67
|
+
state = ensurePendingGenerationIteration(state, { now, abortSignal: options.abortSignal });
|
|
68
|
+
await persistStateChange(store, previousTraceLength, state);
|
|
69
|
+
previousTraceLength = state.trace.length;
|
|
70
|
+
|
|
71
|
+
const iteration = currentPendingIteration(state);
|
|
72
|
+
const iterationDir = store.iterationDir(iteration.iteration);
|
|
73
|
+
await mkdir(iterationDir, { recursive: true });
|
|
74
|
+
|
|
75
|
+
const rawTodoPath = options.outputPath ?? path.join(iterationDir, GOAL_TODO_GENERATION_RAW_FILE);
|
|
76
|
+
const todoPath = path.join(iterationDir, GOAL_TODO_GENERATION_TODO_FILE);
|
|
77
|
+
const goalSpecification = options.goalSpecification ?? (await store.tryLoadGoalSpecification());
|
|
78
|
+
const payload = buildGoalTodoGenerationTaskPayload({
|
|
79
|
+
state,
|
|
80
|
+
iteration: iteration.iteration,
|
|
81
|
+
outputPath: rawTodoPath,
|
|
82
|
+
additionalContext: options.additionalContext ?? buildPreviousIterationContext(state),
|
|
83
|
+
goalSpecification,
|
|
84
|
+
goalSpecificationPath: goalSpecification ? store.paths.goalSpecPath : undefined,
|
|
85
|
+
});
|
|
86
|
+
const payloadPath = path.join(iterationDir, GOAL_TODO_GENERATION_PAYLOAD_FILE);
|
|
87
|
+
await writeFile(payloadPath, payload, "utf8");
|
|
88
|
+
|
|
89
|
+
const childResult = await (options.longTaskRunner ?? runCoordinator)({
|
|
90
|
+
inputText: payload,
|
|
91
|
+
commit: false,
|
|
92
|
+
goal: state.goal,
|
|
93
|
+
cwd: options.cwd,
|
|
94
|
+
runId: `${state.goalRunId}-todo-generation-${String(iteration.iteration).padStart(2, "0")}`,
|
|
95
|
+
abortSignal: options.abortSignal,
|
|
96
|
+
workerModel: options.model,
|
|
97
|
+
workerModelName: options.modelName,
|
|
98
|
+
taskThinking: options.thinkingLevel,
|
|
99
|
+
taskTimeoutMs: timeoutForIteration(iteration, state, now()),
|
|
100
|
+
maxBashTimeoutMs: options.maxBashTimeoutMs,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
throwIfAborted(options.abortSignal);
|
|
104
|
+
const rawOutput = await readGeneratedTodo(rawTodoPath, childResult);
|
|
105
|
+
const todoMarkdown = normalizeGeneratedTodoMarkdown(
|
|
106
|
+
rawOutput,
|
|
107
|
+
state.goal,
|
|
108
|
+
goalSpecification,
|
|
109
|
+
store.paths.goalSpecPath,
|
|
110
|
+
);
|
|
111
|
+
const taskCount = parseTasks(todoMarkdown).length;
|
|
112
|
+
const contentHash = sha256(todoMarkdown);
|
|
113
|
+
await writeFile(todoPath, todoMarkdown, "utf8");
|
|
114
|
+
|
|
115
|
+
state = recordGeneratedTodo(
|
|
116
|
+
state,
|
|
117
|
+
iteration.iteration,
|
|
118
|
+
{
|
|
119
|
+
todoPath,
|
|
120
|
+
summary: `Generated TODO with ${taskCount} task(s).`,
|
|
121
|
+
contentHash,
|
|
122
|
+
payloadPath,
|
|
123
|
+
rawTodoPath,
|
|
124
|
+
generatorRunId: childResult.runId,
|
|
125
|
+
generatorRunDir: childResult.runDir,
|
|
126
|
+
generatorResultPath: childResult.resultPath,
|
|
127
|
+
generatorTaskResultPath: childResult.taskResultPath,
|
|
128
|
+
},
|
|
129
|
+
{ now: now() },
|
|
130
|
+
);
|
|
131
|
+
await persistStateChange(store, previousTraceLength, state);
|
|
132
|
+
const updatedIteration = currentIteration(state, iteration.iteration);
|
|
133
|
+
await store.writeIterationSnapshot(updatedIteration);
|
|
134
|
+
await store.appendIterationResult(updatedIteration);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
state,
|
|
138
|
+
iteration: updatedIteration,
|
|
139
|
+
todoMarkdown,
|
|
140
|
+
todoPath,
|
|
141
|
+
rawTodoPath,
|
|
142
|
+
payloadPath,
|
|
143
|
+
contentHash,
|
|
144
|
+
taskCount,
|
|
145
|
+
childResult,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function buildGoalTodoGenerationTaskPayload(options: {
|
|
150
|
+
state: Pick<GoalLoopState, "goal" | "goalRunId">;
|
|
151
|
+
iteration: number;
|
|
152
|
+
outputPath: string;
|
|
153
|
+
additionalContext?: string;
|
|
154
|
+
goalSpecification?: GoalSpecification;
|
|
155
|
+
goalSpecificationPath?: string;
|
|
156
|
+
}): string {
|
|
157
|
+
const context = options.additionalContext?.trim();
|
|
158
|
+
const contextBlock = context ? `\nAdditional iteration context:\n\n${markdownFence(context, "text")}\n` : "";
|
|
159
|
+
const specificationBlock = options.goalSpecification
|
|
160
|
+
? `\nPersisted goal specification (source of truth for implementation TODOs):\n\n${markdownFence(
|
|
161
|
+
buildGoalSpecificationGenerationContext(options.goalSpecification, options.goalSpecificationPath),
|
|
162
|
+
"markdown",
|
|
163
|
+
)}\n`
|
|
164
|
+
: "";
|
|
165
|
+
|
|
166
|
+
return `# Pi Long Task TODO
|
|
167
|
+
|
|
168
|
+
## Global instructions and constraints
|
|
169
|
+
|
|
170
|
+
- Long task goal: ${oneLine(options.state.goal)}
|
|
171
|
+
- This is goal-loop TODO generation iteration ${options.iteration} for goal run ${options.state.goalRunId}.
|
|
172
|
+
- Only generate TODO markdown for future workers; do not implement, edit, test, refactor, or otherwise perform the goal work in this generation run.
|
|
173
|
+
- Write the generated Pi Long Task-compatible TODO markdown to \`${options.outputPath}\`.
|
|
174
|
+
- Do not wrap the generated file in a code fence and do not include commentary outside the TODO markdown in that file.
|
|
175
|
+
- Keep generated tasks focused, independently assignable, and safe for separate worker sessions.
|
|
176
|
+
${
|
|
177
|
+
options.goalSpecification
|
|
178
|
+
? "- 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"
|
|
179
|
+
: ""
|
|
180
|
+
}
|
|
181
|
+
## Progress
|
|
182
|
+
|
|
183
|
+
- [ ] TODO 1 — Generate Pi Long Task TODO markdown
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## TODO 1 — Generate Pi Long Task TODO markdown
|
|
188
|
+
|
|
189
|
+
**Goal:** Convert the high-level goal into a valid Pi Long Task TODO plan for the next implementation long task.
|
|
190
|
+
|
|
191
|
+
**Status:**
|
|
192
|
+
- [ ] Analyze the high-level goal and any iteration context.
|
|
193
|
+
- [ ] Create TODO markdown that starts with \`# Pi Long Task TODO\`.
|
|
194
|
+
- [ ] Include a \`## Progress\` section with one unchecked \`- [ ] TODO N — Title\` line per generated task.
|
|
195
|
+
- [ ] Include a \`---\` separator before generated task sections.
|
|
196
|
+
- [ ] Include sequential \`## TODO N — Title\` sections with \`**Goal:**\`, \`**Status:**\`, \`**Verify:**\`, and \`**Done when:**\` guidance.
|
|
197
|
+
- [ ] Write only the generated TODO markdown to \`${options.outputPath}\`.
|
|
198
|
+
|
|
199
|
+
**Verify:**
|
|
200
|
+
- Confirm the file at \`${options.outputPath}\` exists.
|
|
201
|
+
- Confirm it starts with \`# Pi Long Task TODO\`, has a \`## Progress\` section, a \`---\` separator, sequential TODO sections, unchecked status checkboxes, and concrete verification instructions.
|
|
202
|
+
|
|
203
|
+
**Done when:**
|
|
204
|
+
- \`${options.outputPath}\` contains valid Pi Long Task-compatible TODO markdown for achieving the high-level goal.
|
|
205
|
+
|
|
206
|
+
High-level goal:
|
|
207
|
+
|
|
208
|
+
${markdownFence(options.state.goal, "text")}
|
|
209
|
+
${specificationBlock}${contextBlock}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function ensurePendingGenerationIteration(
|
|
213
|
+
state: GoalLoopState,
|
|
214
|
+
options: { now: () => Date; abortSignal?: AbortSignal },
|
|
215
|
+
): GoalLoopState {
|
|
216
|
+
if (state.currentIteration > 0) {
|
|
217
|
+
const current = currentIteration(state, state.currentIteration);
|
|
218
|
+
if (current.status === "pending") {
|
|
219
|
+
return state;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return startGoalIteration(state, { now: options.now(), abortSignal: options.abortSignal });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function currentPendingIteration(state: GoalLoopState): GoalIterationState {
|
|
226
|
+
const iteration = currentIteration(state, state.currentIteration);
|
|
227
|
+
if (iteration.status !== "pending") {
|
|
228
|
+
throw new GoalTodoGenerationError(
|
|
229
|
+
`Goal iteration ${iteration.iteration} is ${iteration.status}; expected pending TODO generation.`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
return iteration;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function currentIteration(state: GoalLoopState, iterationNumber: number): GoalIterationState {
|
|
236
|
+
const iteration = state.iterations.find((item) => item.iteration === iterationNumber);
|
|
237
|
+
if (!iteration) {
|
|
238
|
+
throw new GoalTodoGenerationError(`Goal iteration ${iterationNumber || "<none>"} does not exist.`);
|
|
239
|
+
}
|
|
240
|
+
return iteration;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function persistStateChange(
|
|
244
|
+
store: GoalStateStore,
|
|
245
|
+
previousTraceLength: number,
|
|
246
|
+
state: GoalLoopState,
|
|
247
|
+
): Promise<void> {
|
|
248
|
+
await store.saveState(state);
|
|
249
|
+
await store.appendNewTraceEvents(previousTraceLength, state);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function readGeneratedTodo(rawTodoPath: string, childResult: CoordinatorResult): Promise<string> {
|
|
253
|
+
try {
|
|
254
|
+
return await readFile(rawTodoPath, "utf8");
|
|
255
|
+
} catch (error) {
|
|
256
|
+
throw new GoalTodoGenerationError(
|
|
257
|
+
`TODO-generation long task did not write ${rawTodoPath} (child status: ${childResult.status}): ${errorMessage(error)}`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function normalizeGeneratedTodoMarkdown(
|
|
263
|
+
rawOutput: string,
|
|
264
|
+
goal: string,
|
|
265
|
+
goalSpecification: GoalSpecification | undefined,
|
|
266
|
+
goalSpecificationPath: string,
|
|
267
|
+
): string {
|
|
268
|
+
const extracted = extractAndValidateTodoMarkdown(rawOutput);
|
|
269
|
+
const withGoalInstructions = applyGoalInstructionsToTodoMarkdown(extracted, goal);
|
|
270
|
+
if (!goalSpecification) {
|
|
271
|
+
return withGoalInstructions;
|
|
272
|
+
}
|
|
273
|
+
return applyGoalSpecificationInstructionsToTodoMarkdown(
|
|
274
|
+
withGoalInstructions,
|
|
275
|
+
goalSpecification,
|
|
276
|
+
goalSpecificationPath,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function buildGoalSpecificationGenerationContext(spec: GoalSpecification, goalSpecificationPath?: string): string {
|
|
281
|
+
const lines = [
|
|
282
|
+
`Goal spec path: ${goalSpecificationPath ?? "<not provided>"}`,
|
|
283
|
+
`Goal run: ${spec.goalRunId}`,
|
|
284
|
+
`Original user goal: ${oneLine(spec.originalGoal)}`,
|
|
285
|
+
`Specification summary: ${oneLine(spec.summary)}`,
|
|
286
|
+
"",
|
|
287
|
+
"Implementation planning instructions:",
|
|
288
|
+
"- Treat this persisted specification as the implementation source of truth; use the original vague goal only for traceability.",
|
|
289
|
+
"- Generate tasks that map to milestones, in-scope requirements, acceptance criteria, verification gates, and definition-of-done items.",
|
|
290
|
+
"- Cite relevant IDs (REQ-*, MS-*, AC-*, VG-*) in task goals, status checklist items, verification, and done-when guidance.",
|
|
291
|
+
"- Do not create tasks for out-of-scope requirements unless needed to preserve or document non-goals.",
|
|
292
|
+
"",
|
|
293
|
+
"In-scope requirements:",
|
|
294
|
+
...formatRequirementLines(spec.scopedRequirements.inScope),
|
|
295
|
+
"",
|
|
296
|
+
"Out-of-scope requirements / non-goals:",
|
|
297
|
+
...formatRequirementLines(spec.scopedRequirements.outOfScope),
|
|
298
|
+
"",
|
|
299
|
+
"Milestones:",
|
|
300
|
+
...formatMilestoneLines(spec.milestones),
|
|
301
|
+
"",
|
|
302
|
+
"Acceptance criteria:",
|
|
303
|
+
...formatAcceptanceCriterionLines(spec.acceptanceCriteria),
|
|
304
|
+
"",
|
|
305
|
+
"Verification gates:",
|
|
306
|
+
...formatVerificationGateLines(spec.verificationGates),
|
|
307
|
+
"",
|
|
308
|
+
"Definition of done:",
|
|
309
|
+
`- Summary: ${oneLine(spec.definitionOfDone.summary)}`,
|
|
310
|
+
`- Requirement IDs: ${formatIdList(spec.definitionOfDone.requirementIds)}`,
|
|
311
|
+
`- Acceptance criterion IDs: ${formatIdList(spec.definitionOfDone.acceptanceCriterionIds)}`,
|
|
312
|
+
`- Verification gate IDs: ${formatIdList(spec.definitionOfDone.verificationGateIds)}`,
|
|
313
|
+
...spec.definitionOfDone.requiredArtifacts.map((artifact) => `- Required artifact: ${oneLine(artifact)}`),
|
|
314
|
+
...spec.definitionOfDone.notes.map((note) => `- Note: ${oneLine(note)}`),
|
|
315
|
+
"",
|
|
316
|
+
"Design and product constraints:",
|
|
317
|
+
...formatConstraintContext(spec),
|
|
318
|
+
];
|
|
319
|
+
return lines.join("\n");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function applyGoalSpecificationInstructionsToTodoMarkdown(
|
|
323
|
+
markdown: string,
|
|
324
|
+
spec: GoalSpecification,
|
|
325
|
+
goalSpecificationPath: string,
|
|
326
|
+
): string {
|
|
327
|
+
const additions = buildGoalSpecificationTodoInstructions(spec, goalSpecificationPath);
|
|
328
|
+
const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
|
|
329
|
+
const progressIndex = lines.findIndex((line) => /^##\s+Progress\s*$/i.test(line.trim()));
|
|
330
|
+
if (progressIndex < 0) {
|
|
331
|
+
return markdown;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const existingGlobalText = lines.slice(0, progressIndex).join("\n");
|
|
335
|
+
const missing = additions.filter((line) => !existingGlobalText.includes(line));
|
|
336
|
+
if (missing.length === 0) {
|
|
337
|
+
return markdown;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const before = trimTrailingBlankLines(lines.slice(0, progressIndex));
|
|
341
|
+
const after = trimLeadingBlankLines(lines.slice(progressIndex));
|
|
342
|
+
const hasGlobalHeading = /^Global instructions:\s*$/im.test(existingGlobalText);
|
|
343
|
+
const block = hasGlobalHeading ? missing : ["Global instructions:", ...missing];
|
|
344
|
+
const next = ensureTrailingNewline([...before, "", ...block, "", ...after].join("\n"));
|
|
345
|
+
validateTodoMarkdown(next);
|
|
346
|
+
return next;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function buildGoalSpecificationTodoInstructions(spec: GoalSpecification, goalSpecificationPath: string): string[] {
|
|
350
|
+
const lines = [
|
|
351
|
+
`- Persisted goal specification: ${goalSpecificationPath}`,
|
|
352
|
+
`- Goal specification summary: ${oneLine(spec.summary)}`,
|
|
353
|
+
`- Definition of done: ${oneLine(spec.definitionOfDone.summary)}`,
|
|
354
|
+
`- Implementation TODOs must trace to requirements: ${formatIdList(spec.definitionOfDone.requirementIds)}`,
|
|
355
|
+
`- Implementation TODOs must satisfy acceptance criteria: ${formatIdList(
|
|
356
|
+
spec.definitionOfDone.acceptanceCriterionIds,
|
|
357
|
+
)}`,
|
|
358
|
+
`- Required verification gates: ${formatIdList(requiredVerificationGateIds(spec))}`,
|
|
359
|
+
];
|
|
360
|
+
const milestoneIds = spec.milestones.map((milestone) => milestone.id);
|
|
361
|
+
if (milestoneIds.length > 0) {
|
|
362
|
+
lines.push(`- Implementation TODOs should be sequenced by milestones: ${formatIdList(milestoneIds)}`);
|
|
363
|
+
}
|
|
364
|
+
return lines;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function formatRequirementLines(requirements: GoalSpecification["scopedRequirements"]["inScope"]): string[] {
|
|
368
|
+
if (requirements.length === 0) {
|
|
369
|
+
return ["- None specified."];
|
|
370
|
+
}
|
|
371
|
+
return requirements.map(
|
|
372
|
+
(requirement) =>
|
|
373
|
+
`- ${requirement.id} (${requirement.priority}) ${oneLine(requirement.title)} — ${oneLine(
|
|
374
|
+
requirement.description,
|
|
375
|
+
)}; milestones: ${formatIdList(requirement.milestoneIds)}; acceptance: ${formatIdList(
|
|
376
|
+
requirement.acceptanceCriterionIds,
|
|
377
|
+
)}`,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function formatMilestoneLines(milestones: GoalSpecification["milestones"]): string[] {
|
|
382
|
+
if (milestones.length === 0) {
|
|
383
|
+
return ["- None specified."];
|
|
384
|
+
}
|
|
385
|
+
return milestones.map(
|
|
386
|
+
(milestone) =>
|
|
387
|
+
`- ${milestone.id} ${oneLine(milestone.title)} — ${oneLine(milestone.description)}; requirements: ${formatIdList(
|
|
388
|
+
milestone.requirementIds,
|
|
389
|
+
)}; acceptance: ${formatIdList(milestone.acceptanceCriterionIds)}; done when: ${formatIdList(
|
|
390
|
+
milestone.doneWhen.map(oneLine),
|
|
391
|
+
)}`,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function formatAcceptanceCriterionLines(criteria: GoalSpecification["acceptanceCriteria"]): string[] {
|
|
396
|
+
if (criteria.length === 0) {
|
|
397
|
+
return ["- None specified."];
|
|
398
|
+
}
|
|
399
|
+
return criteria.map(
|
|
400
|
+
(criterion) =>
|
|
401
|
+
`- ${criterion.id} ${oneLine(criterion.description)}; requirements: ${formatIdList(
|
|
402
|
+
criterion.requirementIds,
|
|
403
|
+
)}; verification gates: ${formatIdList(criterion.verificationGateIds)}`,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function formatVerificationGateLines(gates: GoalSpecification["verificationGates"]): string[] {
|
|
408
|
+
if (gates.length === 0) {
|
|
409
|
+
return ["- None specified."];
|
|
410
|
+
}
|
|
411
|
+
return gates.map((gate) => {
|
|
412
|
+
const command = gate.command ? `; command: ${oneLine(gate.command)}` : "";
|
|
413
|
+
return `- ${gate.id} ${gate.required ? "required" : "optional"} ${oneLine(gate.title)} — ${oneLine(
|
|
414
|
+
gate.description,
|
|
415
|
+
)}${command}; success: ${formatIdList(gate.successCriteria.map(oneLine))}`;
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function formatConstraintContext(spec: GoalSpecification): string[] {
|
|
420
|
+
const lines = [
|
|
421
|
+
...spec.designConstraints.uxPrinciples.map((item) => `- UX principle: ${oneLine(item)}`),
|
|
422
|
+
...spec.designConstraints.uiRequirements.map((item) => `- UI requirement: ${oneLine(item)}`),
|
|
423
|
+
...spec.designConstraints.accessibility.map((item) => `- Accessibility: ${oneLine(item)}`),
|
|
424
|
+
...spec.designConstraints.architecturalConstraints.map((item) => `- Architecture: ${oneLine(item)}`),
|
|
425
|
+
...spec.designConstraints.constraints.map(
|
|
426
|
+
(constraint) => `- ${constraint.id} ${oneLine(constraint.title)} — ${oneLine(constraint.description)}`,
|
|
427
|
+
),
|
|
428
|
+
...spec.productConstraints.businessRules.map((item) => `- Business rule: ${oneLine(item)}`),
|
|
429
|
+
...spec.productConstraints.compliance.map((item) => `- Compliance: ${oneLine(item)}`),
|
|
430
|
+
...spec.productConstraints.dependencies.map((item) => `- Dependency: ${oneLine(item)}`),
|
|
431
|
+
...spec.productConstraints.risks.map((item) => `- Risk: ${oneLine(item)}`),
|
|
432
|
+
...spec.productConstraints.constraints.map(
|
|
433
|
+
(constraint) => `- ${constraint.id} ${oneLine(constraint.title)} — ${oneLine(constraint.description)}`,
|
|
434
|
+
),
|
|
435
|
+
];
|
|
436
|
+
return lines.length > 0 ? lines : ["- None specified."];
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function requiredVerificationGateIds(spec: GoalSpecification): string[] {
|
|
440
|
+
const required = spec.verificationGates.filter((gate) => gate.required).map((gate) => gate.id);
|
|
441
|
+
return required.length > 0 ? required : spec.definitionOfDone.verificationGateIds;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function formatIdList(values: readonly string[]): string {
|
|
445
|
+
return values.length > 0 ? values.map(oneLine).join(", ") : "none";
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function trimTrailingBlankLines(lines: string[]): string[] {
|
|
449
|
+
while (lines.at(-1)?.trim() === "") {
|
|
450
|
+
lines.pop();
|
|
451
|
+
}
|
|
452
|
+
return lines;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function trimLeadingBlankLines(lines: string[]): string[] {
|
|
456
|
+
while (lines[0]?.trim() === "") {
|
|
457
|
+
lines.shift();
|
|
458
|
+
}
|
|
459
|
+
return lines;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function ensureTrailingNewline(value: string): string {
|
|
463
|
+
return value.endsWith("\n") ? value : `${value}\n`;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function timeoutForIteration(iteration: GoalIterationState, state: GoalLoopState, now: Date): number {
|
|
467
|
+
if (!iteration.deadlineAt) {
|
|
468
|
+
return state.limits.iterationTimeoutMs;
|
|
469
|
+
}
|
|
470
|
+
const remaining = Date.parse(iteration.deadlineAt) - now.getTime();
|
|
471
|
+
if (!Number.isFinite(remaining) || remaining <= 0) {
|
|
472
|
+
return 1_000;
|
|
473
|
+
}
|
|
474
|
+
return Math.min(state.limits.iterationTimeoutMs, Math.max(1_000, Math.floor(remaining)));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function buildPreviousIterationContext(state: GoalLoopState): string {
|
|
478
|
+
const previous = state.iterations.filter((iteration) => iteration.iteration < state.currentIteration);
|
|
479
|
+
if (previous.length === 0) {
|
|
480
|
+
return "";
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return previous
|
|
484
|
+
.map((iteration) => {
|
|
485
|
+
const lines = [`Iteration ${iteration.iteration}: ${iteration.status}`];
|
|
486
|
+
if (iteration.generatedTodo?.todoPath) {
|
|
487
|
+
lines.push(`Generated TODO: ${iteration.generatedTodo.todoPath}`);
|
|
488
|
+
}
|
|
489
|
+
if (iteration.workerResult) {
|
|
490
|
+
lines.push(`Worker result: ${iteration.workerResult.status} — ${iteration.workerResult.summary}`);
|
|
491
|
+
}
|
|
492
|
+
if (iteration.reviewerResult) {
|
|
493
|
+
lines.push(`Reviewer: ${iteration.reviewerResult.decision} — ${iteration.reviewerResult.rationale}`);
|
|
494
|
+
if (iteration.reviewerResult.remainingWork.length > 0) {
|
|
495
|
+
lines.push("Remaining work:", ...iteration.reviewerResult.remainingWork.map((item) => `- ${item}`));
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return lines.join("\n");
|
|
499
|
+
})
|
|
500
|
+
.join("\n\n");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function markdownFence(value: string, language: string): string {
|
|
504
|
+
const ticks = longestBacktickRun(value) + 1;
|
|
505
|
+
const fence = "`".repeat(Math.max(3, ticks));
|
|
506
|
+
return `${fence}${language}\n${value.trim()}\n${fence}`;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function longestBacktickRun(value: string): number {
|
|
510
|
+
let longest = 0;
|
|
511
|
+
let current = 0;
|
|
512
|
+
for (const char of value) {
|
|
513
|
+
if (char === "`") {
|
|
514
|
+
current += 1;
|
|
515
|
+
longest = Math.max(longest, current);
|
|
516
|
+
} else {
|
|
517
|
+
current = 0;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return longest;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function oneLine(value: string): string {
|
|
524
|
+
return value.replace(/\s+/g, " ").trim();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function sha256(value: string): string {
|
|
528
|
+
return createHash("sha256").update(value).digest("hex");
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function throwIfAborted(signal: AbortSignal | undefined): void {
|
|
532
|
+
if (signal?.aborted) {
|
|
533
|
+
throw new GoalTodoGenerationError("TODO generation was aborted before producing a generated TODO.");
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function errorMessage(error: unknown): string {
|
|
538
|
+
return error instanceof Error ? error.message : String(error);
|
|
539
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,9 +3,17 @@ import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
|
3
3
|
import { truncateToWidth, type Component, type OverlayHandle, type TUI } from "@earendil-works/pi-tui";
|
|
4
4
|
|
|
5
5
|
import { runCoordinator, type CoordinatorProgressUpdate, type CoordinatorResult } from "./coordinator.ts";
|
|
6
|
+
import { runGoalLoop, type GoalLoopProgressUpdate, type GoalLoopRunResult } from "./goal_orchestrator.ts";
|
|
6
7
|
import { longTaskInputTransform } from "./input_router.ts";
|
|
7
|
-
import {
|
|
8
|
-
|
|
8
|
+
import {
|
|
9
|
+
formatGoalLoopResultMessage,
|
|
10
|
+
goalTaskDetailsFromResult,
|
|
11
|
+
renderGoalTaskToolCall,
|
|
12
|
+
renderGoalTaskToolResult,
|
|
13
|
+
renderLongTaskToolCall,
|
|
14
|
+
renderLongTaskToolResult,
|
|
15
|
+
} from "./render.ts";
|
|
16
|
+
import { PiGoalTaskParams, PiLongTaskParams } from "./types.ts";
|
|
9
17
|
|
|
10
18
|
export function createWorkerCostAccumulator() {
|
|
11
19
|
let pendingWorkerCostTotal = 0;
|
|
@@ -72,10 +80,35 @@ function toolDetails(result: CoordinatorResult) {
|
|
|
72
80
|
taskProgress: result.taskProgress,
|
|
73
81
|
workerCostTotal: result.workerCostTotal,
|
|
74
82
|
summary: result.summary,
|
|
83
|
+
goal: result.goal,
|
|
75
84
|
error: result.error,
|
|
76
85
|
};
|
|
77
86
|
}
|
|
78
87
|
|
|
88
|
+
function goalLoopCostTotals(result: GoalLoopRunResult): {
|
|
89
|
+
workerCostTotal: number;
|
|
90
|
+
reviewerCostTotal: number;
|
|
91
|
+
totalCost: number;
|
|
92
|
+
} {
|
|
93
|
+
const workerCostTotal = sumFinite([
|
|
94
|
+
...result.generationResults.map((item) => item.childResult.workerCostTotal),
|
|
95
|
+
...result.executionResults.map((item) => item.childResult.workerCostTotal),
|
|
96
|
+
]);
|
|
97
|
+
const reviewerCostTotal = sumFinite(result.reviewResults.map((item) => item.sessionResult.reviewerCostTotal));
|
|
98
|
+
return { workerCostTotal, reviewerCostTotal, totalCost: workerCostTotal + reviewerCostTotal };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function goalToolDetails(result: GoalLoopRunResult) {
|
|
102
|
+
return goalTaskDetailsFromResult({ ...result, ...goalLoopCostTotals(result) });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function sumFinite(values: Array<number | undefined>): number {
|
|
106
|
+
return values.reduce<number>(
|
|
107
|
+
(total, value) => total + (typeof value === "number" && Number.isFinite(value) ? value : 0),
|
|
108
|
+
0,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
79
112
|
const LONG_TASK_WIDGET_KEY = "pi-long-task-sidebar";
|
|
80
113
|
const SIDEBAR_WIDGET_RESERVED_INPUT_ROWS = 8;
|
|
81
114
|
const SIDEBAR_WIDGET_MIN_ROWS = 4;
|
|
@@ -762,4 +795,58 @@ export default function registerPiLongTaskExtension(pi: ExtensionAPI) {
|
|
|
762
795
|
}
|
|
763
796
|
},
|
|
764
797
|
});
|
|
798
|
+
|
|
799
|
+
pi.registerTool({
|
|
800
|
+
name: "pi_goal_task",
|
|
801
|
+
label: "Pi Goal Task",
|
|
802
|
+
description:
|
|
803
|
+
"Run a goal-oriented long-task loop: generate TODO markdown from a high-level goal, execute it, review goal completion, and repeat until complete, cancelled, timed out, or max iterations is reached. Pass the tool cancellation signal to stop the loop.",
|
|
804
|
+
parameters: PiGoalTaskParams,
|
|
805
|
+
renderCall: renderGoalTaskToolCall,
|
|
806
|
+
renderResult: renderGoalTaskToolResult,
|
|
807
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
808
|
+
const sidebar = createLongTaskSidebarController(ctx);
|
|
809
|
+
const publishGoalProgress = (update: GoalLoopProgressUpdate) => {
|
|
810
|
+
onUpdate?.({
|
|
811
|
+
content: [
|
|
812
|
+
{
|
|
813
|
+
type: "text" as const,
|
|
814
|
+
text: update.message,
|
|
815
|
+
},
|
|
816
|
+
],
|
|
817
|
+
details: update,
|
|
818
|
+
});
|
|
819
|
+
};
|
|
820
|
+
const publishWorkerProgress = (update: CoordinatorProgressUpdate) => {
|
|
821
|
+
sidebar?.update(update);
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
try {
|
|
825
|
+
const result = await runGoalLoop({
|
|
826
|
+
...params,
|
|
827
|
+
commit: params.commit ?? true,
|
|
828
|
+
cwd: ctx?.cwd,
|
|
829
|
+
model: ctx?.model,
|
|
830
|
+
abortSignal: signal,
|
|
831
|
+
onProgress: publishGoalProgress,
|
|
832
|
+
onWorkerProgress: publishWorkerProgress,
|
|
833
|
+
});
|
|
834
|
+
const costs = goalLoopCostTotals(result);
|
|
835
|
+
workerCostAccumulator.add(costs.totalCost);
|
|
836
|
+
const message = formatGoalLoopResultMessage({ ...result, ...costs });
|
|
837
|
+
|
|
838
|
+
return {
|
|
839
|
+
content: [
|
|
840
|
+
{
|
|
841
|
+
type: "text" as const,
|
|
842
|
+
text: message,
|
|
843
|
+
},
|
|
844
|
+
],
|
|
845
|
+
details: goalToolDetails(result),
|
|
846
|
+
};
|
|
847
|
+
} finally {
|
|
848
|
+
sidebar?.close();
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
});
|
|
765
852
|
}
|