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 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 `apply_goal_tweak`, which is gated behind the user-initiated `/goal-tweak` drafting flow. This prevents the agent from silently changing the goal contract.
16
- - **`apply_goal_tweak`** is the sole mechanism for updating the objective, available exclusively during a `/goal-tweak` drafting interview. If the user's requirements change, they must run `/goal-tweak` to initiate the revision flow.
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**: 131 tests total — function-level integration tests (12), mock-pi handler tests (4), file-validity checks (6), and real `pi --fork --mode json` tests (3 scenarios: quick-sync, combined sync+complete, deferred archival).
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` | registered; accepted only during goal drafting | Submit a concrete draft for user confirmation |
160
- | `apply_goal_tweak` | tweak drafting only | Submit a revision to an existing goal |
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 `apply_goal_tweak` stops the turn |
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 |
@@ -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 TWEAK_APPLY_TOOL_NAME = "apply_goal_tweak";
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
- TWEAK_APPLY_TOOL_NAME,
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",
@@ -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
- TWEAK_APPLY_TOOL_NAME,
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, or apply_goal_tweak fires, the agent should
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 apply_goal_tweak. Cleared after the tweak is applied
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) / apply_goal_tweak
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
- // apply_goal_tweak is only available during a /goal-tweak drafting flow.
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(TWEAK_APPLY_TOOL_NAME);
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(TWEAK_APPLY_TOOL_NAME);
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 apply_goal_tweak is callable.
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 call apply_goal_tweak.`,
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 / apply_goal_tweak fires in this turn, block all subsequent tool calls except
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 apply_goal_tweak with:",
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. apply_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.",
148
- "3. After apply_goal_tweak returns, stop. If the goal is active, the next continuation will arrive automatically. If the goal is paused, the user will resume it explicitly. Either way, do not begin task work in this same turn.",
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 apply_goal_tweak.",
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 apply_goal_tweak.",
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.10.2",
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",