pi-long-task 0.3.8 → 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 +222 -6
- package/package.json +1 -1
- package/src/coordinator.ts +336 -31
- 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 +90 -2
- package/src/input_router.ts +124 -6
- package/src/render.ts +223 -4
- package/src/session_guard.ts +287 -0
- package/src/todo_generator.ts +143 -5
- package/src/types.ts +66 -3
- package/src/worker_session.ts +23 -2
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assistantTextFromEvent,
|
|
3
|
+
lastAssistantTextFromEvents,
|
|
4
|
+
lastAssistantTextFromMessages,
|
|
5
|
+
type WorkerSessionLike,
|
|
6
|
+
} from "./worker_session.ts";
|
|
7
|
+
|
|
8
|
+
export interface GuardedSessionPromptOptions {
|
|
9
|
+
session: WorkerSessionLike;
|
|
10
|
+
prompt: string;
|
|
11
|
+
promptOptions?: Record<string, unknown>;
|
|
12
|
+
abortSignal?: AbortSignal;
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
gracefulShutdownMs?: number;
|
|
15
|
+
gracefulShutdownPrompt?: string;
|
|
16
|
+
diagnostics?: string[];
|
|
17
|
+
onEvent?: (event: unknown) => void;
|
|
18
|
+
dispose?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface GuardedSessionPromptResult {
|
|
22
|
+
assistantText: string;
|
|
23
|
+
timedOut: boolean;
|
|
24
|
+
aborted: boolean;
|
|
25
|
+
error?: string;
|
|
26
|
+
diagnostics: string[];
|
|
27
|
+
events: unknown[];
|
|
28
|
+
sessionFile?: string;
|
|
29
|
+
sessionId?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function runGuardedSessionPrompt(
|
|
33
|
+
options: GuardedSessionPromptOptions,
|
|
34
|
+
): Promise<GuardedSessionPromptResult> {
|
|
35
|
+
const session = options.session;
|
|
36
|
+
const diagnostics = [...(options.diagnostics ?? [])];
|
|
37
|
+
const events: unknown[] = [];
|
|
38
|
+
const timers = new Set<ReturnType<typeof setTimeout>>();
|
|
39
|
+
let assistantText = "";
|
|
40
|
+
let timedOut = false;
|
|
41
|
+
let aborted = false;
|
|
42
|
+
let error: string | undefined;
|
|
43
|
+
let promptSettled = false;
|
|
44
|
+
let finished = false;
|
|
45
|
+
let unsubscribe: (() => void) | undefined;
|
|
46
|
+
let complete: (() => void) | undefined;
|
|
47
|
+
|
|
48
|
+
const completed = new Promise<void>((resolve) => {
|
|
49
|
+
complete = resolve;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const resolveCompleted = () => {
|
|
53
|
+
complete?.();
|
|
54
|
+
complete = undefined;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const clearTimers = () => {
|
|
58
|
+
for (const timer of timers) {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
timers.clear();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const schedule = (fn: () => void, ms: number) => {
|
|
65
|
+
const timer = setTimeout(() => {
|
|
66
|
+
timers.delete(timer);
|
|
67
|
+
fn();
|
|
68
|
+
}, ms);
|
|
69
|
+
timers.add(timer);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const abortSession = (reason: string) => {
|
|
73
|
+
if (finished || aborted) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
aborted = true;
|
|
77
|
+
error = error ?? reason;
|
|
78
|
+
try {
|
|
79
|
+
const abortResult = session.abort?.();
|
|
80
|
+
if (isPromiseLike(abortResult)) {
|
|
81
|
+
void abortResult.catch((exc: unknown) => {
|
|
82
|
+
diagnostics.push(`session abort failed: ${errorMessage(exc)}`);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
} catch (exc) {
|
|
86
|
+
diagnostics.push(`session abort failed: ${errorMessage(exc)}`);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const requestGracefulShutdown = () => {
|
|
91
|
+
const message = options.gracefulShutdownPrompt?.trim();
|
|
92
|
+
if (!message || finished || promptSettled || aborted) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
if (session.isBashRunning && session.abortBash) {
|
|
98
|
+
session.abortBash();
|
|
99
|
+
diagnostics.push("aborted running bash before graceful shutdown request");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let request: Promise<unknown> | undefined;
|
|
103
|
+
if ((session.isStreaming || session.isBashRunning) && session.steer) {
|
|
104
|
+
request = session.steer(message);
|
|
105
|
+
} else if (session.followUp) {
|
|
106
|
+
request = session.followUp(message);
|
|
107
|
+
} else if (session.steer) {
|
|
108
|
+
request = session.steer(message);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!request) {
|
|
112
|
+
diagnostics.push("graceful shutdown request skipped: session does not support steer/followUp");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
void request.catch((exc: unknown) => {
|
|
117
|
+
diagnostics.push(`graceful shutdown request failed: ${errorMessage(exc)}`);
|
|
118
|
+
});
|
|
119
|
+
} catch (exc) {
|
|
120
|
+
diagnostics.push(`graceful shutdown request failed: ${errorMessage(exc)}`);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const triggerTimeout = () => {
|
|
125
|
+
if (finished || promptSettled || timedOut) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
timedOut = true;
|
|
129
|
+
diagnostics.push(`session prompt timed out after ${formatMilliseconds(timeoutMs(options.timeoutMs))}`);
|
|
130
|
+
requestGracefulShutdown();
|
|
131
|
+
|
|
132
|
+
const graceMs = nonNegativeMilliseconds(options.gracefulShutdownMs);
|
|
133
|
+
const hardAbort = () => {
|
|
134
|
+
if (finished || promptSettled) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
abortSession(`session prompt exceeded ${formatMilliseconds(timeoutMs(options.timeoutMs))} timeout`);
|
|
138
|
+
resolveCompleted();
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (graceMs > 0) {
|
|
142
|
+
schedule(hardAbort, graceMs);
|
|
143
|
+
} else {
|
|
144
|
+
hardAbort();
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const abortListener = () => {
|
|
149
|
+
if (finished || promptSettled) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
abortSession(abortReason(options.abortSignal, "session prompt aborted by outer signal"));
|
|
153
|
+
resolveCompleted();
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
if (options.abortSignal?.aborted) {
|
|
158
|
+
aborted = true;
|
|
159
|
+
error = abortReason(options.abortSignal, "session prompt aborted before start");
|
|
160
|
+
} else {
|
|
161
|
+
unsubscribe = session.subscribe((event: unknown) => {
|
|
162
|
+
events.push(event);
|
|
163
|
+
const text = assistantTextFromEvent(event);
|
|
164
|
+
if (text) {
|
|
165
|
+
assistantText = text;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
options.onEvent?.(event);
|
|
169
|
+
} catch (exc) {
|
|
170
|
+
diagnostics.push(`event listener failed: ${errorMessage(exc)}`);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
options.abortSignal?.addEventListener("abort", abortListener, { once: true });
|
|
175
|
+
|
|
176
|
+
const promptPromise = session.prompt(options.prompt, options.promptOptions).then(
|
|
177
|
+
() => {
|
|
178
|
+
promptSettled = true;
|
|
179
|
+
resolveCompleted();
|
|
180
|
+
},
|
|
181
|
+
(exc: unknown) => {
|
|
182
|
+
promptSettled = true;
|
|
183
|
+
error = error ?? errorMessage(exc);
|
|
184
|
+
resolveCompleted();
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
void promptPromise;
|
|
188
|
+
|
|
189
|
+
const limitMs = timeoutMs(options.timeoutMs);
|
|
190
|
+
if (limitMs > 0) {
|
|
191
|
+
schedule(triggerTimeout, limitMs);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
await completed;
|
|
195
|
+
}
|
|
196
|
+
} catch (exc) {
|
|
197
|
+
error = error ?? errorMessage(exc);
|
|
198
|
+
} finally {
|
|
199
|
+
finished = true;
|
|
200
|
+
clearTimers();
|
|
201
|
+
options.abortSignal?.removeEventListener("abort", abortListener);
|
|
202
|
+
unsubscribe?.();
|
|
203
|
+
assistantText = latestAssistantText(session, events, assistantText);
|
|
204
|
+
if (options.dispose !== false) {
|
|
205
|
+
try {
|
|
206
|
+
const disposeResult = (session.dispose as (() => unknown) | undefined)?.();
|
|
207
|
+
if (isPromiseLike(disposeResult)) {
|
|
208
|
+
await disposeResult;
|
|
209
|
+
}
|
|
210
|
+
} catch (exc) {
|
|
211
|
+
const message = `session dispose failed: ${errorMessage(exc)}`;
|
|
212
|
+
diagnostics.push(message);
|
|
213
|
+
error = error ?? message;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return buildResult(session, events, assistantText, timedOut, aborted, error, diagnostics);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function buildResult(
|
|
222
|
+
session: WorkerSessionLike,
|
|
223
|
+
events: unknown[],
|
|
224
|
+
assistantText: string,
|
|
225
|
+
timedOut: boolean,
|
|
226
|
+
aborted: boolean,
|
|
227
|
+
error: string | undefined,
|
|
228
|
+
diagnostics: string[],
|
|
229
|
+
): GuardedSessionPromptResult {
|
|
230
|
+
return {
|
|
231
|
+
assistantText: latestAssistantText(session, events, assistantText),
|
|
232
|
+
timedOut,
|
|
233
|
+
aborted,
|
|
234
|
+
error,
|
|
235
|
+
diagnostics: [...diagnostics],
|
|
236
|
+
events: [...events],
|
|
237
|
+
sessionFile: session.sessionFile,
|
|
238
|
+
sessionId: session.sessionId,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function latestAssistantText(session: WorkerSessionLike, events: unknown[], fallback: string): string {
|
|
243
|
+
const direct = session.getLastAssistantText?.();
|
|
244
|
+
if (direct) {
|
|
245
|
+
return direct;
|
|
246
|
+
}
|
|
247
|
+
const fromMessages = lastAssistantTextFromMessages(session.messages);
|
|
248
|
+
if (fromMessages) {
|
|
249
|
+
return fromMessages;
|
|
250
|
+
}
|
|
251
|
+
const fromEvents = lastAssistantTextFromEvents(events);
|
|
252
|
+
return fromEvents || fallback;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function timeoutMs(value: number | undefined): number {
|
|
256
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
return Math.max(0, value);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function nonNegativeMilliseconds(value: number | undefined): number {
|
|
263
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
return Math.max(0, value);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function formatMilliseconds(ms: number): string {
|
|
270
|
+
return `${(ms / 1000).toFixed(3)}s`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function abortReason(signal: AbortSignal | undefined, fallback: string): string {
|
|
274
|
+
const reason = signal?.reason;
|
|
275
|
+
if (reason === undefined) {
|
|
276
|
+
return fallback;
|
|
277
|
+
}
|
|
278
|
+
return errorMessage(reason);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function errorMessage(error: unknown): string {
|
|
282
|
+
return error instanceof Error ? error.message : String(error);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
286
|
+
return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
|
|
287
|
+
}
|
package/src/todo_generator.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
coverageGoalAction,
|
|
3
|
+
coverageGoalVerification,
|
|
4
|
+
coverageGoalVerifyBullet,
|
|
5
|
+
parseCoverageGoal,
|
|
6
|
+
} from "./coverage_goal.ts";
|
|
1
7
|
import { parseTasks, TodoParseError } from "./todo_parser.ts";
|
|
2
8
|
|
|
3
9
|
const TODO_HEADING_RE = /^##\s+TODO\s+(\d+)\s+[—-]\s+(.+?)\s*$/gm;
|
|
@@ -20,21 +26,21 @@ interface ExistingTask {
|
|
|
20
26
|
body: string;
|
|
21
27
|
}
|
|
22
28
|
|
|
23
|
-
export function todoMarkdownFromString(rawInput: string): string | undefined {
|
|
29
|
+
export function todoMarkdownFromString(rawInput: string, goal?: string): string | undefined {
|
|
24
30
|
const input = rawInput.trim();
|
|
25
31
|
if (!input) {
|
|
26
32
|
return undefined;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
if (hasTodoHeadings(input)) {
|
|
30
|
-
const markdown = normalizeExistingTodoMarkdown(input);
|
|
36
|
+
const markdown = applyGoalInstructionsToTodoMarkdown(normalizeExistingTodoMarkdown(input), goal);
|
|
31
37
|
validateTodoMarkdown(markdown);
|
|
32
38
|
return markdown;
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
const listItems = simpleListItems(input);
|
|
36
42
|
if (listItems.length >= 2) {
|
|
37
|
-
const markdown = generatedTodoMarkdown(listItems);
|
|
43
|
+
const markdown = applyGoalInstructionsToTodoMarkdown(generatedTodoMarkdown(listItems), goal);
|
|
38
44
|
validateTodoMarkdown(markdown);
|
|
39
45
|
return markdown;
|
|
40
46
|
}
|
|
@@ -116,8 +122,140 @@ export function validateTodoMarkdown(markdown: string): void {
|
|
|
116
122
|
});
|
|
117
123
|
}
|
|
118
124
|
|
|
119
|
-
export function
|
|
120
|
-
|
|
125
|
+
export function applyGoalInstructionsToTodoMarkdown(markdown: string, goal?: string): string {
|
|
126
|
+
const trimmedGoal = oneLine(goal ?? "");
|
|
127
|
+
if (!trimmedGoal) {
|
|
128
|
+
return markdown;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let next = insertGlobalGoalInstructions(markdown, trimmedGoal);
|
|
132
|
+
const coverageGoal = parseCoverageGoal(trimmedGoal);
|
|
133
|
+
if (coverageGoal) {
|
|
134
|
+
next = appendCoverageVerificationToTasks(next, coverageGoalVerifyBullet(coverageGoal));
|
|
135
|
+
}
|
|
136
|
+
validateTodoMarkdown(next);
|
|
137
|
+
return next;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function insertGlobalGoalInstructions(markdown: string, goal: string): string {
|
|
141
|
+
const coverageGoal = parseCoverageGoal(goal);
|
|
142
|
+
const additions = [`- Long task goal: ${goal}`];
|
|
143
|
+
if (coverageGoal) {
|
|
144
|
+
additions.push(`- Coverage goal: ${coverageGoalAction(coverageGoal)}`);
|
|
145
|
+
additions.push(`- Coverage verification: ${coverageGoalVerification(coverageGoal)}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
|
|
149
|
+
const progressIndex = lines.findIndex((line) => /^##\s+Progress\s*$/i.test(line.trim()));
|
|
150
|
+
if (progressIndex < 0) {
|
|
151
|
+
return markdown;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const existingGlobalText = lines.slice(0, progressIndex).join("\n");
|
|
155
|
+
const missing = additions.filter((line) => !existingGlobalText.includes(line));
|
|
156
|
+
if (missing.length === 0) {
|
|
157
|
+
return markdown;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const before = trimTrailingBlankLines(lines.slice(0, progressIndex));
|
|
161
|
+
const after = trimLeadingBlankLines(lines.slice(progressIndex));
|
|
162
|
+
const hasGlobalHeading = /^Global instructions:\s*$/im.test(existingGlobalText);
|
|
163
|
+
const block = hasGlobalHeading ? missing : ["Global instructions:", ...missing];
|
|
164
|
+
return ensureTrailingNewline([...before, "", ...block, "", ...after].join("\n"));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function appendCoverageVerificationToTasks(markdown: string, verificationBullet: string): string {
|
|
168
|
+
const tasks = parseTasks(markdown);
|
|
169
|
+
const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
|
|
170
|
+
for (const task of [...tasks].reverse()) {
|
|
171
|
+
if (task.section.includes(verificationBullet)) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const startIndex = task.startLine - 1;
|
|
176
|
+
const endIndex = task.endLine;
|
|
177
|
+
const verifyIndex = findVerifyLineIndex(lines, startIndex, endIndex);
|
|
178
|
+
if (verifyIndex >= 0) {
|
|
179
|
+
lines.splice(verifyIndex + 1, 0, verificationBullet);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const doneIndex = findDoneWhenLineIndex(lines, startIndex, endIndex);
|
|
184
|
+
const insertIndex = doneIndex >= 0 ? doneIndex : endIndex;
|
|
185
|
+
lines.splice(insertIndex, 0, "**Verify:**", verificationBullet, "");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return ensureTrailingNewline(lines.join("\n"));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function findVerifyLineIndex(lines: readonly string[], startIndex: number, endIndex: number): number {
|
|
192
|
+
for (let idx = startIndex; idx < endIndex; idx += 1) {
|
|
193
|
+
if (/^\*\*Verify:\*\*/i.test(lines[idx].trim())) {
|
|
194
|
+
return idx;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return -1;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function findDoneWhenLineIndex(lines: readonly string[], startIndex: number, endIndex: number): number {
|
|
201
|
+
for (let idx = startIndex; idx < endIndex; idx += 1) {
|
|
202
|
+
if (/^\*\*Done when:\*\*/i.test(lines[idx].trim())) {
|
|
203
|
+
return idx;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return -1;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function trimTrailingBlankLines(lines: string[]): string[] {
|
|
210
|
+
while (lines.at(-1)?.trim() === "") {
|
|
211
|
+
lines.pop();
|
|
212
|
+
}
|
|
213
|
+
return lines;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function trimLeadingBlankLines(lines: string[]): string[] {
|
|
217
|
+
while (lines[0]?.trim() === "") {
|
|
218
|
+
lines.shift();
|
|
219
|
+
}
|
|
220
|
+
return lines;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function oneLine(value: string): string {
|
|
224
|
+
return value.replace(/\s+/g, " ").trim();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function buildTodoCreationPrompt(rawInput: string, goal?: string): string {
|
|
228
|
+
const goalBlock = todoGoalPromptBlock(goal);
|
|
229
|
+
return `Convert the following raw project request into Pi Long Task-compatible TODO markdown.\n\nRequirements:\n- Output only markdown, with no commentary and no code fence.\n- Start with exactly: # Pi Long Task TODO\n- Include a ## Progress section with one unchecked line per task: - [ ] TODO N — Title\n- Include a --- separator before task sections.\n- Create sequential sections named ## TODO N — Title.\n- Each task section must include **Goal:**, **Status:** with unchecked checkbox items, **Verify:** with concrete verification guidance, and **Done when:**.\n- Preserve any global instructions or constraints that apply to all tasks above ## Progress.\n- Keep tasks focused and independently assignable to worker sessions.\n${goalBlock}\nRaw input:\n\n${rawInput.trim()}\n`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function buildTodoRepairPrompt(
|
|
233
|
+
rawInput: string,
|
|
234
|
+
invalidOutput: string,
|
|
235
|
+
validationError: string,
|
|
236
|
+
goal?: string,
|
|
237
|
+
): string {
|
|
238
|
+
const goalBlock = todoGoalPromptBlock(goal);
|
|
239
|
+
return `Your previous response was not valid Pi Long Task TODO markdown. Correct it now.\n\nValidation/extraction error:\n${validationError.trim() || "Unknown validation error."}\n\nRequirements:\n- Output only corrected markdown, with no commentary and no code fence.\n- Start with exactly: # Pi Long Task TODO\n- Include a ## Progress section with one unchecked line per task: - [ ] TODO N — Title\n- Include a --- separator before task sections.\n- Create sequential sections named ## TODO N — Title.\n- Each task section must include **Goal:**, **Status:** with unchecked checkbox items, **Verify:** with concrete verification guidance, and **Done when:**.\n- Preserve any global instructions or constraints that apply to all tasks above ## Progress.\n- Keep tasks focused and independently assignable to worker sessions.\n${goalBlock}\nOriginal raw input:\n\n${rawInput.trim()}\n\nPrevious invalid output:\n\n${invalidOutput.trim()}\n`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function todoGoalPromptBlock(goal: string | undefined): string {
|
|
243
|
+
const trimmed = goal?.trim();
|
|
244
|
+
if (!trimmed) {
|
|
245
|
+
return "";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const coverageGoal = parseCoverageGoal(trimmed);
|
|
249
|
+
const coverageBlock = coverageGoal
|
|
250
|
+
? `\nCoverage goal requirements:\n- Include global instructions that tell workers to ${coverageGoalAction(coverageGoal)}\n- Include verification guidance that tells workers to ${coverageGoalVerification(coverageGoal)}\n- Do not hardcode a fixed coverage threshold; use the requested threshold (${coverageGoal.thresholdText}%).\n`
|
|
251
|
+
: "";
|
|
252
|
+
return `\nOverall goal:\n\n${trimmed}\n${coverageBlock}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function extractAndValidateTodoMarkdown(assistantText: string): string {
|
|
256
|
+
const markdown = extractTodoMarkdown(assistantText);
|
|
257
|
+
validateTodoMarkdown(markdown);
|
|
258
|
+
return markdown;
|
|
121
259
|
}
|
|
122
260
|
|
|
123
261
|
export function extractTodoMarkdown(assistantText: string): string {
|
package/src/types.ts
CHANGED
|
@@ -6,18 +6,80 @@ import type { SessionOutcome } from "./worker_session.ts";
|
|
|
6
6
|
|
|
7
7
|
export const PiLongTaskParams = Type.Object(
|
|
8
8
|
{
|
|
9
|
-
inputText: Type.
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
inputText: Type.Optional(
|
|
10
|
+
Type.String({
|
|
11
|
+
description:
|
|
12
|
+
"Optional TODO file content or the user's long-task instructions to process. Natural-language routing parses commit and goal separately.",
|
|
13
|
+
}),
|
|
14
|
+
),
|
|
12
15
|
commit: Type.Boolean({
|
|
13
16
|
description:
|
|
14
17
|
"Whether Pi Long Task may commit completed worker changes. Use true when the user asks for commits or committing as work progresses; otherwise use false.",
|
|
15
18
|
}),
|
|
19
|
+
goal: Type.Optional(
|
|
20
|
+
Type.String({
|
|
21
|
+
description: "Optional high-level goal or desired outcome for the long-task run.",
|
|
22
|
+
}),
|
|
23
|
+
),
|
|
24
|
+
},
|
|
25
|
+
{ additionalProperties: false },
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export const PiGoalTaskParams = Type.Object(
|
|
29
|
+
{
|
|
30
|
+
goal: Type.String({
|
|
31
|
+
description:
|
|
32
|
+
"High-level goal to achieve through repeated TODO generation, long-task execution, and reviewer iterations until complete or stopped by limits.",
|
|
33
|
+
}),
|
|
34
|
+
commit: Type.Optional(
|
|
35
|
+
Type.Boolean({
|
|
36
|
+
description:
|
|
37
|
+
"Whether worker long tasks may commit completed TODO work during the goal loop. Defaults to true for goal loops.",
|
|
38
|
+
}),
|
|
39
|
+
),
|
|
40
|
+
maxIterations: Type.Optional(
|
|
41
|
+
Type.Integer({
|
|
42
|
+
minimum: 1,
|
|
43
|
+
description: "Maximum number of generate → execute → review iterations before stopping. Defaults to 50.",
|
|
44
|
+
}),
|
|
45
|
+
),
|
|
46
|
+
timeoutMs: Type.Optional(
|
|
47
|
+
Type.Integer({
|
|
48
|
+
minimum: 1,
|
|
49
|
+
description: "Overall goal-loop timeout in milliseconds. Defaults to 172800000 (48 hours).",
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
52
|
+
iterationTimeoutMs: Type.Optional(
|
|
53
|
+
Type.Integer({
|
|
54
|
+
minimum: 1,
|
|
55
|
+
description:
|
|
56
|
+
"Timeout budget in milliseconds for each generated TODO worker iteration. Defaults to 10800000 (3 hours).",
|
|
57
|
+
}),
|
|
58
|
+
),
|
|
59
|
+
reviewerTimeoutMs: Type.Optional(
|
|
60
|
+
Type.Integer({
|
|
61
|
+
minimum: 1,
|
|
62
|
+
description: "Timeout budget in milliseconds for each reviewer session. Defaults to 1800000 (30 minutes).",
|
|
63
|
+
}),
|
|
64
|
+
),
|
|
65
|
+
maxAttemptsPerTask: Type.Optional(
|
|
66
|
+
Type.Integer({
|
|
67
|
+
minimum: 1,
|
|
68
|
+
description: "Maximum attempts for each TODO inside worker long-task runs.",
|
|
69
|
+
}),
|
|
70
|
+
),
|
|
71
|
+
maxBashTimeoutMs: Type.Optional(
|
|
72
|
+
Type.Integer({
|
|
73
|
+
minimum: 1,
|
|
74
|
+
description: "Maximum bash command timeout in milliseconds allowed in worker sessions.",
|
|
75
|
+
}),
|
|
76
|
+
),
|
|
16
77
|
},
|
|
17
78
|
{ additionalProperties: false },
|
|
18
79
|
);
|
|
19
80
|
|
|
20
81
|
export type PiLongTaskInput = Static<typeof PiLongTaskParams>;
|
|
82
|
+
export type PiGoalTaskInput = Static<typeof PiGoalTaskParams>;
|
|
21
83
|
|
|
22
84
|
export type CoordinatorStatus = "done" | "partial" | "blocked" | "failed";
|
|
23
85
|
|
|
@@ -64,5 +126,6 @@ export interface PiLongTaskResult {
|
|
|
64
126
|
taskProgress: TaskProgressModel;
|
|
65
127
|
workerCostTotal: number;
|
|
66
128
|
commit: boolean;
|
|
129
|
+
goal?: string;
|
|
67
130
|
error?: string;
|
|
68
131
|
}
|
package/src/worker_session.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { coverageGoalAction, coverageGoalVerification, parseCoverageGoal } from "./coverage_goal.ts";
|
|
1
2
|
import { hasTaskResult, hasTaskResultStatus, isDoneStatus, parseReportedStatus } from "./result_writer.ts";
|
|
2
3
|
import type { Task } from "./todo_parser.ts";
|
|
3
4
|
|
|
@@ -8,6 +9,7 @@ export interface WorkerTaskPromptOptions {
|
|
|
8
9
|
commitRequested: boolean;
|
|
9
10
|
previousAttempts?: string;
|
|
10
11
|
globalInstructions?: string;
|
|
12
|
+
goal?: string;
|
|
11
13
|
maxBashTimeoutSeconds: number;
|
|
12
14
|
}
|
|
13
15
|
|
|
@@ -22,7 +24,7 @@ export function taskLabel(task: Pick<Task, "taskId" | "title">): string {
|
|
|
22
24
|
|
|
23
25
|
export function buildTaskPrompt(options: WorkerTaskPromptOptions): string {
|
|
24
26
|
const commitText = options.commitRequested
|
|
25
|
-
? "Pi Long Task will commit after your session if needed. Do not run git commit."
|
|
27
|
+
? "Commit mode is enabled: complete your assigned work and report status accurately; Pi Long Task will commit eligible completed work after your session if needed. Do not run git commit."
|
|
26
28
|
: "Do not run git commit. Pi Long Task was started with commits disabled.";
|
|
27
29
|
|
|
28
30
|
const previousAttempts = (options.previousAttempts || "").trim();
|
|
@@ -47,11 +49,30 @@ ${globalInstructions}
|
|
|
47
49
|
`
|
|
48
50
|
: "";
|
|
49
51
|
|
|
52
|
+
const goal = (options.goal || "").trim();
|
|
53
|
+
const coverageGoal = parseCoverageGoal(goal);
|
|
54
|
+
const coverageGoalText = coverageGoal
|
|
55
|
+
? `
|
|
56
|
+
|
|
57
|
+
Coverage goal guidance:
|
|
58
|
+
- ${coverageGoalAction(coverageGoal)}
|
|
59
|
+
- ${coverageGoalVerification(coverageGoal)} Prefer the project-specific coverage script when available (for example, \`npm run test:coverage\`, \`npm run coverage\`, or \`npm test -- --coverage\`).`
|
|
60
|
+
: "";
|
|
61
|
+
const goalText = goal
|
|
62
|
+
? `
|
|
63
|
+
|
|
64
|
+
Long task goal:
|
|
65
|
+
|
|
66
|
+
\`\`\`text
|
|
67
|
+
${goal}
|
|
68
|
+
\`\`\`${coverageGoalText}`
|
|
69
|
+
: "";
|
|
70
|
+
|
|
50
71
|
return `You are one Pi SDK worker session assigned to exactly one TODO task.
|
|
51
72
|
|
|
52
73
|
Assigned TODO file path: \`${options.todoPath}\`
|
|
53
74
|
Assigned task: \`${taskLabel(options.task)}\`
|
|
54
|
-
Attempt: ${options.attempt}
|
|
75
|
+
Attempt: ${options.attempt}${goalText}
|
|
55
76
|
|
|
56
77
|
Rules:
|
|
57
78
|
- Work only on the assigned task below. Do not start or fix other TODO tasks.
|