pi-goal-x 0.20.1 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/docs/agent-flow-design.md +3 -0
- package/docs/architecture.md +5 -3
- package/extensions/goal-record.ts +2 -2
- package/extensions/goal.ts +116 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,6 +84,7 @@ If the objective is already final and should start immediately, use:
|
|
|
84
84
|
/goal-status Show focused goal state
|
|
85
85
|
/goal-list List all open goals in .pi/goals/
|
|
86
86
|
/goal-focus Choose this session's focused goal
|
|
87
|
+
/goal-unfocus Stop this session's goal work without modifying the shared goal
|
|
87
88
|
/goal-tweak <change> Draft a revision to the focused active/paused goal
|
|
88
89
|
/goal-pause Pause the focused active goal
|
|
89
90
|
/goal-resume Resume a paused goal
|
|
@@ -104,7 +105,7 @@ Pressing `Esc` or aborting an active run pauses the goal so it does not remain f
|
|
|
104
105
|
- **Branch-local focus**: because focus is reconstructed from the current session branch, `/tree` navigation can restore a different focus for a different branch.
|
|
105
106
|
- **One continuation chain**: auto-continue only schedules work for the focused goal in the current session.
|
|
106
107
|
|
|
107
|
-
Creating a goal with `/goals`, `/sisyphus`, `/goals-set`, or `/sisyphus-set` no longer clears other open goals. It creates a new active goal file and focuses it. Use `/goal-list` to inspect open goals
|
|
108
|
+
Creating a goal with `/goals`, `/sisyphus`, `/goals-set`, or `/sisyphus-set` no longer clears other open goals. It creates a new active goal file and focuses it. Use `/goal-list` to inspect open goals, `/goal-focus` to switch the session focus, and `/goal-unfocus` to detach the current session without pausing, modifying, archiving, or recording a project-ledger focus change for the shared goal. Unfocus also aborts in-flight work and audits owned by that session; asynchronous lifecycle results are discarded if focus changed while they were pending. If the latest focus entry explicitly clears focus, or points at a missing/stale goal, a remaining single open goal is not auto-focused and resume does not prompt to replace that explicit choice. By default (`autoSelectSingleGoal: false`) sessions start unfocused so focus stays session-scoped — useful when multiple sessions share the same `.pi/goals/` directory. Set `autoSelectSingleGoal: true` to restore the old behavior where a single open goal is auto-focused when no focus entry exists at all. If multiple open goals exist and the session has no valid focus, `/goal-resume`, `/goal-clear`, `/goal-abort`, `/goal-pause`, and `/goal-tweak` ask the user to choose a goal instead of acting on all of them.
|
|
108
109
|
|
|
109
110
|
## Agent tools
|
|
110
111
|
|
|
@@ -77,6 +77,8 @@ let focusedGoalId: string | null = null;
|
|
|
77
77
|
| `goalWorkToolCalledThisTurn` | empty-turn guard:只有本 turn 做了有意义工作才继续 auto-continue。 |
|
|
78
78
|
| `turnStoppedFor` | pause/abort/complete/tweak 后阻止同一 turn 继续乱调用工具。 |
|
|
79
79
|
| `postCompactReminderPending` | session compact 后给下一轮 agent 注入 deterministic resync prompt。 |
|
|
80
|
+
| `focusRevision` | session focus 改变时递增;异步 completion/tweak/task proposal 在写入前必须验证 revision,避免 unfocus 后迟到结果修改共享 goal。 |
|
|
81
|
+
| `hasExplicitSessionFocus` | 区分“从未选择 focus”和显式 null focus,保证 resume/tree 不覆盖 `/goal-unfocus`。 |
|
|
80
82
|
| `activeGetGoalTurnsByGoalId` | 统计重复 `get_goal`,用于 soft nudge,不是 hard block。 |
|
|
81
83
|
|
|
82
84
|
## 4. 持久化:Goal 文件与 Ledger
|
|
@@ -140,6 +142,7 @@ ledger append 是 best-effort:写入失败不应该让用户的生命周期动
|
|
|
140
142
|
| `/sisyphus-set <objective>` | 直接创建并启动 Sisyphus goal,不进入 draft discussion。 |
|
|
141
143
|
| `/goal-list` | 列出 `.pi/goals/` 下所有 open goals。 |
|
|
142
144
|
| `/goal-focus` | 让用户选择当前 session focus。 |
|
|
145
|
+
| `/goal-unfocus` | 清除当前 session focus,终止该 session 的 continuation/in-flight work/audit,并用 revision gate 丢弃迟到异步结果;不暂停、修改、归档共享 goal,也不写 project-global focus event。 |
|
|
143
146
|
| `/goal-status` / `/goal` | 显示 focused goal 状态和其他 open goals 提示。 |
|
|
144
147
|
| `/goal-tweak <change>` | 修改 focused goal,但也要先走 tweak drafting。 |
|
|
145
148
|
| `/goal-pause` | 用户暂停 focused active goal。 |
|
package/docs/architecture.md
CHANGED
|
@@ -68,6 +68,7 @@ Reusable logic is split into smaller modules:
|
|
|
68
68
|
├─ multiple open goals
|
|
69
69
|
│ ├─ /goal-list shows the project goal pool
|
|
70
70
|
│ ├─ /goal-focus chooses the session focus
|
|
71
|
+
│ ├─ /goal-unfocus clears only the session focus and leaves the shared goal open
|
|
71
72
|
│ └─ unfocused sessions guide the user to choose instead of letting the agent decide
|
|
72
73
|
│
|
|
73
74
|
└─ /goal-clear or /goal-abort archives the focused goal or cancels drafting
|
|
@@ -89,7 +90,7 @@ Session focus is separate. Focus changes append a custom session entry:
|
|
|
89
90
|
{
|
|
90
91
|
version: 1,
|
|
91
92
|
focusedGoalId: string | null,
|
|
92
|
-
reason: "created" | "selected" | "resumed" | "completed" | "cleared" | "aborted" | "migrated"
|
|
93
|
+
reason: "created" | "selected" | "unfocused" | "resumed" | "completed" | "cleared" | "aborted" | "migrated"
|
|
93
94
|
}
|
|
94
95
|
```
|
|
95
96
|
|
|
@@ -98,8 +99,8 @@ Because this is stored with `pi.appendEntry("pi-goal-focus", ...)`, it is sessio
|
|
|
98
99
|
1. Use a valid focused id from the latest focus entry.
|
|
99
100
|
2. If the latest focus entry explicitly has `focusedGoalId: null`, or points at a missing/stale goal, remain unfocused.
|
|
100
101
|
3. If no focus entry exists, merge a compatible legacy `pi-goal-state { version: 3, goal }` goal and focus it. If disk already has the same id, the disk record wins and the legacy session record only supplies focus.
|
|
101
|
-
4. If no focus entry exists and
|
|
102
|
-
5.
|
|
102
|
+
4. If no focus entry exists and `autoSelectSingleGoal` is enabled, auto-focus the sole open goal for compatibility. The default is disabled.
|
|
103
|
+
5. Otherwise remain unfocused until the user explicitly selects a goal. `/goal-unfocus` appends a null focus entry so the current session stays detached without modifying the shared goal or appending a project-global focus event. Resume and tree reconstruction preserve that explicit null focus.
|
|
103
104
|
|
|
104
105
|
Focus is human-owned. No agent tool can switch focus. Lifecycle tools operate only on the focused goal.
|
|
105
106
|
|
|
@@ -137,6 +138,7 @@ A deprecated optional `draftId` parameter is accepted for compatibility but igno
|
|
|
137
138
|
- `/goals-set` and `/sisyphus-set` directly create and focus a new open goal from the supplied objective.
|
|
138
139
|
- `/goal-list` prints all open goals with id, status, mode, usage, objective title, path, and a focus marker.
|
|
139
140
|
- `/goal-focus` uses `ctx.ui.select` when multiple goals are open and updates only session focus.
|
|
141
|
+
- `/goal-unfocus` writes a null session focus entry, clears continuation/runtime state, aborts in-flight work and audits for that session, and leaves the shared active goal file and project-global focus ledger unchanged. Focus revision tokens prevent pending completion, tweak, and task-list confirmation results from mutating a goal after detachment.
|
|
140
142
|
- `/goal-status` and `/goal` show the focused goal plus an `other open goals` hint.
|
|
141
143
|
- `/goal-resume` resumes the focused paused goal; when unfocused with multiple open goals, it asks the user to choose. Choosing an already active goal only focuses it.
|
|
142
144
|
- `/goal-clear` and `/goal-abort` archive only the focused/selected goal and never clear the whole pool at once.
|
|
@@ -2,7 +2,7 @@ export type GoalStatus = "active" | "paused" | "complete";
|
|
|
2
2
|
export type StopReason = "user" | "agent";
|
|
3
3
|
export type GoalEventKind = "checkpoint" | "stale" | "drafting";
|
|
4
4
|
export type DraftingFocus = "goal" | "sisyphus";
|
|
5
|
-
export type GoalFocusReason = "created" | "selected" | "resumed" | "completed" | "cleared" | "aborted" | "migrated";
|
|
5
|
+
export type GoalFocusReason = "created" | "selected" | "unfocused" | "resumed" | "completed" | "cleared" | "aborted" | "migrated";
|
|
6
6
|
|
|
7
7
|
export type TaskStatus = "pending" | "complete" | "skipped";
|
|
8
8
|
|
|
@@ -146,7 +146,7 @@ export function normalizeGoalFocusEntry(value: unknown): GoalFocusEntry | null {
|
|
|
146
146
|
? safeIdPart(raw.focusedGoalId)
|
|
147
147
|
: null;
|
|
148
148
|
const reason: GoalFocusReason =
|
|
149
|
-
raw.reason === "created" || raw.reason === "selected" || raw.reason === "resumed" || raw.reason === "completed" || raw.reason === "cleared" || raw.reason === "aborted" || raw.reason === "migrated"
|
|
149
|
+
raw.reason === "created" || raw.reason === "selected" || raw.reason === "unfocused" || raw.reason === "resumed" || raw.reason === "completed" || raw.reason === "cleared" || raw.reason === "aborted" || raw.reason === "migrated"
|
|
150
150
|
? raw.reason
|
|
151
151
|
: "selected";
|
|
152
152
|
return { version: 1, focusedGoalId, reason };
|
package/extensions/goal.ts
CHANGED
|
@@ -388,9 +388,39 @@ function isMeaningfulProgressToolCall(toolName: string, args: unknown): boolean
|
|
|
388
388
|
|
|
389
389
|
// ---------- extension entry point ----------
|
|
390
390
|
|
|
391
|
-
export default function goalExtension(
|
|
391
|
+
export default function goalExtension(
|
|
392
|
+
pi: ExtensionAPI,
|
|
393
|
+
dependencies: { runCompletionAuditor?: typeof runGoalCompletionAuditor } = {},
|
|
394
|
+
): void {
|
|
392
395
|
let goalsById = new Map<string, GoalRecord>();
|
|
393
396
|
let focusedGoalId: string | null = null;
|
|
397
|
+
let focusRevision = 0;
|
|
398
|
+
let hasExplicitSessionFocus = false;
|
|
399
|
+
|
|
400
|
+
function assignFocusedGoalId(next: string | null): void {
|
|
401
|
+
if (focusedGoalId !== next) focusRevision += 1;
|
|
402
|
+
focusedGoalId = next;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function focusedOperationToken(goalId: string): { goalId: string; revision: number } {
|
|
406
|
+
return { goalId, revision: focusRevision };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function isFocusedOperationCurrent(token: { goalId: string; revision: number }): boolean {
|
|
410
|
+
return focusedGoalId === token.goalId && focusRevision === token.revision;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function focusedOperationCancelledResult(action: string, token: { goalId: string; revision: number }) {
|
|
414
|
+
return {
|
|
415
|
+
content: [{
|
|
416
|
+
type: "text" as const,
|
|
417
|
+
text: `${action} cancelled because goal ${token.goalId} is no longer focused in this session. The shared goal was not modified.`,
|
|
418
|
+
}],
|
|
419
|
+
details: goalDetails(state.goal),
|
|
420
|
+
terminate: true,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
394
424
|
const state = {
|
|
395
425
|
get goal(): GoalRecord | null {
|
|
396
426
|
return focusedGoalFromPool(goalsById, focusedGoalId);
|
|
@@ -398,11 +428,11 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
398
428
|
set goal(next: GoalRecord | null) {
|
|
399
429
|
if (next) {
|
|
400
430
|
goalsById.set(next.id, next);
|
|
401
|
-
|
|
431
|
+
assignFocusedGoalId(next.id);
|
|
402
432
|
return;
|
|
403
433
|
}
|
|
404
434
|
if (focusedGoalId) goalsById.delete(focusedGoalId);
|
|
405
|
-
|
|
435
|
+
assignFocusedGoalId(null);
|
|
406
436
|
},
|
|
407
437
|
};
|
|
408
438
|
let continuationQueuedFor: string | null = null;
|
|
@@ -602,11 +632,11 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
602
632
|
if (current && !current.activePath) {
|
|
603
633
|
goalsById = fresh;
|
|
604
634
|
goalsById.set(current.id, current);
|
|
605
|
-
|
|
635
|
+
assignFocusedGoalId(current.id);
|
|
606
636
|
return true;
|
|
607
637
|
}
|
|
608
638
|
goalsById = fresh;
|
|
609
|
-
|
|
639
|
+
assignFocusedGoalId(null);
|
|
610
640
|
clearStoppedRuntimeState();
|
|
611
641
|
if (current) resetGetGoalNudgeState(current.id);
|
|
612
642
|
if (tweakDraftingFor !== null) tweakDraftingFor = null;
|
|
@@ -619,19 +649,25 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
619
649
|
: diskGoal;
|
|
620
650
|
goalsById = fresh;
|
|
621
651
|
goalsById.set(reconciled.id, reconciled);
|
|
622
|
-
|
|
652
|
+
assignFocusedGoalId(reconciled.id);
|
|
623
653
|
if (reconciled.status !== "active" || !reconciled.autoContinue) clearContinuationState();
|
|
624
654
|
if (reconciled.status !== "active") clearActiveAccounting();
|
|
625
655
|
return true;
|
|
626
656
|
}
|
|
627
657
|
|
|
628
658
|
function appendFocusEntry(goalId: string | null, reason: GoalFocusReason): void {
|
|
659
|
+
hasExplicitSessionFocus = true;
|
|
629
660
|
pi.appendEntry(FOCUS_ENTRY, goalFocusDetails(goalId, reason));
|
|
630
661
|
}
|
|
631
662
|
|
|
632
|
-
function setFocusedGoalId(
|
|
663
|
+
function setFocusedGoalId(
|
|
664
|
+
goalId: string | null,
|
|
665
|
+
ctx: ExtensionContext,
|
|
666
|
+
reason: GoalFocusReason,
|
|
667
|
+
opts: { recordLedger?: boolean } = {},
|
|
668
|
+
): void {
|
|
633
669
|
const previousGoalId = focusedGoalId;
|
|
634
|
-
|
|
670
|
+
assignFocusedGoalId(goalId && goalsById.has(goalId) ? goalId : null);
|
|
635
671
|
if (previousGoalId !== focusedGoalId) {
|
|
636
672
|
clearContinuationState();
|
|
637
673
|
clearActiveAccounting();
|
|
@@ -642,9 +678,9 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
642
678
|
appendFocusEntry(focusedGoalId, reason);
|
|
643
679
|
// Append ledger event for focus changes
|
|
644
680
|
try {
|
|
645
|
-
if (focusedGoalId) {
|
|
681
|
+
if (opts.recordLedger !== false && focusedGoalId) {
|
|
646
682
|
appendGoalEvent(ctx, { type: "goal_focused", goalId: focusedGoalId, reason, at: nowIso() });
|
|
647
|
-
} else if (previousGoalId) {
|
|
683
|
+
} else if (opts.recordLedger !== false && previousGoalId) {
|
|
648
684
|
appendGoalEvent(ctx, { type: "goal_unfocused", reason, at: nowIso() });
|
|
649
685
|
}
|
|
650
686
|
} catch {
|
|
@@ -657,7 +693,7 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
657
693
|
function updateFocusedGoal(next: GoalRecord, ctx: ExtensionContext, shouldPersist = true): void {
|
|
658
694
|
const previousGoalId = focusedGoalId;
|
|
659
695
|
goalsById.set(next.id, next);
|
|
660
|
-
|
|
696
|
+
assignFocusedGoalId(next.id);
|
|
661
697
|
if (previousGoalId !== focusedGoalId) {
|
|
662
698
|
resetGetGoalNudgeState(previousGoalId);
|
|
663
699
|
resetGetGoalNudgeState(focusedGoalId);
|
|
@@ -675,7 +711,7 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
675
711
|
function removeFocusedGoal(ctx: ExtensionContext, reason: GoalFocusReason): void {
|
|
676
712
|
const previousGoalId = focusedGoalId;
|
|
677
713
|
if (focusedGoalId) goalsById.delete(focusedGoalId);
|
|
678
|
-
|
|
714
|
+
assignFocusedGoalId(null);
|
|
679
715
|
clearStoppedRuntimeState();
|
|
680
716
|
resetGetGoalNudgeState(previousGoalId);
|
|
681
717
|
appendFocusEntry(null, reason);
|
|
@@ -859,7 +895,9 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
859
895
|
|
|
860
896
|
function loadState(ctx: ExtensionContext): void {
|
|
861
897
|
goalsById = readActiveGoalPool(ctx);
|
|
862
|
-
|
|
898
|
+
focusRevision += 1; // Session reload/tree navigation invalidates pending async focus operations.
|
|
899
|
+
assignFocusedGoalId(null);
|
|
900
|
+
hasExplicitSessionFocus = false;
|
|
863
901
|
let focusEntry: GoalFocusEntry | null = null;
|
|
864
902
|
let legacyGoal: GoalRecord | null = null;
|
|
865
903
|
let legacyStateSeen = false;
|
|
@@ -880,7 +918,8 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
880
918
|
legacyGoal = sanitizeGoalPaths(ctx, mergeGoalPromptFromDisk(ctx, legacyGoal));
|
|
881
919
|
}
|
|
882
920
|
const settings = loadGoalSettings(ctx.cwd);
|
|
883
|
-
|
|
921
|
+
hasExplicitSessionFocus = focusEntry !== null;
|
|
922
|
+
assignFocusedGoalId(resolveSessionFocus({ pool: goalsById, focusEntry, legacyGoal, autoSelectSingleGoal: settings.autoSelectSingleGoal }));
|
|
884
923
|
if (!focusEntry && focusedGoalId) {
|
|
885
924
|
try {
|
|
886
925
|
appendFocusEntry(focusedGoalId, legacyGoal?.id === focusedGoalId ? "migrated" : "selected");
|
|
@@ -1038,7 +1077,7 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
1038
1077
|
state.goal = null;
|
|
1039
1078
|
if (focusedGoalId === prevId) {
|
|
1040
1079
|
goalsById.delete(prevId);
|
|
1041
|
-
|
|
1080
|
+
assignFocusedGoalId(null);
|
|
1042
1081
|
}
|
|
1043
1082
|
clearStoppedRuntimeState();
|
|
1044
1083
|
syncGoalTools();
|
|
@@ -1503,6 +1542,34 @@ Verification contract:
|
|
|
1503
1542
|
ctx.ui.notify(`Focused goal: ${oneLineSummary(state.goal)}`, "info");
|
|
1504
1543
|
}
|
|
1505
1544
|
|
|
1545
|
+
function unfocusGoalCommand(ctx: ExtensionContext): void {
|
|
1546
|
+
const runtimeGoalId = state.goal?.id ?? runningGoalId ?? checkpointGoalId;
|
|
1547
|
+
reconcileFocusedGoalFromDisk(ctx);
|
|
1548
|
+
const current = state.goal;
|
|
1549
|
+
const detachedGoalId = current?.id ?? runtimeGoalId;
|
|
1550
|
+
let wasBusy = false;
|
|
1551
|
+
try {
|
|
1552
|
+
wasBusy = !ctx.isIdle();
|
|
1553
|
+
} catch {}
|
|
1554
|
+
if (detachedGoalId && wasBusy) turnStoppedFor = { goalId: detachedGoalId, turnSeq };
|
|
1555
|
+
setFocusedGoalId(null, ctx, "unfocused", { recordLedger: false });
|
|
1556
|
+
runningGoalId = null;
|
|
1557
|
+
checkpointGoalId = null;
|
|
1558
|
+
postCompactReminderPending = false;
|
|
1559
|
+
if (auditAbortController) auditAbortController.abort();
|
|
1560
|
+
if (detachedGoalId && wasBusy) {
|
|
1561
|
+
try {
|
|
1562
|
+
ctx.abort?.();
|
|
1563
|
+
} catch {}
|
|
1564
|
+
}
|
|
1565
|
+
if (!current) {
|
|
1566
|
+
const openCount = openGoals().length;
|
|
1567
|
+
ctx.ui.notify(openCount > 0 ? buildUnfocusedOpenGoalsSummary(openCount) : detailedSummary(null), "info");
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
ctx.ui.notify(`Goal unfocused for this session. It remains open in .pi/goals: ${current.id}`, "info");
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1506
1573
|
async function handleGoalCommandTopic(rawTopic: string, ctx: ExtensionContext, focus: DraftingFocus, opts: { replace: boolean }): Promise<void> {
|
|
1507
1574
|
const topic = rawTopic.trim();
|
|
1508
1575
|
if (opts.replace) {
|
|
@@ -1761,7 +1828,7 @@ Verification contract:
|
|
|
1761
1828
|
},
|
|
1762
1829
|
};
|
|
1763
1830
|
pi.registerCommand("goal", {
|
|
1764
|
-
description: "Show focused goal status. Discuss with /goals or /sisyphus; direct-start with /goals-set or /sisyphus-set; manage with /goal-list, /goal-focus, /goal-settings, /goal-tweak, /goal-clear, /goal-abort, /goal-pause, /goal-resume.",
|
|
1831
|
+
description: "Show focused goal status. Discuss with /goals or /sisyphus; direct-start with /goals-set or /sisyphus-set; manage with /goal-list, /goal-focus, /goal-unfocus, /goal-settings, /goal-tweak, /goal-clear, /goal-abort, /goal-pause, /goal-resume.",
|
|
1765
1832
|
handler: statusCommand.handler,
|
|
1766
1833
|
});
|
|
1767
1834
|
pi.registerCommand("goal-status", statusCommand);
|
|
@@ -1779,6 +1846,12 @@ Verification contract:
|
|
|
1779
1846
|
await focusGoalCommand(ctx);
|
|
1780
1847
|
},
|
|
1781
1848
|
});
|
|
1849
|
+
pi.registerCommand("goal-unfocus", {
|
|
1850
|
+
description: "Stop focusing the current goal in this session without modifying or archiving the shared goal.",
|
|
1851
|
+
handler: async (_rawArgs, ctx) => {
|
|
1852
|
+
unfocusGoalCommand(ctx);
|
|
1853
|
+
},
|
|
1854
|
+
});
|
|
1782
1855
|
pi.registerCommand("goal-settings", {
|
|
1783
1856
|
description: "Open pi-goal settings, including auditor provider/model/thinking_level.",
|
|
1784
1857
|
handler: async (_rawArgs, ctx) => {
|
|
@@ -2240,6 +2313,7 @@ ${objective}` : objective,
|
|
|
2240
2313
|
});
|
|
2241
2314
|
|
|
2242
2315
|
const headless = shouldAutoConfirmProposal({ hasUI: ctx.hasUI, autoConfirmEnv: process.env.PI_GOAL_AUTO_CONFIRM });
|
|
2316
|
+
const tweakFocus = focusedOperationToken(state.goal.id);
|
|
2243
2317
|
|
|
2244
2318
|
let decision: { decision: "confirm" | "continue"; auditorEnabled: boolean };
|
|
2245
2319
|
if (headless) {
|
|
@@ -2256,6 +2330,9 @@ ${objective}` : objective,
|
|
|
2256
2330
|
};
|
|
2257
2331
|
}
|
|
2258
2332
|
}
|
|
2333
|
+
if (!isFocusedOperationCurrent(tweakFocus)) {
|
|
2334
|
+
return focusedOperationCancelledResult("Goal tweak", tweakFocus);
|
|
2335
|
+
}
|
|
2259
2336
|
|
|
2260
2337
|
if (decision.decision === "confirm") {
|
|
2261
2338
|
// Persist any auditor toggle change
|
|
@@ -2418,6 +2495,7 @@ ${objective}` : objective,
|
|
|
2418
2495
|
}
|
|
2419
2496
|
|
|
2420
2497
|
const auditTarget = mergeGoalPromptFromDisk(ctx, state.goal);
|
|
2498
|
+
const completionFocus = focusedOperationToken(auditTarget.id);
|
|
2421
2499
|
// Append ledger: completion requested
|
|
2422
2500
|
try {
|
|
2423
2501
|
appendGoalEvent(ctx, {
|
|
@@ -2562,6 +2640,9 @@ ${objective}` : objective,
|
|
|
2562
2640
|
display: true,
|
|
2563
2641
|
details: { phase: "started", goalId: auditTarget.id, auditor: auditorLabel },
|
|
2564
2642
|
}, { triggerTurn: true });
|
|
2643
|
+
if (!isFocusedOperationCurrent(completionFocus)) {
|
|
2644
|
+
return focusedOperationCancelledResult("Goal completion", completionFocus);
|
|
2645
|
+
}
|
|
2565
2646
|
// Append ledger: audit started
|
|
2566
2647
|
try {
|
|
2567
2648
|
appendGoalEvent(ctx, {
|
|
@@ -2596,16 +2677,17 @@ ${objective}` : objective,
|
|
|
2596
2677
|
|
|
2597
2678
|
// Create a dedicated AbortController for the audit so it can be interrupted via Escape
|
|
2598
2679
|
auditAbortController?.abort(); // Clean up any stale controller
|
|
2599
|
-
|
|
2680
|
+
const completionAuditController = new AbortController();
|
|
2681
|
+
auditAbortController = completionAuditController;
|
|
2600
2682
|
|
|
2601
|
-
const auditor = await runGoalCompletionAuditor({
|
|
2683
|
+
const auditor = await (dependencies.runCompletionAuditor ?? runGoalCompletionAuditor)({
|
|
2602
2684
|
ctx,
|
|
2603
2685
|
goal: auditTarget,
|
|
2604
2686
|
completionSummary: params.completionSummary,
|
|
2605
2687
|
detailedSummary: detailedSummary(auditTarget),
|
|
2606
2688
|
verificationSummary: params.verificationSummary,
|
|
2607
2689
|
settings: loadGoalSettings(ctx.cwd),
|
|
2608
|
-
signal:
|
|
2690
|
+
signal: completionAuditController.signal,
|
|
2609
2691
|
onProgress: (progress) => {
|
|
2610
2692
|
auditProgress = {
|
|
2611
2693
|
...progress,
|
|
@@ -2615,9 +2697,14 @@ ${objective}` : objective,
|
|
|
2615
2697
|
},
|
|
2616
2698
|
});
|
|
2617
2699
|
// Clear abort controller — audit finished on its own
|
|
2618
|
-
auditAbortController = null;
|
|
2700
|
+
if (auditAbortController === completionAuditController) auditAbortController = null;
|
|
2619
2701
|
// Clear auditor progress display
|
|
2620
2702
|
stopAuditAnimation();
|
|
2703
|
+
if (!isFocusedOperationCurrent(completionFocus)) {
|
|
2704
|
+
auditProgress = null;
|
|
2705
|
+
goalWidgetComponent?.invalidate();
|
|
2706
|
+
return focusedOperationCancelledResult("Goal completion", completionFocus);
|
|
2707
|
+
}
|
|
2621
2708
|
|
|
2622
2709
|
// If the audit was aborted by the user (Esc), show a TUI dialog letting
|
|
2623
2710
|
// the user choose: mark complete without audit, or continue working.
|
|
@@ -2629,6 +2716,9 @@ ${objective}` : objective,
|
|
|
2629
2716
|
showingEscapeDialog = true;
|
|
2630
2717
|
const userChoice: EscapeDialogResult = await showEscapeDialog(ctx, auditTarget.objective);
|
|
2631
2718
|
showingEscapeDialog = false;
|
|
2719
|
+
if (!isFocusedOperationCurrent(completionFocus)) {
|
|
2720
|
+
return focusedOperationCancelledResult("Goal completion", completionFocus);
|
|
2721
|
+
}
|
|
2632
2722
|
|
|
2633
2723
|
if (userChoice === "complete_without_audit") {
|
|
2634
2724
|
// ── Mark complete without audit ────────────────────────────
|
|
@@ -3053,8 +3143,12 @@ ${objective}` : objective,
|
|
|
3053
3143
|
const taskLines = renderTaskLines(taskList.tasks);
|
|
3054
3144
|
const gateLabel = blockCompletion ? " (blockCompletion enabled)" : "";
|
|
3055
3145
|
const proposalText = [`Proposed task list${gateLabel}:`, "", ...taskLines].join("\n");
|
|
3146
|
+
const taskListFocus = focusedOperationToken(state.goal.id);
|
|
3056
3147
|
|
|
3057
3148
|
const dialogResult = await showProposalDialog(ctx, proposalText, "goal", !state.goal?.skipAuditor);
|
|
3149
|
+
if (!isFocusedOperationCurrent(taskListFocus)) {
|
|
3150
|
+
return focusedOperationCancelledResult("Task list proposal", taskListFocus);
|
|
3151
|
+
}
|
|
3058
3152
|
if (dialogResult.decision !== "confirm") {
|
|
3059
3153
|
return {
|
|
3060
3154
|
content: [{ type: "text", text: "Task list proposal declined." }],
|
|
@@ -3427,7 +3521,7 @@ promptGuidelines: [
|
|
|
3427
3521
|
const archived = archiveGoalFile(ctx, completedGoal);
|
|
3428
3522
|
resetGetGoalNudgeState(completedGoal.id);
|
|
3429
3523
|
goalsById.delete(completedGoal.id);
|
|
3430
|
-
|
|
3524
|
+
assignFocusedGoalId(null);
|
|
3431
3525
|
appendFocusEntry(null, "completed");
|
|
3432
3526
|
syncGoalTools();
|
|
3433
3527
|
updateUI(ctx);
|
|
@@ -3469,7 +3563,7 @@ promptGuidelines: [
|
|
|
3469
3563
|
loadState(ctx);
|
|
3470
3564
|
syncGoalTools();
|
|
3471
3565
|
syncTerminalInputPause(ctx);
|
|
3472
|
-
if (event.reason === "resume" && !state.goal && openGoals().length > 1 && ctx.hasUI) {
|
|
3566
|
+
if (event.reason === "resume" && !state.goal && !hasExplicitSessionFocus && openGoals().length > 1 && ctx.hasUI) {
|
|
3473
3567
|
await focusGoalCommand(ctx);
|
|
3474
3568
|
}
|
|
3475
3569
|
// Codex behavior: prompt before reactivating a paused goal on resume.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-x",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Goal mode extension for pi: persistent long-running objectives, /goal-set drafting, Sisyphus prompt style, autoContinue, and an above-editor status overlay. Fork of @capyup/pi-goal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "pi-goal-x contributors",
|