pi-goal-x 0.10.2 → 0.11.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 +7 -6
- package/extensions/goal-draft.ts +27 -0
- package/extensions/goal-tool-names.ts +2 -3
- package/extensions/goal.ts +156 -126
- package/extensions/prompts/goal-prompts.ts +7 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,8 +12,8 @@ All core features of [@capyup/pi-goal](https://github.com/capyup/pi-goal) are pr
|
|
|
12
12
|
|
|
13
13
|
### Goal objective is immutable
|
|
14
14
|
|
|
15
|
-
- The goal objective is immutable — the agent **must not** modify it autonomously. Objective changes are only possible through `
|
|
16
|
-
- **`
|
|
15
|
+
- The goal objective is immutable — the agent **must not** modify it autonomously. Objective changes are only possible through `propose_goal_tweak`, which presents the user with a Confirm / Continue Chatting dialog matching the `propose_goal_draft` confirmation pattern. This prevents the agent from silently changing the goal contract.
|
|
16
|
+
- **`propose_goal_tweak`** is the sole mechanism for updating the objective, available exclusively during a `/goal-tweak` drafting flow. If the user's requirements change, they must run `/goal-tweak` to initiate the revision flow.
|
|
17
17
|
|
|
18
18
|
### Deferred archival
|
|
19
19
|
|
|
@@ -23,7 +23,7 @@ All core features of [@capyup/pi-goal](https://github.com/capyup/pi-goal) are pr
|
|
|
23
23
|
### E2e test infrastructure
|
|
24
24
|
|
|
25
25
|
- **Deterministic fork tests using `--mode json`**: the e2e suite spawns a real `pi --fork --mode json` session, parses structured `tool_execution_start`/`tool_execution_end` JSON events for field-level assertions — no free-text AI output parsing. Uses `--append-system-prompt` + `--tools` to force deterministic tool calls.
|
|
26
|
-
- **Full coverage**:
|
|
26
|
+
- **Full coverage**: 143 tests total — function-level integration tests (12), mock-pi handler tests (4), file-validity checks (6), real `pi --fork --mode json` tests (3 scenarios: quick-sync, combined sync+complete, deferred archival), and propose_goal_tweak unit/integration/e2e tests (15).
|
|
27
27
|
|
|
28
28
|
### Completion auditor
|
|
29
29
|
|
|
@@ -156,11 +156,12 @@ The extension exposes tools only when they make sense for the current lifecycle
|
|
|
156
156
|
| `goal_question` | drafting / tweak drafting | Ask one focused user question |
|
|
157
157
|
| `goal_questionnaire` | drafting / tweak drafting | Ask multiple structured questions |
|
|
158
158
|
| `get_goal` | always | Read the focused goal state; mentions other open goals when present |
|
|
159
|
-
| `propose_goal_draft` |
|
|
160
|
-
| `
|
|
159
|
+
| `propose_goal_draft` | drafting only (goal creation) | Submit a concrete draft for user confirmation |
|
|
160
|
+
| `propose_goal_tweak` | tweak drafting only | Submit a revision to an existing goal (shows Confirm / Continue Chatting dialog) |
|
|
161
161
|
| `update_goal` | focused active or paused goal | Mark the focused goal complete when all requirements are satisfied. When the auditor is disabled, supply `confirmBypassAuditor: true` after user confirmation to bypass the audit |
|
|
162
162
|
| `pause_goal` | focused active goal | Pause the focused goal because of a real blocker |
|
|
163
163
|
| `abort_goal` | focused active or paused goal | Abort/archive an obsolete, impossible, unsafe, or user-cancelled focused goal |
|
|
164
|
+
| `propose_goal_tweak` | tweak drafting only | Submit a revision to the focused goal (shows Confirm / Continue Chatting dialog) |
|
|
164
165
|
| `step_complete` | hidden / legacy | Compatibility no-op; Sisyphus no longer requires a step counter |
|
|
165
166
|
| `create_goal` | hidden | Direct calls are rejected; normal creation goes through `propose_goal_draft` |
|
|
166
167
|
|
|
@@ -227,7 +228,7 @@ The shipped gates are intentionally small and mechanical.
|
|
|
227
228
|
| Completion auditor gate | Archiving completion unless an independent pi auditor agent returns `<approved/>` |
|
|
228
229
|
| Abort gate | Aborting missing, stale, completed, or reasonless goals |
|
|
229
230
|
| Direct-create rejection | Hidden `create_goal` calls creating goals without the confirmation flow |
|
|
230
|
-
| Post-stop block | Continuing to call tools after `pause_goal`, `abort_goal`, `update_goal`, or `
|
|
231
|
+
| Post-stop block | Continuing to call tools after `pause_goal`, `abort_goal`, `update_goal`, or `propose_goal_tweak` stops the turn |
|
|
231
232
|
| Empty-turn guard | Pure chat loops that would keep auto-continuing without meaningful goal work |
|
|
232
233
|
| Abort pause | Active goals staying active after user abort / Ctrl-C |
|
|
233
234
|
| Disk reconciliation | External pause/archive/delete/status changes being ignored or overwritten by stale memory |
|
package/extensions/goal-draft.ts
CHANGED
|
@@ -50,6 +50,33 @@ export function buildDraftConfirmationText(args: {
|
|
|
50
50
|
return lines.join("\n");
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
export function buildTweakConfirmationText(args: {
|
|
54
|
+
currentObjective: string;
|
|
55
|
+
newObjective: string;
|
|
56
|
+
changeSummary: string;
|
|
57
|
+
sisyphus: boolean;
|
|
58
|
+
}): string {
|
|
59
|
+
const lines: string[] = [];
|
|
60
|
+
const modeLabel = args.sisyphus ? "Sisyphus (prompt/criteria style)" : "Normal goal";
|
|
61
|
+
lines.push("Goal tweak ready for confirmation.");
|
|
62
|
+
lines.push("");
|
|
63
|
+
lines.push("Draft details:");
|
|
64
|
+
lines.push(`Mode: ${modeLabel}`);
|
|
65
|
+
lines.push("");
|
|
66
|
+
lines.push("Change:");
|
|
67
|
+
lines.push("");
|
|
68
|
+
lines.push(args.changeSummary);
|
|
69
|
+
lines.push("");
|
|
70
|
+
lines.push("Current objective:");
|
|
71
|
+
lines.push("");
|
|
72
|
+
lines.push(args.currentObjective);
|
|
73
|
+
lines.push("");
|
|
74
|
+
lines.push("Proposed new objective:");
|
|
75
|
+
lines.push("");
|
|
76
|
+
lines.push(args.newObjective);
|
|
77
|
+
return lines.join("\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
53
80
|
export function evaluateDraftingToolGate(args: {
|
|
54
81
|
toolName: string;
|
|
55
82
|
draftingFocus?: GoalDraftingFocus | null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const SISYPHUS_STEP_TOOL_NAME = "step_complete";
|
|
2
|
-
export const
|
|
2
|
+
export const PROPOSE_TWEAK_TOOL_NAME = "propose_goal_tweak";
|
|
3
3
|
export const PROPOSE_DRAFT_TOOL_NAME = "propose_goal_draft";
|
|
4
4
|
export const CREATE_GOAL_TOOL_NAME = "create_goal";
|
|
5
5
|
export const QUESTION_TOOL_NAME = "goal_question";
|
|
@@ -14,7 +14,7 @@ export const GOAL_WORK_TOOL_NAMES = [
|
|
|
14
14
|
"update_goal",
|
|
15
15
|
"pause_goal",
|
|
16
16
|
ABORT_GOAL_TOOL_NAME,
|
|
17
|
-
|
|
17
|
+
PROPOSE_TWEAK_TOOL_NAME,
|
|
18
18
|
CREATE_GOAL_TOOL_NAME,
|
|
19
19
|
PROPOSE_DRAFT_TOOL_NAME,
|
|
20
20
|
QUESTION_TOOL_NAME,
|
|
@@ -33,7 +33,6 @@ export const GOAL_PROGRESS_TOOL_NAMES = [
|
|
|
33
33
|
"update_goal",
|
|
34
34
|
"pause_goal",
|
|
35
35
|
ABORT_GOAL_TOOL_NAME,
|
|
36
|
-
TWEAK_APPLY_TOOL_NAME,
|
|
37
36
|
"write",
|
|
38
37
|
"edit",
|
|
39
38
|
"bash",
|
package/extensions/goal.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "./goal-core.ts";
|
|
11
11
|
import {
|
|
12
12
|
buildDraftConfirmationText,
|
|
13
|
+
buildTweakConfirmationText,
|
|
13
14
|
goalDraftingPrompt,
|
|
14
15
|
validateGoalDraftProposal,
|
|
15
16
|
type GoalDraftingFocus,
|
|
@@ -38,7 +39,7 @@ import {
|
|
|
38
39
|
SISYPHUS_STEP_TOOL_NAME,
|
|
39
40
|
GOAL_PROGRESS_TOOL_NAMES,
|
|
40
41
|
lifecycleToolNamesForGoalStatus,
|
|
41
|
-
|
|
42
|
+
PROPOSE_TWEAK_TOOL_NAME,
|
|
42
43
|
} from "./goal-tool-names.ts";
|
|
43
44
|
import {
|
|
44
45
|
asRecord,
|
|
@@ -127,14 +128,14 @@ const GOAL_PROGRESS_TOOL_SET = new Set<string>(GOAL_PROGRESS_TOOL_NAMES);
|
|
|
127
128
|
|
|
128
129
|
/**
|
|
129
130
|
* Tools that are NEVER blocked by the post-stop in-turn block. After pause_goal,
|
|
130
|
-
* abort_goal, update_goal=complete
|
|
131
|
+
* abort_goal, or update_goal=complete fires, the agent should
|
|
131
132
|
* yield the turn; we block all subsequent tool calls except these read-only inspections.
|
|
132
133
|
*/
|
|
133
134
|
const POST_STOP_ALLOWED_TOOL_SET = new Set<string>(POST_STOP_ALLOWED_TOOLS);
|
|
134
135
|
|
|
135
136
|
/**
|
|
136
137
|
* When non-null, /goal-tweak drafting is in progress for this goal id and the
|
|
137
|
-
* agent is allowed to call
|
|
138
|
+
* agent is allowed to call propose_goal_tweak. Cleared after the tweak is applied
|
|
138
139
|
* or when a user-driven turn arrives without a tweak follow-through. This is
|
|
139
140
|
* the schema-level affordance gate that prevents the agent from "tweaking" via
|
|
140
141
|
* arbitrary write/edit calls.
|
|
@@ -375,7 +376,7 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
375
376
|
// Per-turn flags reset in turn_start (#4, C9 fix).
|
|
376
377
|
// goalWorkToolCalledThisTurn: tracks whether a real goal-work tool was called.
|
|
377
378
|
// If false at turn_end, we don't queue another autoContinue (empty chat turn).
|
|
378
|
-
// turnStoppedFor: set by pause_goal / update_goal(complete) /
|
|
379
|
+
// turnStoppedFor: set by pause_goal / update_goal(complete) / propose_goal_tweak
|
|
379
380
|
// after their successful execute. Once set, pi.on("tool_call") blocks all
|
|
380
381
|
// subsequent in-turn tool calls except POST_STOP_ALLOWED_TOOLS. This is the
|
|
381
382
|
// schema fix for "agent keeps writing files after pause_goal".
|
|
@@ -392,18 +393,6 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
392
393
|
lastAccountedAt: null as number | null,
|
|
393
394
|
};
|
|
394
395
|
|
|
395
|
-
const draftingHiddenWorkTools = [
|
|
396
|
-
"bash",
|
|
397
|
-
"read",
|
|
398
|
-
"write",
|
|
399
|
-
"edit",
|
|
400
|
-
"grep",
|
|
401
|
-
"find",
|
|
402
|
-
"ls",
|
|
403
|
-
SISYPHUS_STEP_TOOL_NAME,
|
|
404
|
-
TWEAK_APPLY_TOOL_NAME,
|
|
405
|
-
CREATE_GOAL_TOOL_NAME,
|
|
406
|
-
] as const;
|
|
407
396
|
const goalExecutionWorkTools = ["read", "bash", "edit", "write"] as const;
|
|
408
397
|
|
|
409
398
|
function syncGoalTools(): void {
|
|
@@ -420,15 +409,16 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
420
409
|
// mechanism. Keep step_complete registered for legacy transcripts, but do
|
|
421
410
|
// not expose it as an active work tool.
|
|
422
411
|
active.delete(SISYPHUS_STEP_TOOL_NAME);
|
|
423
|
-
//
|
|
412
|
+
// propose_goal_tweak is only available during a /goal-tweak drafting flow.
|
|
424
413
|
// Note: tweak drafting can run against active OR paused goals.
|
|
425
414
|
if (state.goal && tweakDraftingFor === state.goal.id) {
|
|
426
|
-
active.add(
|
|
415
|
+
active.add(PROPOSE_TWEAK_TOOL_NAME);
|
|
427
416
|
active.add(QUESTION_TOOL_NAME);
|
|
428
417
|
active.add(QUESTIONNAIRE_TOOL_NAME);
|
|
429
418
|
} else {
|
|
430
|
-
active.delete(
|
|
419
|
+
active.delete(PROPOSE_TWEAK_TOOL_NAME);
|
|
431
420
|
}
|
|
421
|
+
|
|
432
422
|
// Keep the commit tool available and let its validator enforce that a
|
|
433
423
|
// drafting flow is active. This avoids fragile hidden-tool drift after
|
|
434
424
|
// question turns, compaction, or active-tool resync.
|
|
@@ -1053,11 +1043,11 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
1053
1043
|
if (!focused) return;
|
|
1054
1044
|
const sisyphusOn = focused.sisyphus;
|
|
1055
1045
|
const label = sisyphusOn ? "Sisyphus tweak drafting" : "Goal tweak drafting";
|
|
1056
|
-
// Activate the tweak edit-gate so
|
|
1046
|
+
// Activate the tweak edit-gate so propose_goal_tweak is callable.
|
|
1057
1047
|
tweakDraftingFor = focused.id;
|
|
1058
1048
|
syncGoalTools();
|
|
1059
1049
|
ctx.ui.notify(
|
|
1060
|
-
`${label} started${trimmed ? `: ${truncateText(trimmed, 60)}` : ""}. The agent will interview you and then
|
|
1050
|
+
`${label} started${trimmed ? `: ${truncateText(trimmed, 60)}` : ""}. The agent will interview you and then propose the revision for you to Confirm.`,
|
|
1061
1051
|
"info",
|
|
1062
1052
|
);
|
|
1063
1053
|
const draftId = `tweak-${focused.id}-${Date.now().toString(36)}`;
|
|
@@ -1694,6 +1684,150 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
1694
1684
|
},
|
|
1695
1685
|
}));
|
|
1696
1686
|
|
|
1687
|
+
pi.registerTool(defineTool({
|
|
1688
|
+
name: PROPOSE_TWEAK_TOOL_NAME,
|
|
1689
|
+
label: "Propose Goal Tweak",
|
|
1690
|
+
description: "During a /goal-tweak drafting flow, present the revised goal to the user for confirmation. The user sees a Confirm / Continue Chatting dialog — only on Confirm is the goal updated.",
|
|
1691
|
+
promptSnippet: "Propose the revised goal to the user with a Confirm / Continue Chatting dialog.",
|
|
1692
|
+
promptGuidelines: [
|
|
1693
|
+
"Only call propose_goal_tweak inside a /goal-tweak drafting flow (the prompt makes that explicit). It is rejected at any other time.",
|
|
1694
|
+
"newObjective must be the FULL revised objective text, formatted the same way as the original (=== Goal === or === Sisyphus Goal === block). Do NOT pass a diff or partial patch; pass the whole new objective.",
|
|
1695
|
+
"For Sisyphus goals: preserve the Sisyphus style and ordered-plan wording unless the user explicitly asks to remove it.",
|
|
1696
|
+
"changeSummary is a one-sentence description of WHAT changed (for the confirmation dialog, activity log, and tweak log).",
|
|
1697
|
+
"The user will see a full plain-text report plus a [Confirm] / [Continue Chatting] choice. Confirm applies the tweak; Continue Chatting returns control to you to ask follow-up questions.",
|
|
1698
|
+
"If the tool returns 'continue chatting', ask the user what they want changed. Do NOT re-propose the same content immediately; iterate based on their feedback first.",
|
|
1699
|
+
"Do NOT use write/edit/bash to modify the active goal file directly. propose_goal_tweak is the only sanctioned channel.",
|
|
1700
|
+
],
|
|
1701
|
+
parameters: Type.Object({
|
|
1702
|
+
newObjective: Type.String({ description: "The complete revised objective text. For Sisyphus goals, preserve the Sisyphus style unless the user explicitly changes it." }),
|
|
1703
|
+
changeSummary: Type.String({ description: "One-sentence description of what was changed (used in confirmation dialog and tweak log)." }),
|
|
1704
|
+
}),
|
|
1705
|
+
executionMode: "sequential",
|
|
1706
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
1707
|
+
reconcileFocusedGoalFromDisk(ctx);
|
|
1708
|
+
if (!state.goal) {
|
|
1709
|
+
return {
|
|
1710
|
+
content: [{ type: "text", text: "No goal is set; propose_goal_tweak is a no-op." }],
|
|
1711
|
+
details: goalDetails(state.goal),
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
if (tweakDraftingFor !== state.goal.id) {
|
|
1715
|
+
return {
|
|
1716
|
+
content: [{
|
|
1717
|
+
type: "text",
|
|
1718
|
+
text: "propose_goal_tweak REJECTED: no /goal-tweak drafting flow is active for this goal. " +
|
|
1719
|
+
"This tool can only be called during a /goal-tweak drafting interview that the user initiated. " +
|
|
1720
|
+
"If you want to change the goal, ask the user to run /goal-tweak.",
|
|
1721
|
+
}],
|
|
1722
|
+
details: goalDetails(state.goal),
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
if (state.goal.status !== "active" && state.goal.status !== "paused") {
|
|
1726
|
+
return {
|
|
1727
|
+
content: [{ type: "text", text: `Goal is ${statusLabel(state.goal)}; cannot apply a tweak.` }],
|
|
1728
|
+
details: goalDetails(state.goal),
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
const newObjective = params.newObjective.trim();
|
|
1732
|
+
if (!newObjective) throw new Error("propose_goal_tweak requires a non-empty newObjective.");
|
|
1733
|
+
const changeSummary = params.changeSummary.trim();
|
|
1734
|
+
if (!changeSummary) throw new Error("propose_goal_tweak requires a non-empty changeSummary.");
|
|
1735
|
+
|
|
1736
|
+
// Build the confirmation dialog text.
|
|
1737
|
+
const draftSummary = buildTweakConfirmationText({
|
|
1738
|
+
currentObjective: state.goal.objective,
|
|
1739
|
+
newObjective,
|
|
1740
|
+
changeSummary,
|
|
1741
|
+
sisyphus: !!state.goal.sisyphus,
|
|
1742
|
+
});
|
|
1743
|
+
|
|
1744
|
+
const headless = shouldAutoConfirmProposal({ hasUI: ctx.hasUI, autoConfirmEnv: process.env.PI_GOAL_AUTO_CONFIRM });
|
|
1745
|
+
|
|
1746
|
+
let decision: "confirm" | "continue";
|
|
1747
|
+
if (headless) {
|
|
1748
|
+
decision = "confirm";
|
|
1749
|
+
} else {
|
|
1750
|
+
try {
|
|
1751
|
+
decision = await showProposalDialog(ctx, draftSummary, state.goal.sisyphus ? "sisyphus" : "goal");
|
|
1752
|
+
} catch (err) {
|
|
1753
|
+
const message = proposalDialogFailureMessage(err);
|
|
1754
|
+
ctx.ui.notify(message, "error");
|
|
1755
|
+
return {
|
|
1756
|
+
content: [{ type: "text", text: message }],
|
|
1757
|
+
details: goalDetails(state.goal),
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
if (decision === "confirm") {
|
|
1763
|
+
// Apply the tweak: write the new objective to disk authoritatively.
|
|
1764
|
+
const next: GoalRecord = {
|
|
1765
|
+
...state.goal,
|
|
1766
|
+
objective: newObjective,
|
|
1767
|
+
updatedAt: nowIso(),
|
|
1768
|
+
// Clear any prior agent pause reason — the user has redefined the work.
|
|
1769
|
+
pauseReason: undefined,
|
|
1770
|
+
pauseSuggestedAction: undefined,
|
|
1771
|
+
};
|
|
1772
|
+
// IMPORTANT: bypass setGoal() / persist() here. persist() calls
|
|
1773
|
+
// syncGoalPromptFromDisk() which would RE-READ the stale objective
|
|
1774
|
+
// from the still-old goal file on disk and clobber our new objective
|
|
1775
|
+
// before writing. propose_goal_tweak is the authoritative source for
|
|
1776
|
+
// objective changes — the disk is downstream, not upstream. Do the
|
|
1777
|
+
// minimal state update manually:
|
|
1778
|
+
// 1) write the new record to disk authoritatively
|
|
1779
|
+
// 2) update in-memory `goal` to the canonical post-write record
|
|
1780
|
+
// 3) append the state entry and re-sync tools
|
|
1781
|
+
// 4) clear the tweak drafting gate so propose_goal_tweak can't be re-used
|
|
1782
|
+
state.goal = writeActiveGoalFile(ctx, next);
|
|
1783
|
+
pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
|
|
1784
|
+
tweakDraftingFor = null;
|
|
1785
|
+
// Reset autoContinue counter — plan changed, agent gets a fresh chain.
|
|
1786
|
+
resetGetGoalNudgeState(state.goal.id);
|
|
1787
|
+
// Mark turn-stopped so subsequent in-turn tool calls are blocked.
|
|
1788
|
+
turnStoppedFor = state.goal.id;
|
|
1789
|
+
syncGoalTools();
|
|
1790
|
+
updateUI(ctx);
|
|
1791
|
+
ctx.ui.notify(`Goal tweaked: ${truncateText(changeSummary, 160)}`, "info");
|
|
1792
|
+
// Append ledger event for tweak
|
|
1793
|
+
try {
|
|
1794
|
+
appendGoalEvent(ctx, {
|
|
1795
|
+
type: "goal_tweaked",
|
|
1796
|
+
goalId: state.goal.id,
|
|
1797
|
+
changeSummary,
|
|
1798
|
+
at: state.goal.updatedAt,
|
|
1799
|
+
});
|
|
1800
|
+
} catch {
|
|
1801
|
+
// Ledger append failure should not crash tweak
|
|
1802
|
+
}
|
|
1803
|
+
return {
|
|
1804
|
+
content: [{
|
|
1805
|
+
type: "text",
|
|
1806
|
+
text: `Goal tweak applied. ${changeSummary}\nStop now; the next continuation will arrive automatically if the goal is active.`,
|
|
1807
|
+
}],
|
|
1808
|
+
details: goalDetails(state.goal),
|
|
1809
|
+
terminate: true,
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// "continue" — user wants to keep chatting. Drafting state stays armed.
|
|
1814
|
+
return {
|
|
1815
|
+
content: [{
|
|
1816
|
+
type: "text",
|
|
1817
|
+
text: "Goal tweak refinement requested (Continue Chatting). The tweak was not applied — drafting remains active. Ask the user what they want changed about the revision, then revise and call propose_goal_tweak again. Do not re-propose the same content — wait for the user's input first.",
|
|
1818
|
+
}],
|
|
1819
|
+
details: goalDetails(state.goal),
|
|
1820
|
+
};
|
|
1821
|
+
},
|
|
1822
|
+
renderCall(args, theme) {
|
|
1823
|
+
const summary = typeof args?.changeSummary === "string" ? truncateText(args.changeSummary, 80) : "";
|
|
1824
|
+
return new Text(theme.fg("toolTitle", "propose_goal_tweak ") + theme.fg("muted", summary), 0, 0);
|
|
1825
|
+
},
|
|
1826
|
+
renderResult(result, _options, theme) {
|
|
1827
|
+
return renderGoalResult(result, theme);
|
|
1828
|
+
},
|
|
1829
|
+
}));
|
|
1830
|
+
|
|
1697
1831
|
pi.registerTool(defineTool({
|
|
1698
1832
|
name: "update_goal",
|
|
1699
1833
|
label: "Update Goal",
|
|
@@ -2200,110 +2334,6 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
2200
2334
|
},
|
|
2201
2335
|
}));
|
|
2202
2336
|
|
|
2203
|
-
pi.registerTool(defineTool({
|
|
2204
|
-
name: TWEAK_APPLY_TOOL_NAME,
|
|
2205
|
-
label: "Apply Goal Tweak",
|
|
2206
|
-
description: "Atomically apply a /goal-tweak revision to the active goal. The ONLY way to modify an active goal's objective. Only available during a /goal-tweak drafting flow.",
|
|
2207
|
-
promptSnippet: "Apply the revised goal objective produced by a /goal-tweak drafting interview.",
|
|
2208
|
-
promptGuidelines: [
|
|
2209
|
-
"Only call apply_goal_tweak inside a /goal-tweak drafting flow (the prompt makes that explicit). It is rejected at any other time.",
|
|
2210
|
-
"newObjective must be the FULL revised objective text, formatted the same way as the original (=== Goal === or === Sisyphus Goal === block). Do NOT pass a diff or partial patch; pass the whole new objective.",
|
|
2211
|
-
"For Sisyphus goals: preserve the Sisyphus style and ordered-plan wording unless the user explicitly asks to remove it.",
|
|
2212
|
-
"changeSummary is a one-sentence description of WHAT changed (for the activity log and pause messages).",
|
|
2213
|
-
"Do NOT use write/edit/bash to modify the active goal file directly. apply_goal_tweak is the only sanctioned channel.",
|
|
2214
|
-
"After apply_goal_tweak returns, stop. Do not begin new task work in the same turn. The system will queue the next continuation.",
|
|
2215
|
-
],
|
|
2216
|
-
parameters: Type.Object({
|
|
2217
|
-
newObjective: Type.String({ description: "The complete revised objective text. For Sisyphus goals, preserve the Sisyphus style unless the user explicitly changes it." }),
|
|
2218
|
-
changeSummary: Type.String({ description: "One-sentence description of what was changed (used in UI notification and tweak log)." }),
|
|
2219
|
-
}),
|
|
2220
|
-
executionMode: "sequential",
|
|
2221
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
2222
|
-
reconcileFocusedGoalFromDisk(ctx);
|
|
2223
|
-
if (!state.goal) {
|
|
2224
|
-
return {
|
|
2225
|
-
content: [{ type: "text", text: "No goal is set; apply_goal_tweak is a no-op." }],
|
|
2226
|
-
details: goalDetails(state.goal),
|
|
2227
|
-
};
|
|
2228
|
-
}
|
|
2229
|
-
if (tweakDraftingFor !== state.goal.id) {
|
|
2230
|
-
return {
|
|
2231
|
-
content: [{
|
|
2232
|
-
type: "text",
|
|
2233
|
-
text: "apply_goal_tweak REJECTED: no /goal-tweak drafting flow is active for this goal. " +
|
|
2234
|
-
"This tool can only be called during a /goal-tweak drafting interview that the user initiated. " +
|
|
2235
|
-
"If you want to change the goal, ask the user to run /goal-tweak.",
|
|
2236
|
-
}],
|
|
2237
|
-
details: goalDetails(state.goal),
|
|
2238
|
-
};
|
|
2239
|
-
}
|
|
2240
|
-
if (state.goal.status !== "active" && state.goal.status !== "paused") {
|
|
2241
|
-
return {
|
|
2242
|
-
content: [{ type: "text", text: `Goal is ${statusLabel(state.goal)}; cannot apply a tweak.` }],
|
|
2243
|
-
details: goalDetails(state.goal),
|
|
2244
|
-
};
|
|
2245
|
-
}
|
|
2246
|
-
const newObjective = params.newObjective.trim();
|
|
2247
|
-
if (!newObjective) throw new Error("apply_goal_tweak requires a non-empty newObjective.");
|
|
2248
|
-
const changeSummary = params.changeSummary.trim();
|
|
2249
|
-
if (!changeSummary) throw new Error("apply_goal_tweak requires a non-empty changeSummary.");
|
|
2250
|
-
const next: GoalRecord = {
|
|
2251
|
-
...state.goal,
|
|
2252
|
-
objective: newObjective,
|
|
2253
|
-
updatedAt: nowIso(),
|
|
2254
|
-
// Clear any prior agent pause reason — the user has redefined the work.
|
|
2255
|
-
pauseReason: undefined,
|
|
2256
|
-
pauseSuggestedAction: undefined,
|
|
2257
|
-
};
|
|
2258
|
-
// IMPORTANT: bypass setGoal() / persist() here. persist() calls
|
|
2259
|
-
// syncGoalPromptFromDisk() which would RE-READ the stale objective
|
|
2260
|
-
// from the still-old goal file on disk and clobber our new objective
|
|
2261
|
-
// before writing. apply_goal_tweak is the authoritative source for
|
|
2262
|
-
// objective changes — the disk is downstream, not upstream. Do the
|
|
2263
|
-
// minimal state update manually:
|
|
2264
|
-
// 1) write the new record to disk authoritatively
|
|
2265
|
-
// 2) update in-memory `goal` to the canonical post-write record
|
|
2266
|
-
// 3) append the state entry and re-sync tools
|
|
2267
|
-
// 4) clear the tweak drafting gate so apply_goal_tweak can't be re-used
|
|
2268
|
-
state.goal = writeActiveGoalFile(ctx, next);
|
|
2269
|
-
pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
|
|
2270
|
-
tweakDraftingFor = null;
|
|
2271
|
-
// Reset autoContinue counter — plan changed, agent gets a fresh chain.
|
|
2272
|
-
resetGetGoalNudgeState(state.goal.id);
|
|
2273
|
-
// C9 fix: mark turn-stopped so subsequent in-turn tool calls are blocked.
|
|
2274
|
-
turnStoppedFor = state.goal.id;
|
|
2275
|
-
syncGoalTools();
|
|
2276
|
-
updateUI(ctx);
|
|
2277
|
-
ctx.ui.notify(`Goal tweaked: ${truncateText(changeSummary, 160)}`, "info");
|
|
2278
|
-
// Append ledger event for tweak
|
|
2279
|
-
try {
|
|
2280
|
-
appendGoalEvent(ctx, {
|
|
2281
|
-
type: "goal_tweaked",
|
|
2282
|
-
goalId: state.goal.id,
|
|
2283
|
-
changeSummary,
|
|
2284
|
-
at: state.goal.updatedAt,
|
|
2285
|
-
});
|
|
2286
|
-
} catch {
|
|
2287
|
-
// Ledger append failure should not crash tweak
|
|
2288
|
-
}
|
|
2289
|
-
return {
|
|
2290
|
-
content: [{
|
|
2291
|
-
type: "text",
|
|
2292
|
-
text: `Goal tweak applied. ${changeSummary}\nStop now; the next continuation will arrive automatically if the goal is active.`,
|
|
2293
|
-
}],
|
|
2294
|
-
details: goalDetails(state.goal),
|
|
2295
|
-
terminate: true,
|
|
2296
|
-
};
|
|
2297
|
-
},
|
|
2298
|
-
renderCall(args, theme) {
|
|
2299
|
-
const summary = typeof args?.changeSummary === "string" ? truncateText(args.changeSummary, 80) : "";
|
|
2300
|
-
return new Text(theme.fg("toolTitle", "apply_goal_tweak ") + theme.fg("muted", summary), 0, 0);
|
|
2301
|
-
},
|
|
2302
|
-
renderResult(result, _options, theme) {
|
|
2303
|
-
return renderGoalResult(result, theme);
|
|
2304
|
-
},
|
|
2305
|
-
}));
|
|
2306
|
-
|
|
2307
2337
|
syncGoalTools();
|
|
2308
2338
|
|
|
2309
2339
|
pi.on("context", async (event): Promise<{ messages: typeof event.messages } | undefined> => {
|
|
@@ -2353,7 +2383,7 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
2353
2383
|
// #4 + C9 fix + Phase 5 C3: gate in-turn tool calls based on lifecycle state.
|
|
2354
2384
|
pi.on("tool_call", async (event, ctx) => {
|
|
2355
2385
|
// Post-stop in-turn block (C9 0ad8 fix): after pause_goal / abort_goal /
|
|
2356
|
-
// update_goal=complete /
|
|
2386
|
+
// update_goal=complete / propose_goal_tweak fires in this turn, block all subsequent tool calls except
|
|
2357
2387
|
// read-only inspection. Forces the agent to yield the turn instead of "fixing"
|
|
2358
2388
|
// the situation by creating extra files etc.
|
|
2359
2389
|
if (turnStoppedFor !== null && !POST_STOP_ALLOWED_TOOL_SET.has(event.toolName)) {
|
|
@@ -139,17 +139,19 @@ export function goalTweakDraftingPrompt(current: GoalRecord, hint: string): stri
|
|
|
139
139
|
...focusItems,
|
|
140
140
|
"",
|
|
141
141
|
"When the revision is clear:",
|
|
142
|
-
"1. Call
|
|
142
|
+
"1. Call propose_goal_tweak with:",
|
|
143
143
|
" - newObjective: the FULL revised objective text, formatted the same way as the original" + (sisyphusOn
|
|
144
144
|
? " === Sisyphus Goal === block (Objective / Success criteria / Boundaries / Constraints / If blocked / Sisyphus reminder)."
|
|
145
145
|
: " === Goal === block (Objective / Success criteria / Boundaries / Constraints / If blocked)."),
|
|
146
146
|
" - changeSummary: one sentence describing what changed.",
|
|
147
|
-
"2.
|
|
148
|
-
"
|
|
147
|
+
"2. propose_goal_tweak opens the user's Confirm / Continue Chatting dialog.",
|
|
148
|
+
" - Confirm applies the tweak. Stop; the next continuation will arrive automatically if the goal is active.",
|
|
149
|
+
" - Continue Chatting means the drafting stays active — ask the user what they want changed, then revise and call propose_goal_tweak again.",
|
|
150
|
+
"3. propose_goal_tweak is the ONLY sanctioned way to change an active goal's objective. It atomically updates the goal record and the on-disk file. Do not attempt to bypass it.",
|
|
149
151
|
"",
|
|
150
152
|
"Edge cases:",
|
|
151
|
-
"- If you decide no change is actually needed, say so clearly in one sentence and stop without calling
|
|
152
|
-
"- If the hint conflicts with the existing goal in a major way, propose two or three concrete alternative revisions and let the user pick before calling
|
|
153
|
+
"- If you decide no change is actually needed, say so clearly in one sentence and stop without calling propose_goal_tweak.",
|
|
154
|
+
"- If the hint conflicts with the existing goal in a major way, propose two or three concrete alternative revisions and let the user pick before calling propose_goal_tweak.",
|
|
153
155
|
].join("\n");
|
|
154
156
|
}
|
|
155
157
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-x",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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",
|