@zhushanwen/pi-goal 0.1.2 → 0.1.3
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/package.json +1 -1
- package/src/index.ts +86 -86
- package/src/templates.ts +47 -47
- package/src/tool-handler.ts +40 -42
- package/src/widget.ts +15 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-goal",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Codex-style /goal command for Pi — persistent goal-driven autonomous loop with evidence-based completion, token/time budgets, blocked detection, and steering templates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -150,19 +150,19 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
150
150
|
switch (parsed.action) {
|
|
151
151
|
case "status": {
|
|
152
152
|
if (!session.state) {
|
|
153
|
-
ctx.ui.notify("Goal
|
|
153
|
+
ctx.ui.notify("Goal mode not active. Use /goal <objective> to start.", "info");
|
|
154
154
|
return;
|
|
155
155
|
}
|
|
156
156
|
const completed = getCompletedCount(session.state.tasks);
|
|
157
157
|
const total = session.state.tasks.length;
|
|
158
158
|
const elapsed = getElapsedTimeSeconds(session.state);
|
|
159
159
|
const lines = [
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
160
|
+
`Objective: ${session.state.objective}`,
|
|
161
|
+
`Status: ${session.state.status}`,
|
|
162
|
+
`Turn: ${session.state.turnCount}/${session.state.budget.maxTurns}`,
|
|
163
|
+
`Tasks: ${completed}/${total} completed`,
|
|
164
|
+
`Stall turns: ${session.state.stallCount}`,
|
|
165
|
+
`Time elapsed: ${Math.floor(elapsed / SECONDS_PER_MINUTE)}m${Math.floor(elapsed % SECONDS_PER_MINUTE)}s`,
|
|
166
166
|
session.state.budget.tokenBudget ? `Token: ${session.state.tokensUsed}/${session.state.budget.tokenBudget}` : null,
|
|
167
167
|
`Goal ID: ${session.state.goalId}`,
|
|
168
168
|
].filter(Boolean);
|
|
@@ -172,31 +172,31 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
172
172
|
|
|
173
173
|
case "pause": {
|
|
174
174
|
if (!session.state) {
|
|
175
|
-
ctx.ui.notify("Goal
|
|
175
|
+
ctx.ui.notify("Goal mode not active.", "warning");
|
|
176
176
|
return;
|
|
177
177
|
}
|
|
178
178
|
if (isTerminalStatus(session.state.status)) {
|
|
179
|
-
ctx.ui.notify(`Goal
|
|
179
|
+
ctx.ui.notify(`Goal is in terminal state (${session.state.status}), cannot pause.`, "warning");
|
|
180
180
|
return;
|
|
181
181
|
}
|
|
182
182
|
session.state.status = transitionStatus(session.state.status, "paused");
|
|
183
183
|
persistGoalState(pi, session, ctx);
|
|
184
184
|
updateWidget(session, ctx);
|
|
185
|
-
ctx.ui.notify("Goal
|
|
185
|
+
ctx.ui.notify("Goal paused. Use /goal resume to continue.", "info");
|
|
186
186
|
return;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
case "resume": {
|
|
190
190
|
if (!session.state) {
|
|
191
|
-
ctx.ui.notify("Goal
|
|
191
|
+
ctx.ui.notify("Goal mode not active.", "warning");
|
|
192
192
|
return;
|
|
193
193
|
}
|
|
194
194
|
if (isTerminalStatus(session.state.status)) {
|
|
195
|
-
ctx.ui.notify(`Goal
|
|
195
|
+
ctx.ui.notify(`Goal is in terminal state (${session.state.status}), cannot resume.`, "warning");
|
|
196
196
|
return;
|
|
197
197
|
}
|
|
198
198
|
if (session.state.status !== "paused" && session.state.status !== "blocked") {
|
|
199
|
-
ctx.ui.notify("Goal
|
|
199
|
+
ctx.ui.notify("Goal is not paused or blocked, no need to resume.", "info");
|
|
200
200
|
return;
|
|
201
201
|
}
|
|
202
202
|
session.state.status = "active";
|
|
@@ -210,7 +210,7 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
210
210
|
session.state.status = transitionStatus(session.state.status, dim === "token" ? "budget_limited" : "time_limited");
|
|
211
211
|
persistGoalState(pi, session, ctx);
|
|
212
212
|
updateWidget(session, ctx);
|
|
213
|
-
ctx.ui.notify(`${dim === "token" ? "Token" : "
|
|
213
|
+
ctx.ui.notify(`${dim === "token" ? "Token" : "Time"} budget exhausted, cannot resume. Use /goal clear to reset.`, "warning");
|
|
214
214
|
return;
|
|
215
215
|
}
|
|
216
216
|
|
|
@@ -220,17 +220,17 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
220
220
|
const incomplete = getIncompleteTasks(session.state.tasks);
|
|
221
221
|
if (incomplete.length > 0) {
|
|
222
222
|
pi.sendUserMessage(
|
|
223
|
-
`Goal
|
|
223
|
+
`Goal resumed. Continuing with ${incomplete.length} remaining tasks.` +
|
|
224
224
|
(session.state.lastBlockerReason ? `
|
|
225
225
|
|
|
226
|
-
|
|
226
|
+
Previous blocker: ${session.state.lastBlockerReason}. Try a different approach.` : "") +
|
|
227
227
|
`
|
|
228
228
|
|
|
229
|
-
|
|
229
|
+
Objective: ${session.state.objective}`,
|
|
230
230
|
{ deliverAs: "followUp" },
|
|
231
231
|
);
|
|
232
232
|
} else {
|
|
233
|
-
ctx.ui.notify("
|
|
233
|
+
ctx.ui.notify("All tasks completed.", "info");
|
|
234
234
|
}
|
|
235
235
|
return;
|
|
236
236
|
}
|
|
@@ -250,13 +250,13 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
250
250
|
}>>;
|
|
251
251
|
|
|
252
252
|
if (historyEntries.length === 0) {
|
|
253
|
-
ctx.ui.notify("
|
|
253
|
+
ctx.ui.notify("No goal history", "info");
|
|
254
254
|
return;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
// 按时间倒序
|
|
258
258
|
const sorted = [...historyEntries].reverse();
|
|
259
|
-
const lines: string[] = ["
|
|
259
|
+
const lines: string[] = ["Goal history:\n"];
|
|
260
260
|
for (let i = 0; i < sorted.length; i++) {
|
|
261
261
|
const h = sorted[i]!.data;
|
|
262
262
|
if (!h) continue;
|
|
@@ -271,7 +271,7 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
271
271
|
const mins = Math.floor(h.elapsedSeconds / SECONDS_PER_MINUTE);
|
|
272
272
|
const secs = Math.floor(h.elapsedSeconds % SECONDS_PER_MINUTE);
|
|
273
273
|
lines.push(`${i + 1}. ${statusIcon} ${objDisplay}`);
|
|
274
|
-
lines.push(` ${h.completedTasks}/${h.totalTasks}
|
|
274
|
+
lines.push(` ${h.completedTasks}/${h.totalTasks} tasks | ${mins}m${secs}s | ${h.status}`);
|
|
275
275
|
}
|
|
276
276
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
277
277
|
return;
|
|
@@ -279,7 +279,7 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
279
279
|
|
|
280
280
|
case "clear": {
|
|
281
281
|
if (!session.state) {
|
|
282
|
-
ctx.ui.notify("Goal
|
|
282
|
+
ctx.ui.notify("Goal mode not active.", "info");
|
|
283
283
|
return;
|
|
284
284
|
}
|
|
285
285
|
session.state.status = "cancelled";
|
|
@@ -287,17 +287,17 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
287
287
|
writeGoalHistoryEntry(pi, session);
|
|
288
288
|
persistGoalState(pi, session, ctx);
|
|
289
289
|
clearGoalSession(session, ctx);
|
|
290
|
-
ctx.ui.notify("Goal
|
|
290
|
+
ctx.ui.notify("Goal cleared.", "info");
|
|
291
291
|
return;
|
|
292
292
|
}
|
|
293
293
|
|
|
294
294
|
case "update": {
|
|
295
295
|
if (!session.state) {
|
|
296
|
-
ctx.ui.notify("Goal
|
|
296
|
+
ctx.ui.notify("Goal mode not active.", "warning");
|
|
297
297
|
return;
|
|
298
298
|
}
|
|
299
299
|
if (!parsed.objective) {
|
|
300
|
-
ctx.ui.notify("
|
|
300
|
+
ctx.ui.notify("Usage: /goal update <new-objective>", "warning");
|
|
301
301
|
return;
|
|
302
302
|
}
|
|
303
303
|
const oldObjective = session.state.objective;
|
|
@@ -313,7 +313,7 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
313
313
|
session.tasksCompletedAtAgentStart = 0;
|
|
314
314
|
persistGoalState(pi, session, ctx);
|
|
315
315
|
updateWidget(session, ctx);
|
|
316
|
-
ctx.ui.notify(
|
|
316
|
+
ctx.ui.notify(`Objective updated:\nPrevious: ${oldObjective}\nNew: ${parsed.objective}`, "info");
|
|
317
317
|
|
|
318
318
|
if (isActiveStatus(session.state.status)) {
|
|
319
319
|
pi.sendUserMessage(objectiveUpdatedPrompt(session.state, oldObjective), { deliverAs: "steer" });
|
|
@@ -323,16 +323,16 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
323
323
|
|
|
324
324
|
case "set": {
|
|
325
325
|
if (!parsed.objective) {
|
|
326
|
-
ctx.ui.notify("
|
|
326
|
+
ctx.ui.notify("Usage: /goal <objective> [--tokens N] [--timeout N]", "warning");
|
|
327
327
|
return;
|
|
328
328
|
}
|
|
329
329
|
if (!parsed.objective.trim()) {
|
|
330
|
-
ctx.ui.notify("
|
|
330
|
+
ctx.ui.notify("Objective cannot be empty.", "warning");
|
|
331
331
|
return;
|
|
332
332
|
}
|
|
333
333
|
if (session.state && !isTerminalStatus(session.state.status)) {
|
|
334
334
|
ctx.ui.notify(
|
|
335
|
-
|
|
335
|
+
`Cancelled previous Goal: ${session.state.objective}\n(new goal started)`,
|
|
336
336
|
"info",
|
|
337
337
|
);
|
|
338
338
|
session.state.status = "cancelled";
|
|
@@ -342,7 +342,7 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
if (parsed.budget?.tokenBudget !== undefined && parsed.budget.tokenBudget <= 0) {
|
|
345
|
-
ctx.ui.notify("Token
|
|
345
|
+
ctx.ui.notify("Token budget must be greater than 0.", "warning");
|
|
346
346
|
return;
|
|
347
347
|
}
|
|
348
348
|
const budget: Partial<BudgetConfig> = {};
|
|
@@ -359,11 +359,11 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
359
359
|
updateWidget(session, ctx);
|
|
360
360
|
|
|
361
361
|
const budgetNotice: string[] = [];
|
|
362
|
-
if (budget.tokenBudget) budgetNotice.push(`Token
|
|
363
|
-
if (budget.timeBudgetMinutes) budgetNotice.push(
|
|
362
|
+
if (budget.tokenBudget) budgetNotice.push(`Token budget: ${budget.tokenBudget}`);
|
|
363
|
+
if (budget.timeBudgetMinutes) budgetNotice.push(`Time budget: ${budget.timeBudgetMinutes} min`);
|
|
364
364
|
const notice = [
|
|
365
|
-
`Goal
|
|
366
|
-
|
|
365
|
+
`Goal started: ${parsed.objective}`,
|
|
366
|
+
`Max turns: ${budget.maxTurns}`,
|
|
367
367
|
...budgetNotice,
|
|
368
368
|
].join("\n");
|
|
369
369
|
ctx.ui.notify(notice, "info");
|
|
@@ -477,11 +477,11 @@ async function handleBeforeAgentStart(pi: ExtensionAPI, session: GoalSession, ct
|
|
|
477
477
|
message: {
|
|
478
478
|
customType: "goal-context-exceeded",
|
|
479
479
|
content:
|
|
480
|
-
"[GOAL —
|
|
481
|
-
"1.
|
|
482
|
-
"2.
|
|
483
|
-
"3.
|
|
484
|
-
"
|
|
480
|
+
"[GOAL — context space low, must wrap up now]\n" +
|
|
481
|
+
"1. Use goal_manager's list_tasks to check remaining tasks\n" +
|
|
482
|
+
"2. Only mark tasks you genuinely completed with evidence\n" +
|
|
483
|
+
"3. Summarize current progress and remaining work\n" +
|
|
484
|
+
"Do not start new tasks.",
|
|
485
485
|
display: false,
|
|
486
486
|
},
|
|
487
487
|
};
|
|
@@ -510,7 +510,7 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
|
|
|
510
510
|
if (checkStale()) return;
|
|
511
511
|
updateWidget(session, ctx);
|
|
512
512
|
ctx.ui.notify(
|
|
513
|
-
|
|
513
|
+
`Objective completed ✓ (${getCompletedCount(session.state.tasks)}/${session.state.tasks.length} tasks, ${session.state.turnCount} turns)`,
|
|
514
514
|
"info",
|
|
515
515
|
);
|
|
516
516
|
return;
|
|
@@ -520,7 +520,7 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
|
|
|
520
520
|
persistGoalState(pi, session, ctx);
|
|
521
521
|
if (checkStale()) return;
|
|
522
522
|
updateWidget(session, ctx);
|
|
523
|
-
ctx.ui.notify("Goal
|
|
523
|
+
ctx.ui.notify("Goal blocked. Use /goal resume to continue or /goal clear to reset.", "warning");
|
|
524
524
|
return;
|
|
525
525
|
}
|
|
526
526
|
|
|
@@ -542,10 +542,10 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
|
|
|
542
542
|
for (const w of budgetResult.warnings) {
|
|
543
543
|
if (w.type === "warning90") {
|
|
544
544
|
session.state.budgetWarning90Sent = true;
|
|
545
|
-
ctx.ui.notify(`${w.dimension === "token" ? "Token" : "
|
|
545
|
+
ctx.ui.notify(`${w.dimension === "token" ? "Token" : "Time"} budget 90% used — start wrapping up.`, "warning");
|
|
546
546
|
} else if (w.type === "warning70") {
|
|
547
547
|
session.state.budgetWarning70Sent = true;
|
|
548
|
-
ctx.ui.notify(`${w.dimension === "token" ? "Token" : "
|
|
548
|
+
ctx.ui.notify(`${w.dimension === "token" ? "Token" : "Time"} budget 70% used — keep scope in check.`, "info");
|
|
549
549
|
}
|
|
550
550
|
}
|
|
551
551
|
|
|
@@ -560,8 +560,8 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
|
|
|
560
560
|
updateWidget(session, ctx);
|
|
561
561
|
ctx.ui.notify(
|
|
562
562
|
dim === "token"
|
|
563
|
-
? "Token
|
|
564
|
-
:
|
|
563
|
+
? "Token budget exhausted, Goal terminated."
|
|
564
|
+
: `Time budget exhausted (${session.state.budget.timeBudgetMinutes} min), Goal terminated.`,
|
|
565
565
|
"warning",
|
|
566
566
|
);
|
|
567
567
|
return;
|
|
@@ -595,7 +595,7 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
|
|
|
595
595
|
if (checkStale()) return;
|
|
596
596
|
updateWidget(session, ctx);
|
|
597
597
|
ctx.ui.notify(
|
|
598
|
-
|
|
598
|
+
`All tasks completed, Goal auto-closed. (${progress.completedCount}/${progress.totalCount} tasks, ${session.state.turnCount} turns)`,
|
|
599
599
|
"info",
|
|
600
600
|
);
|
|
601
601
|
return;
|
|
@@ -603,15 +603,15 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
|
|
|
603
603
|
|
|
604
604
|
if (progress.budgetTight) {
|
|
605
605
|
pi.sendUserMessage(
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
`\n\
|
|
606
|
+
`All tasks completed, token budget ${Math.round(session.state.tokensUsed / session.state.budget.tokenBudget! * PERCENT_FACTOR)}% used.` +
|
|
607
|
+
`Call goal_manager's complete_goal now with overall evidence.` +
|
|
608
|
+
`\n\nObjective: ${session.state.objective}`,
|
|
609
609
|
{ deliverAs: "steer" },
|
|
610
610
|
);
|
|
611
611
|
} else {
|
|
612
612
|
pi.sendUserMessage(
|
|
613
|
-
|
|
614
|
-
`\n\
|
|
613
|
+
`All ${progress.totalCount} tasks completed. Call goal_manager's complete_goal with overall evidence.` +
|
|
614
|
+
`\n\nObjective: ${session.state.objective}`,
|
|
615
615
|
{ deliverAs: "followUp" },
|
|
616
616
|
);
|
|
617
617
|
}
|
|
@@ -630,14 +630,14 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
|
|
|
630
630
|
if (checkStale()) return;
|
|
631
631
|
updateWidget(session, ctx);
|
|
632
632
|
ctx.ui.notify(
|
|
633
|
-
|
|
633
|
+
`Max turns reached (${session.state.budget.maxTurns}), LLM did not create task list.`,
|
|
634
634
|
"warning",
|
|
635
635
|
);
|
|
636
636
|
return;
|
|
637
637
|
}
|
|
638
638
|
pi.sendUserMessage(
|
|
639
|
-
|
|
640
|
-
`\n\
|
|
639
|
+
`No task list created yet. Call goal_manager's create_tasks immediately to decompose the work into verifiable task steps.` +
|
|
640
|
+
`\n\nObjective: ${session.state.objective}`,
|
|
641
641
|
{ deliverAs: "followUp" },
|
|
642
642
|
);
|
|
643
643
|
persistGoalState(pi, session, ctx);
|
|
@@ -655,7 +655,7 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
|
|
|
655
655
|
if (checkStale()) return;
|
|
656
656
|
updateWidget(session, ctx);
|
|
657
657
|
ctx.ui.notify(
|
|
658
|
-
|
|
658
|
+
`Max turns reached (${session.state.budget.maxTurns}), ${incomplete.length} tasks still incomplete.`,
|
|
659
659
|
"warning",
|
|
660
660
|
);
|
|
661
661
|
return;
|
|
@@ -675,7 +675,7 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
|
|
|
675
675
|
if (checkStale()) return;
|
|
676
676
|
updateWidget(session, ctx);
|
|
677
677
|
ctx.ui.notify(
|
|
678
|
-
|
|
678
|
+
`${session.state.stallCount} consecutive turns without progress, Goal auto-blocked. Use /goal resume to continue or /goal clear to reset.`,
|
|
679
679
|
"warning",
|
|
680
680
|
);
|
|
681
681
|
return;
|
|
@@ -715,33 +715,33 @@ export default function goalExtension(pi: ExtensionAPI) {
|
|
|
715
715
|
name: "goal_manager",
|
|
716
716
|
label: "Goal Manager",
|
|
717
717
|
description:
|
|
718
|
-
"Goal
|
|
719
|
-
"\n\
|
|
720
|
-
"\n- create_tasks:
|
|
721
|
-
"\n- add_tasks:
|
|
722
|
-
"\n- update_tasks:
|
|
723
|
-
"\n- list_tasks:
|
|
724
|
-
"\n- complete_goal:
|
|
725
|
-
"\n- cancel_goal:
|
|
726
|
-
"\n- report_blocked:
|
|
727
|
-
"\n- add_subtasks:
|
|
728
|
-
"\n- update_subtasks:
|
|
729
|
-
"\n- delete_subtasks:
|
|
730
|
-
promptSnippet: "
|
|
718
|
+
"Goal mode task manager. This tool is only available after starting a goal via the /goal command. AI cannot trigger it proactively. If Goal mode is not active, calling this tool will error." +
|
|
719
|
+
"\n\nAvailable actions:" +
|
|
720
|
+
"\n- create_tasks: Decompose the objective into a task list (call once at goal start). Each task description must be a one-line summary (max 60 chars), no newlines or markdown" +
|
|
721
|
+
"\n- add_tasks: Append new tasks to the existing list (when omissions are discovered). Each task description must be a one-line summary (max 60 chars), no newlines or markdown" +
|
|
722
|
+
"\n- update_tasks: Batch update task statuses (completed requires evidence, cancelled does not block goal completion)" +
|
|
723
|
+
"\n- list_tasks: View progress and remaining budget" +
|
|
724
|
+
"\n- complete_goal: Mark the objective as achieved (all tasks must be completed + evidence)" +
|
|
725
|
+
"\n- cancel_goal: Cancel the current goal (use when user wants to exit/stop)" +
|
|
726
|
+
"\n- report_blocked: Report being blocked (use when encountering unsolvable issues)" +
|
|
727
|
+
"\n- add_subtasks: Add subtasks to a specified task (params: taskId, texts[]). Use this instead of todo tool in Goal mode" +
|
|
728
|
+
"\n- update_subtasks: Batch update subtask statuses (params: taskId, subUpdates[])" +
|
|
729
|
+
"\n- delete_subtasks: Delete subtasks from a specified task (params: taskId, subIds[])",
|
|
730
|
+
promptSnippet: "Manage task list, completion status, and exit for /goal mode",
|
|
731
731
|
promptGuidelines: [
|
|
732
|
-
"[
|
|
733
|
-
"[
|
|
734
|
-
"[
|
|
735
|
-
"[
|
|
736
|
-
"[
|
|
737
|
-
"[
|
|
738
|
-
"[
|
|
739
|
-
"[
|
|
740
|
-
"[
|
|
741
|
-
"[
|
|
742
|
-
"[
|
|
743
|
-
"[
|
|
744
|
-
"[
|
|
732
|
+
"[Workflow] After receiving the objective, the first step must be create_tasks to decompose. Do not re-call if task list already exists",
|
|
733
|
+
"[Format] Each task description must be a one-line summary, max 60 chars. No newlines, markdown, or detailed parameter lists — those go in execution phase. Example: 'Fix hook-registry dedup logic' not 'Fix hook-registry dedup + transport-execute enhancementConfig guard + failover-loop ...'",
|
|
734
|
+
"[Append] When discovering omissions during execution, use add_tasks to append — do not re-call create_tasks",
|
|
735
|
+
"[Completion] After completing a task, call update_tasks with status=completed and provide evidence (e.g. 'test X passed', 'file F created')",
|
|
736
|
+
"[Goal completion] Only call complete_goal when all tasks are completed with overall evidence",
|
|
737
|
+
"[Exit] When user says 'stop', 'exit', 'cancel', '不用了', '结束', etc. indicating they don't want to continue, immediately call cancel_goal — do not guide them through complete_goal",
|
|
738
|
+
"[Blocked] When encountering unsolvable technical issues, call report_blocked with the reason",
|
|
739
|
+
"[Progress] Use list_tasks anytime to check remaining tasks and budget",
|
|
740
|
+
"[Cancel] To cancel a task, use update_tasks with status=cancelled. Cancelled tasks do not block goal completion",
|
|
741
|
+
"[Forbidden] Do not mark tasks as completed without evidence, and do not call complete_goal without evidence",
|
|
742
|
+
"[Forbidden] Do not force task completion when the user explicitly wants to exit — call cancel_goal directly",
|
|
743
|
+
"[Forbidden] Do not re-call create_tasks to overwrite existing incomplete tasks — use add_tasks to append",
|
|
744
|
+
"[Subtask] For fine-grained step tracking in Goal mode, use add_subtasks — do not use the todo tool",
|
|
745
745
|
],
|
|
746
746
|
parameters: GoalManagerParams,
|
|
747
747
|
|
|
@@ -777,7 +777,7 @@ export default function goalExtension(pi: ExtensionAPI) {
|
|
|
777
777
|
}
|
|
778
778
|
const tasks = details.tasks;
|
|
779
779
|
const completed = tasks.filter((t) => t.status === "completed").length;
|
|
780
|
-
const summary = theme.fg("success", `✓ ${completed}/${tasks.length}
|
|
780
|
+
const summary = theme.fg("success", `✓ ${completed}/${tasks.length} completed`);
|
|
781
781
|
if (!expanded || tasks.length === 0) {
|
|
782
782
|
return new Text(summary, 0, 0);
|
|
783
783
|
}
|
|
@@ -816,7 +816,7 @@ export default function goalExtension(pi: ExtensionAPI) {
|
|
|
816
816
|
|
|
817
817
|
pi.registerCommand("goal", {
|
|
818
818
|
description:
|
|
819
|
-
"
|
|
819
|
+
"Goal-driven mode: /goal <objective> [--tokens N] [--timeout N] [--max-turns N] | /goal pause | /goal resume | /goal clear | /goal update <new-objective> | /goal status | /goal history",
|
|
820
820
|
handler: async (args: string | undefined, ctx: ExtensionCommandContext) => {
|
|
821
821
|
await handleGoalCommand(pi, session, args, ctx);
|
|
822
822
|
},
|
|
@@ -890,9 +890,9 @@ export default function goalExtension(pi: ExtensionAPI) {
|
|
|
890
890
|
pi.registerMessageRenderer(customType, (message: any, _options: any, theme: Theme) => {
|
|
891
891
|
const prefix =
|
|
892
892
|
message.customType === "goal-context-exceeded"
|
|
893
|
-
? theme.fg("error", "[GOAL
|
|
893
|
+
? theme.fg("error", "[GOAL Budget] ")
|
|
894
894
|
: message.customType === "goal-staleness-reminder"
|
|
895
|
-
? theme.fg("warning", "[GOAL
|
|
895
|
+
? theme.fg("warning", "[GOAL Reminder] ")
|
|
896
896
|
: theme.fg("accent", "[GOAL] ");
|
|
897
897
|
const content = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
|
|
898
898
|
return new Text(prefix + theme.fg("dim", content), 0, 0);
|
package/src/templates.ts
CHANGED
|
@@ -33,20 +33,20 @@ export function continuationPrompt(state: GoalRuntimeState): string {
|
|
|
33
33
|
|
|
34
34
|
// Budget info (single line, Codex style)
|
|
35
35
|
const budgetLine = formatBudgetLine(state);
|
|
36
|
-
const stallLine = state.stallCount > 0 ? `\nStall: ${state.stallCount}/${state.budget.maxStallTurns}
|
|
36
|
+
const stallLine = state.stallCount > 0 ? `\nStall: ${state.stallCount}/${state.budget.maxStallTurns} turns stalled` : "";
|
|
37
37
|
|
|
38
38
|
// Task summary (only IDs, not full descriptions — descriptions in before_agent_start)
|
|
39
39
|
const taskLine = total > 0
|
|
40
|
-
? `Tasks: ${completedCount}/${total}${incomplete.length > 0 ? ` (
|
|
41
|
-
: "Tasks:
|
|
40
|
+
? `Tasks: ${completedCount}/${total}${incomplete.length > 0 ? ` (remaining: ${incomplete.map(t => `#${t.id}`).join(",")})` : " ✓"}`
|
|
41
|
+
: "Tasks: Not created. Call create_tasks immediately.";
|
|
42
42
|
|
|
43
43
|
return (
|
|
44
44
|
`<goal_context>\n` +
|
|
45
45
|
`[GOAL] Turn ${state.turnCount}/${state.budget.maxTurns}${budgetLine}${stallLine}\n` +
|
|
46
46
|
`<objective>${objective}</objective>\n` +
|
|
47
47
|
`${taskLine}\n` +
|
|
48
|
-
`Rules: create_tasks→update_tasks(evidence)→complete_goal(evidence). blocked→report_blocked(reason). subtask: add_subtasks/update_subtasks (
|
|
49
|
-
`Audit:
|
|
48
|
+
`Rules: create_tasks→update_tasks(evidence)→complete_goal(evidence). blocked→report_blocked(reason). subtask: add_subtasks/update_subtasks (replaces todo tool).\n` +
|
|
49
|
+
`Audit: Verify each requirement has authoritative evidence. Do not mark completed due to budget exhaustion, do not mark blocked due to difficulty.\n` +
|
|
50
50
|
`</goal_context>`
|
|
51
51
|
);
|
|
52
52
|
}
|
|
@@ -62,24 +62,24 @@ export function budgetLimitPrompt(state: GoalRuntimeState, limitType: "token" |
|
|
|
62
62
|
const incomplete = getIncompleteTasks(state.tasks);
|
|
63
63
|
const incompleteSummary =
|
|
64
64
|
incomplete.length > 0
|
|
65
|
-
?
|
|
66
|
-
: "
|
|
65
|
+
? `Incomplete: ${incomplete.map((t) => `#${t.id}`).join(", ")}`
|
|
66
|
+
: "All tasks completed.";
|
|
67
67
|
|
|
68
68
|
return (
|
|
69
69
|
`<goal_context>\n` +
|
|
70
|
-
`[GOAL — ${limitType === "token" ? "TOKEN
|
|
70
|
+
`[GOAL — ${limitType === "token" ? "TOKEN budget" : "time budget"} almost exhausted]\n\n` +
|
|
71
71
|
`<objective>\n${objective}\n</objective>\n\n` +
|
|
72
|
-
|
|
72
|
+
`Current progress: ${completedCount}/${total} tasks completed\n` +
|
|
73
73
|
`${incompleteSummary}\n` +
|
|
74
74
|
(limitType === "token"
|
|
75
|
-
? `
|
|
76
|
-
:
|
|
77
|
-
`\n
|
|
78
|
-
`1.
|
|
79
|
-
`2.
|
|
80
|
-
`3.
|
|
81
|
-
`4.
|
|
82
|
-
|
|
75
|
+
? `Tokens used: ${state.tokensUsed} / ${state.budget.tokenBudget ?? "unknown"}\n`
|
|
76
|
+
: `Time elapsed: ${Math.floor(elapsed / SECONDS_PER_MINUTE)}m${Math.floor(elapsed % SECONDS_PER_MINUTE)}s / ${state.budget.timeBudgetMinutes ?? "unknown"} min\n`) +
|
|
77
|
+
`\nYou must wrap up immediately:\n` +
|
|
78
|
+
`1. Use goal_manager's list_tasks to check remaining tasks\n` +
|
|
79
|
+
`2. Only mark tasks you have genuinely completed with evidence\n` +
|
|
80
|
+
`3. If the objective is met, call goal_manager's complete_goal\n` +
|
|
81
|
+
`4. Summarize current progress and remaining work\n` +
|
|
82
|
+
`Do not start new tasks. Do not mark completed due to budget exhaustion.\n` +
|
|
83
83
|
`</goal_context>`
|
|
84
84
|
);
|
|
85
85
|
}
|
|
@@ -92,14 +92,14 @@ export function objectiveUpdatedPrompt(state: GoalRuntimeState, oldObjective: st
|
|
|
92
92
|
|
|
93
93
|
return (
|
|
94
94
|
`<goal_context>\n` +
|
|
95
|
-
`[GOAL —
|
|
96
|
-
|
|
95
|
+
`[GOAL — Objective updated]\n\n` +
|
|
96
|
+
`Previous objective: ${escapedOld}\n` +
|
|
97
97
|
`<untrusted_objective>\n${newObjective}\n</untrusted_objective>\n\n` +
|
|
98
|
-
|
|
99
|
-
`1.
|
|
100
|
-
`2.
|
|
101
|
-
`3.
|
|
102
|
-
`4.
|
|
98
|
+
`This new objective supersedes all prior objective context. You must:\n` +
|
|
99
|
+
`1. Immediately stop working toward the old objective\n` +
|
|
100
|
+
`2. Re-evaluate the task list — call goal_manager's create_tasks to re-decompose if needed\n` +
|
|
101
|
+
`3. Only continue old work if it also serves the new objective\n` +
|
|
102
|
+
`4. Proceed with the new objective\n` +
|
|
103
103
|
`</goal_context>`
|
|
104
104
|
);
|
|
105
105
|
}
|
|
@@ -114,17 +114,17 @@ export function contextInjectionPrompt(state: GoalRuntimeState): string {
|
|
|
114
114
|
|
|
115
115
|
return (
|
|
116
116
|
`<goal_context>\n` +
|
|
117
|
-
`[GOAL
|
|
117
|
+
`[GOAL mode activated]\n\n` +
|
|
118
118
|
`<objective>\n${objective}\n</objective>\n` +
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
`1.
|
|
124
|
-
`2.
|
|
125
|
-
`3.
|
|
126
|
-
`4.
|
|
127
|
-
`5. Goal
|
|
119
|
+
`Status: ${state.status}\n` +
|
|
120
|
+
`Turn: ${state.turnCount}/${state.budget.maxTurns}${budgetInfo}\n` +
|
|
121
|
+
`Task progress: ${completedCount}/${total}\n\n` +
|
|
122
|
+
`Strict rules:\n` +
|
|
123
|
+
`1. First step: call goal_manager's create_tasks to decompose tasks (if not yet created)\n` +
|
|
124
|
+
`2. After completing a task, call update_tasks with status=completed and provide evidence\n` +
|
|
125
|
+
`3. Only call complete_goal with concrete evidence\n` +
|
|
126
|
+
`4. If blocked, call report_blocked\n` +
|
|
127
|
+
`5. In Goal mode, do not use the todo tool — use add_subtasks / update_subtasks for fine-grained tracking\n` +
|
|
128
128
|
`</goal_context>`
|
|
129
129
|
);
|
|
130
130
|
}
|
|
@@ -144,22 +144,22 @@ export function stalenessReminderPrompt(
|
|
|
144
144
|
const lines: string[] = [];
|
|
145
145
|
|
|
146
146
|
lines.push("<goal_context>");
|
|
147
|
-
lines.push("[GOAL
|
|
147
|
+
lines.push("[GOAL reminder — tasks stalled]\n");
|
|
148
148
|
|
|
149
149
|
if (allTerminal) {
|
|
150
|
-
lines.push("
|
|
150
|
+
lines.push("All tasks completed but goal_manager is still open. Call complete_goal or cancel_goal.");
|
|
151
151
|
} else {
|
|
152
|
-
lines.push(
|
|
152
|
+
lines.push(`The following tasks have exceeded ${TASK_STALL_TURN_THRESHOLD} turns without update:\n`);
|
|
153
153
|
for (const item of staleTasks) {
|
|
154
|
-
lines.push(` #${item.task.id}: ${item.task.description} (${item.staleTurns}
|
|
154
|
+
lines.push(` #${item.task.id}: ${item.task.description} (${item.staleTurns} turns idle)`);
|
|
155
155
|
for (const s of item.staleSubtasks) {
|
|
156
|
-
lines.push(` - ${s.text} (${s.staleTurns}
|
|
156
|
+
lines.push(` - ${s.text} (${s.staleTurns} turns)`);
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
|
-
lines.push("\
|
|
159
|
+
lines.push("\nCheck these tasks — call update_tasks to report progress or cancel tasks that are no longer needed.");
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
lines.push(`\
|
|
162
|
+
lines.push(`\nObjective: ${objective}`);
|
|
163
163
|
lines.push(`Turn: ${state.turnCount}/${state.budget.maxTurns}`);
|
|
164
164
|
lines.push("</goal_context>");
|
|
165
165
|
|
|
@@ -177,7 +177,7 @@ function formatBudgetInfo(state: GoalRuntimeState): string {
|
|
|
177
177
|
if (state.budget.timeBudgetMinutes) {
|
|
178
178
|
const elapsed = getElapsedTimeSeconds(state);
|
|
179
179
|
const pct = Math.round((elapsed / (state.budget.timeBudgetMinutes * SECONDS_PER_MINUTE)) * PERCENT_FACTOR);
|
|
180
|
-
parts.push(
|
|
180
|
+
parts.push(`Time: ${pct}%`);
|
|
181
181
|
}
|
|
182
182
|
return parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
183
183
|
}
|
|
@@ -197,13 +197,13 @@ function formatBudgetLine(state: GoalRuntimeState): string {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
export function formatTaskList(tasks: GoalTask[]): string {
|
|
200
|
-
if (tasks.length === 0) return "
|
|
200
|
+
if (tasks.length === 0) return "No tasks yet.";
|
|
201
201
|
const completed = tasks.filter(t => t.status === "completed");
|
|
202
202
|
const active = tasks.filter(t => t.status === "in_progress" || t.status === "pending");
|
|
203
203
|
const cancelled = tasks.filter(t => t.status === "cancelled");
|
|
204
204
|
const lines: string[] = [];
|
|
205
205
|
if (active.length > 0) {
|
|
206
|
-
lines.push(
|
|
206
|
+
lines.push(`In progress / Pending (${active.length}):`);
|
|
207
207
|
for (const t of active) {
|
|
208
208
|
const icon = t.status === "in_progress" ? "●" : "☐";
|
|
209
209
|
lines.push(` ${icon} #${t.id}: ${t.description}`);
|
|
@@ -216,17 +216,17 @@ export function formatTaskList(tasks: GoalTask[]): string {
|
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
if (completed.length > 0) {
|
|
219
|
-
lines.push(
|
|
219
|
+
lines.push(`Completed (${completed.length}):`);
|
|
220
220
|
for (const t of completed) {
|
|
221
221
|
const evidence = t.evidence ? ` — ${t.evidence}` : "";
|
|
222
222
|
lines.push(` ✓ #${t.id}: ${t.description}${evidence}`);
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
if (cancelled.length > 0) {
|
|
226
|
-
lines.push(
|
|
226
|
+
lines.push(`Cancelled (${cancelled.length}):`);
|
|
227
227
|
for (const t of cancelled) lines.push(` ✗ #${t.id}: ${t.description}`);
|
|
228
228
|
}
|
|
229
|
-
const summary = `${completed.length}/${tasks.length}
|
|
229
|
+
const summary = `${completed.length}/${tasks.length} completed` + (cancelled.length > 0 ? `, ${cancelled.length} cancelled` : "");
|
|
230
230
|
lines.push(summary);
|
|
231
231
|
return lines.join("\n");
|
|
232
232
|
}
|
package/src/tool-handler.ts
CHANGED
|
@@ -61,19 +61,19 @@ export const GoalManagerParams = Type.Object({
|
|
|
61
61
|
"update_subtasks",
|
|
62
62
|
"delete_subtasks",
|
|
63
63
|
] as const),
|
|
64
|
-
tasks: Type.Optional(Type.Array(Type.String(), { description: "Task descriptions.
|
|
64
|
+
tasks: Type.Optional(Type.Array(Type.String(), { description: "Task descriptions. Each must be a one-line summary (max 60 chars), no newlines or markdown" })),
|
|
65
65
|
updates: Type.Optional(Type.Array(Type.Object({
|
|
66
66
|
taskId: Type.Number(),
|
|
67
67
|
status: StringEnum(GOAL_TASK_STATUSES),
|
|
68
68
|
evidence: Type.Optional(Type.String()),
|
|
69
69
|
}))),
|
|
70
|
-
taskId: Type.Optional(Type.Number({ description: "Task ID
|
|
71
|
-
texts: Type.Optional(Type.Array(Type.String(), { description: "Subtask
|
|
70
|
+
taskId: Type.Optional(Type.Number({ description: "Task ID (required for subtask operations)" })),
|
|
71
|
+
texts: Type.Optional(Type.Array(Type.String(), { description: "Subtask text list (for add_subtasks)" })),
|
|
72
72
|
subUpdates: Type.Optional(Type.Array(Type.Object({
|
|
73
73
|
subId: Type.Number(),
|
|
74
74
|
status: StringEnum(SUBTASK_STATUSES),
|
|
75
75
|
}))),
|
|
76
|
-
subIds: Type.Optional(Type.Array(Type.Number(), { description: "Subtask ID
|
|
76
|
+
subIds: Type.Optional(Type.Array(Type.Number(), { description: "Subtask ID list (for delete_subtasks)" })),
|
|
77
77
|
evidence: Type.Optional(Type.String({ description: "Evidence for completion (required for complete_goal)" })),
|
|
78
78
|
reason: Type.Optional(Type.String({ description: "Reason for being blocked (required for report_blocked)" })),
|
|
79
79
|
cancelReason: Type.Optional(Type.String({ description: "Why the user wants to cancel (required for cancel_goal)" })),
|
|
@@ -160,12 +160,12 @@ export function makeGoalResult(session: GoalSession, text: string) {
|
|
|
160
160
|
const budgetInfo: string[] = [];
|
|
161
161
|
if (state.budget.tokenBudget) {
|
|
162
162
|
const remaining = Math.max(state.budget.tokenBudget - state.tokensUsed, 0);
|
|
163
|
-
budgetInfo.push(`Token: ${state.tokensUsed}/${state.budget.tokenBudget} (
|
|
163
|
+
budgetInfo.push(`Token: ${state.tokensUsed}/${state.budget.tokenBudget} (${remaining} remaining)`);
|
|
164
164
|
}
|
|
165
165
|
if (state.budget.timeBudgetMinutes) {
|
|
166
166
|
const elapsed = getElapsedTimeSeconds(state);
|
|
167
167
|
const remaining = Math.max(state.budget.timeBudgetMinutes * SECONDS_PER_MINUTE - elapsed, 0);
|
|
168
|
-
budgetInfo.push(
|
|
168
|
+
budgetInfo.push(`Time: ${Math.floor(elapsed / SECONDS_PER_MINUTE)}m/${state.budget.timeBudgetMinutes}m (${Math.floor(remaining / SECONDS_PER_MINUTE)}m remaining)`);
|
|
169
169
|
}
|
|
170
170
|
const suffix = budgetInfo.length > 0 ? `\n\n[Budget] ${budgetInfo.join(" | ")}` : "";
|
|
171
171
|
return {
|
|
@@ -177,7 +177,7 @@ export function makeGoalResult(session: GoalSession, text: string) {
|
|
|
177
177
|
status: state.status,
|
|
178
178
|
_render: {
|
|
179
179
|
type: "task-list" as const,
|
|
180
|
-
summary: `${getCompletedCount(state.tasks)}/${state.tasks.length}
|
|
180
|
+
summary: `${getCompletedCount(state.tasks)}/${state.tasks.length} completed`,
|
|
181
181
|
data: {
|
|
182
182
|
items: state.tasks.map((t) => ({
|
|
183
183
|
id: t.id,
|
|
@@ -192,8 +192,8 @@ export function makeGoalResult(session: GoalSession, text: string) {
|
|
|
192
192
|
})),
|
|
193
193
|
meta: {
|
|
194
194
|
...(state.budget.tokenBudget ? { "Token": `${state.tokensUsed}/${state.budget.tokenBudget}` } : {}),
|
|
195
|
-
...(state.budget.timeBudgetMinutes ? { "
|
|
196
|
-
"
|
|
195
|
+
...(state.budget.timeBudgetMinutes ? { "Time": `${Math.floor(getElapsedTimeSeconds(state) / SECONDS_PER_MINUTE)}m/${state.budget.timeBudgetMinutes}m` } : {}),
|
|
196
|
+
"Turn": `${state.turnCount}/${state.budget.maxTurns}`,
|
|
197
197
|
},
|
|
198
198
|
},
|
|
199
199
|
},
|
|
@@ -222,7 +222,7 @@ export async function executeGoalAction(
|
|
|
222
222
|
) {
|
|
223
223
|
const state = session.state;
|
|
224
224
|
if (!state) {
|
|
225
|
-
throw new Error("Goal
|
|
225
|
+
throw new Error("Goal mode not active. Use /goal <objective> to start.");
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
switch (params.action) {
|
|
@@ -233,8 +233,7 @@ export async function executeGoalAction(
|
|
|
233
233
|
const existingIncomplete = getIncompleteTasks(state.tasks);
|
|
234
234
|
if (state.tasks.length > 0 && existingIncomplete.length > 0) {
|
|
235
235
|
throw new Error(
|
|
236
|
-
|
|
237
|
-
`如需追加任务请用 add_tasks,如需全部重新规划请用 /goal update。`,
|
|
236
|
+
`Already has ${state.tasks.length} tasks (${existingIncomplete.length} incomplete). Use add_tasks to append, or /goal update to re-plan.`,
|
|
238
237
|
);
|
|
239
238
|
}
|
|
240
239
|
state.tasks = params.tasks.map((desc: string, i: number) => ({
|
|
@@ -245,7 +244,7 @@ export async function executeGoalAction(
|
|
|
245
244
|
}));
|
|
246
245
|
persistGoalState(pi, session, ctx);
|
|
247
246
|
return makeGoalResult(session,
|
|
248
|
-
|
|
247
|
+
`Created ${state.tasks.length} tasks:\n${state.tasks.map((t) => ` #${t.id}: ${t.description}`).join("\n")}`,
|
|
249
248
|
);
|
|
250
249
|
}
|
|
251
250
|
|
|
@@ -265,7 +264,7 @@ export async function executeGoalAction(
|
|
|
265
264
|
state.tasks.push(...newTasks);
|
|
266
265
|
persistGoalState(pi, session, ctx);
|
|
267
266
|
return makeGoalResult(session,
|
|
268
|
-
|
|
267
|
+
`Appended ${newTasks.length} tasks:\n${newTasks.map((t) => ` #${t.id}: ${t.description}`).join("\n")}`,
|
|
269
268
|
);
|
|
270
269
|
}
|
|
271
270
|
|
|
@@ -276,7 +275,7 @@ export async function executeGoalAction(
|
|
|
276
275
|
const taskIds = params.updates.map((u: { taskId: number; status: string; evidence?: string }) => u.taskId);
|
|
277
276
|
const duplicateIds = taskIds.filter((id: number, i: number) => taskIds.indexOf(id) !== i);
|
|
278
277
|
if (duplicateIds.length > 0) {
|
|
279
|
-
throw new Error(
|
|
278
|
+
throw new Error(`Duplicate taskIds: ${[...new Set(duplicateIds)].join(", ")}`);
|
|
280
279
|
}
|
|
281
280
|
for (const u of params.updates) {
|
|
282
281
|
const task = state.tasks.find((t) => t.id === u.taskId);
|
|
@@ -284,10 +283,10 @@ export async function executeGoalAction(
|
|
|
284
283
|
throw new Error(`Task #${u.taskId} not found`);
|
|
285
284
|
}
|
|
286
285
|
if (isTerminalTaskStatus(task.status)) {
|
|
287
|
-
throw new Error(`Task #${task.id}
|
|
286
|
+
throw new Error(`Task #${task.id} already in terminal state (${task.status}), cannot be changed`);
|
|
288
287
|
}
|
|
289
288
|
if (u.status === "completed" && (!u.evidence || u.evidence.trim() === "")) {
|
|
290
|
-
throw new Error(`Task #${task.id}: completed
|
|
289
|
+
throw new Error(`Task #${task.id}: completed requires evidence`);
|
|
291
290
|
}
|
|
292
291
|
}
|
|
293
292
|
const results: string[] = [];
|
|
@@ -305,7 +304,7 @@ export async function executeGoalAction(
|
|
|
305
304
|
}
|
|
306
305
|
}
|
|
307
306
|
persistGoalState(pi, session, ctx);
|
|
308
|
-
return makeGoalResult(session,
|
|
307
|
+
return makeGoalResult(session, `Updated ${results.length} tasks:\n${results.join("\n")}`);
|
|
309
308
|
}
|
|
310
309
|
|
|
311
310
|
case "list_tasks": {
|
|
@@ -314,54 +313,53 @@ export async function executeGoalAction(
|
|
|
314
313
|
|
|
315
314
|
case "complete_goal": {
|
|
316
315
|
if (!params.evidence || params.evidence.trim() === "") {
|
|
317
|
-
throw new Error("complete_goal requires evidence —
|
|
316
|
+
throw new Error("complete_goal requires evidence — provide concrete proof that the objective has been achieved");
|
|
318
317
|
}
|
|
319
318
|
if (state.tasks.length === 0) {
|
|
320
|
-
throw new Error("
|
|
319
|
+
throw new Error("Create a task list with create_tasks before completing the goal.");
|
|
321
320
|
}
|
|
322
321
|
const incomplete = getIncompleteTasks(state.tasks);
|
|
323
322
|
if (incomplete.length > 0) {
|
|
324
323
|
throw new Error(
|
|
325
|
-
|
|
326
|
-
`请先完成这些任务或提供理由说明为什么它们不需要完成。`,
|
|
324
|
+
`${incomplete.length} tasks still incomplete: ${incomplete.map((t) => `#${t.id}`).join(", ")}. Complete them first or explain why they don't need completion.`,
|
|
327
325
|
);
|
|
328
326
|
}
|
|
329
327
|
const completedCount = getCompletedCount(state.tasks);
|
|
330
328
|
if (completedCount === 0) {
|
|
331
|
-
throw new Error("
|
|
329
|
+
throw new Error("At least one task must be completed. All-cancelled does not count.");
|
|
332
330
|
}
|
|
333
331
|
state.status = transitionStatus(state.status, "complete");
|
|
334
332
|
state.completedAtTurnIndex = state.currentTurnIndex;
|
|
335
333
|
writeGoalHistoryEntry(pi, session);
|
|
336
334
|
persistGoalState(pi, session, ctx);
|
|
337
335
|
const budgetReport: string[] = [];
|
|
338
|
-
budgetReport.push(
|
|
339
|
-
budgetReport.push(
|
|
336
|
+
budgetReport.push(`Total turns: ${state.turnCount}`);
|
|
337
|
+
budgetReport.push(`Tasks completed: ${getCompletedCount(state.tasks)}/${state.tasks.length}`);
|
|
340
338
|
if (state.budget.tokenBudget) {
|
|
341
|
-
budgetReport.push(`Token
|
|
339
|
+
budgetReport.push(`Token usage: ${state.tokensUsed}/${state.budget.tokenBudget}`);
|
|
342
340
|
}
|
|
343
341
|
const elapsed = getElapsedTimeSeconds(state);
|
|
344
|
-
budgetReport.push(
|
|
342
|
+
budgetReport.push(`Duration: ${Math.floor(elapsed / SECONDS_PER_MINUTE)}m${Math.floor(elapsed % SECONDS_PER_MINUTE)}s`);
|
|
345
343
|
return makeGoalResult(session,
|
|
346
|
-
|
|
344
|
+
`Objective completed!\nEvidence: ${params.evidence}\n\n--- Budget Report ---\n${budgetReport.join("\n")}`,
|
|
347
345
|
);
|
|
348
346
|
}
|
|
349
347
|
|
|
350
348
|
case "report_blocked": {
|
|
351
349
|
if (!params.reason || params.reason.trim() === "") {
|
|
352
|
-
throw new Error("report_blocked requires reason —
|
|
350
|
+
throw new Error("report_blocked requires reason — describe what is blocking you");
|
|
353
351
|
}
|
|
354
352
|
state.lastBlockerReason = params.reason;
|
|
355
353
|
state.status = transitionStatus(state.status, "blocked");
|
|
356
354
|
persistGoalState(pi, session, ctx);
|
|
357
|
-
return makeGoalResult(session,
|
|
355
|
+
return makeGoalResult(session, `Blocked reported. Reason: ${params.reason}`);
|
|
358
356
|
}
|
|
359
357
|
|
|
360
358
|
case "cancel_goal": {
|
|
361
359
|
if (isTerminalStatus(state.status)) {
|
|
362
|
-
throw new Error(`Goal
|
|
360
|
+
throw new Error(`Goal is already in terminal state (${state.status}).`);
|
|
363
361
|
}
|
|
364
|
-
const reason = params.cancelReason ?? "
|
|
362
|
+
const reason = params.cancelReason ?? "User requested cancellation";
|
|
365
363
|
const goalId = state.goalId;
|
|
366
364
|
state.status = "cancelled";
|
|
367
365
|
state.completedAtTurnIndex = state.currentTurnIndex;
|
|
@@ -369,7 +367,7 @@ export async function executeGoalAction(
|
|
|
369
367
|
persistGoalState(pi, session, ctx);
|
|
370
368
|
clearGoalSession(session, ctx);
|
|
371
369
|
return {
|
|
372
|
-
content: [{ type: "text" as const, text: `Goal
|
|
370
|
+
content: [{ type: "text" as const, text: `Goal cancelled: ${reason}` }],
|
|
373
371
|
details: {
|
|
374
372
|
action: "cancel",
|
|
375
373
|
tasks: [],
|
|
@@ -377,7 +375,7 @@ export async function executeGoalAction(
|
|
|
377
375
|
status: "cancelled",
|
|
378
376
|
_render: {
|
|
379
377
|
type: "task-list" as const,
|
|
380
|
-
summary: "
|
|
378
|
+
summary: "Cancelled",
|
|
381
379
|
data: { items: [], meta: {} },
|
|
382
380
|
},
|
|
383
381
|
} satisfies GoalManagerDetails,
|
|
@@ -396,13 +394,13 @@ export async function executeGoalAction(
|
|
|
396
394
|
throw new Error(`Task #${params.taskId} not found`);
|
|
397
395
|
}
|
|
398
396
|
if (isTerminalTaskStatus(parentTask.status)) {
|
|
399
|
-
throw new Error(`Task #${parentTask.id}
|
|
397
|
+
throw new Error(`Task #${parentTask.id} already in terminal state (${parentTask.status}), cannot add subtask`);
|
|
400
398
|
}
|
|
401
399
|
const subtasks = parentTask.subtasks ?? [];
|
|
402
400
|
const startId = subtasks.length > 0 ? Math.max(...subtasks.map((s) => s.id)) + 1 : 1;
|
|
403
401
|
const trimmed = params.texts.map((t: string) => t.trim()).filter((t: string) => t.length > 0);
|
|
404
402
|
if (trimmed.length === 0) {
|
|
405
|
-
throw new Error("texts
|
|
403
|
+
throw new Error("texts requires at least one non-empty string");
|
|
406
404
|
}
|
|
407
405
|
const newSubtasks: Subtask[] = trimmed.map((text: string, i: number) => ({
|
|
408
406
|
id: startId + i,
|
|
@@ -413,7 +411,7 @@ export async function executeGoalAction(
|
|
|
413
411
|
parentTask.subtasks = [...subtasks, ...newSubtasks];
|
|
414
412
|
persistGoalState(pi, session, ctx);
|
|
415
413
|
return makeGoalResult(session,
|
|
416
|
-
|
|
414
|
+
`Added ${newSubtasks.length} subtasks to Task #${parentTask.id}:\n` +
|
|
417
415
|
newSubtasks.map((s) => ` - #${parentTask.id}.${s.id}: ${s.text}`).join("\n"),
|
|
418
416
|
);
|
|
419
417
|
}
|
|
@@ -430,7 +428,7 @@ export async function executeGoalAction(
|
|
|
430
428
|
throw new Error(`Task #${params.taskId} not found`);
|
|
431
429
|
}
|
|
432
430
|
if (!targetTask.subtasks || targetTask.subtasks.length === 0) {
|
|
433
|
-
throw new Error(`Task #${params.taskId}
|
|
431
|
+
throw new Error(`Task #${params.taskId} has no subtasks`);
|
|
434
432
|
}
|
|
435
433
|
const results: string[] = [];
|
|
436
434
|
for (const u of params.subUpdates) {
|
|
@@ -439,7 +437,7 @@ export async function executeGoalAction(
|
|
|
439
437
|
throw new Error(`Subtask #${params.taskId}.${u.subId} not found`);
|
|
440
438
|
}
|
|
441
439
|
if (sub.status === "completed") {
|
|
442
|
-
throw new Error(`Subtask #${params.taskId}.${sub.id}
|
|
440
|
+
throw new Error(`Subtask #${params.taskId}.${sub.id} already completed, cannot be changed`);
|
|
443
441
|
}
|
|
444
442
|
const prev = sub.status;
|
|
445
443
|
sub.status = u.status;
|
|
@@ -448,7 +446,7 @@ export async function executeGoalAction(
|
|
|
448
446
|
}
|
|
449
447
|
persistGoalState(pi, session, ctx);
|
|
450
448
|
return makeGoalResult(session,
|
|
451
|
-
|
|
449
|
+
`Updated ${results.length} subtasks:\n${results.join("\n")}`,
|
|
452
450
|
);
|
|
453
451
|
}
|
|
454
452
|
|
|
@@ -464,7 +462,7 @@ export async function executeGoalAction(
|
|
|
464
462
|
throw new Error(`Task #${params.taskId} not found`);
|
|
465
463
|
}
|
|
466
464
|
if (!delTask.subtasks || delTask.subtasks.length === 0) {
|
|
467
|
-
throw new Error(`Task #${params.taskId}
|
|
465
|
+
throw new Error(`Task #${params.taskId} has no subtasks`);
|
|
468
466
|
}
|
|
469
467
|
const uniqueIds = [...new Set(params.subIds)];
|
|
470
468
|
const missing = uniqueIds.filter((id) => !delTask.subtasks!.some((s) => s.id === id));
|
|
@@ -477,7 +475,7 @@ export async function executeGoalAction(
|
|
|
477
475
|
}
|
|
478
476
|
persistGoalState(pi, session, ctx);
|
|
479
477
|
return makeGoalResult(session,
|
|
480
|
-
|
|
478
|
+
`Deleted ${uniqueIds.length} subtasks, Task #${params.taskId} has ${delTask.subtasks?.length ?? 0} remaining`,
|
|
481
479
|
);
|
|
482
480
|
}
|
|
483
481
|
|
package/src/widget.ts
CHANGED
|
@@ -42,10 +42,10 @@ export function renderStatusLine(state: GoalRuntimeState, th: ThemeLike): string
|
|
|
42
42
|
let text = th.fg("accent", `◆ Goal`) + th.fg("muted", ` ${state.turnCount}/${state.budget.maxTurns}`);
|
|
43
43
|
|
|
44
44
|
if (total > 0) {
|
|
45
|
-
text += th.fg("muted", ` | ${completedCount}/${total}
|
|
45
|
+
text += th.fg("muted", ` | ${completedCount}/${total} tasks`);
|
|
46
46
|
const cancelledCount = state.tasks.filter(t => t.status === "cancelled").length;
|
|
47
47
|
if (cancelledCount > 0) {
|
|
48
|
-
text += th.fg("dim", `, ${cancelledCount}
|
|
48
|
+
text += th.fg("dim", `, ${cancelledCount} cancelled`);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -60,25 +60,25 @@ export function renderStatusLine(state: GoalRuntimeState, th: ThemeLike): string
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
if (state.stallCount > 0) {
|
|
63
|
-
text += th.fg("warning", ` | ⚠ ${state.stallCount}
|
|
63
|
+
text += th.fg("warning", ` | ⚠ ${state.stallCount} turns stalled`);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// Status suffix
|
|
67
67
|
switch (state.status) {
|
|
68
68
|
case "paused":
|
|
69
|
-
text += th.fg("warning", " | ⏸
|
|
69
|
+
text += th.fg("warning", " | ⏸ Paused");
|
|
70
70
|
break;
|
|
71
71
|
case "blocked":
|
|
72
|
-
text += th.fg("error", " | ⊘
|
|
72
|
+
text += th.fg("error", " | ⊘ Blocked");
|
|
73
73
|
break;
|
|
74
74
|
case "complete":
|
|
75
|
-
text += th.fg("success", " | ✓
|
|
75
|
+
text += th.fg("success", " | ✓ Completed");
|
|
76
76
|
break;
|
|
77
77
|
case "budget_limited":
|
|
78
|
-
text += th.fg("error", " | ⊗ Token
|
|
78
|
+
text += th.fg("error", " | ⊗ Token budget exhausted");
|
|
79
79
|
break;
|
|
80
80
|
case "time_limited":
|
|
81
|
-
text += th.fg("error", " | ⏱
|
|
81
|
+
text += th.fg("error", " | ⏱ Time budget exhausted");
|
|
82
82
|
break;
|
|
83
83
|
}
|
|
84
84
|
|
|
@@ -96,19 +96,19 @@ export function renderTerminalStatusLine(state: GoalRuntimeState, th: ThemeLike)
|
|
|
96
96
|
// 状态后缀
|
|
97
97
|
switch (state.status) {
|
|
98
98
|
case "complete":
|
|
99
|
-
text += th.fg("success", " ✓
|
|
99
|
+
text += th.fg("success", " ✓ Completed");
|
|
100
100
|
break;
|
|
101
101
|
case "budget_limited":
|
|
102
|
-
text += th.fg("error", " ⊗ Token
|
|
102
|
+
text += th.fg("error", " ⊗ Token budget exhausted");
|
|
103
103
|
break;
|
|
104
104
|
case "time_limited":
|
|
105
|
-
text += th.fg("error", " ⏱
|
|
105
|
+
text += th.fg("error", " ⏱ Time budget exhausted");
|
|
106
106
|
break;
|
|
107
107
|
default:
|
|
108
108
|
break;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
text += th.fg("muted", ` | ${completedCount}/${total}
|
|
111
|
+
text += th.fg("muted", ` | ${completedCount}/${total} tasks`);
|
|
112
112
|
|
|
113
113
|
// 预算摘要
|
|
114
114
|
if (state.budget.tokenBudget && state.budget.tokenBudget > 0) {
|
|
@@ -137,11 +137,11 @@ export function renderWidgetLines(state: GoalRuntimeState, th: ThemeLike): strin
|
|
|
137
137
|
const objDisplay = objSingleLine.length > OBJECTIVE_DISPLAY_LIMIT
|
|
138
138
|
? objSingleLine.slice(0, OBJECTIVE_TRUNCATE_KEEP) + "..."
|
|
139
139
|
: objSingleLine;
|
|
140
|
-
lines.push(th.fg("dim",
|
|
140
|
+
lines.push(th.fg("dim", `Objective: ${objDisplay}`));
|
|
141
141
|
|
|
142
142
|
// Task list
|
|
143
143
|
if (total === 0) {
|
|
144
|
-
lines.push(th.fg("dim", "
|
|
144
|
+
lines.push(th.fg("dim", " Waiting for task list creation..."));
|
|
145
145
|
} else {
|
|
146
146
|
for (const t of state.tasks) {
|
|
147
147
|
const desc = toSingleLine(t.description);
|
|
@@ -178,7 +178,7 @@ export function renderWidgetLines(state: GoalRuntimeState, th: ThemeLike): strin
|
|
|
178
178
|
const pct = getTimeUsagePercent(state) / PERCENT_FACTOR;
|
|
179
179
|
const elapsed = getElapsedTimeSeconds(state);
|
|
180
180
|
const mins = Math.floor(elapsed / SECONDS_PER_MINUTE);
|
|
181
|
-
lines.push(`
|
|
181
|
+
lines.push(` Time: ${renderProgressBar(pct)} ${mins}/${state.budget.timeBudgetMinutes}min`);
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
return lines;
|