pi-goal-x 0.7.2 → 0.8.2

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
@@ -1,20 +1,34 @@
1
1
  # pi-goal-x
2
2
 
3
- > **Fork of [@capyup/pi-goal](https://github.com/capyup/pi-goal).** Upstream changes can be merged from the original repository.
3
+ > **Fork of [@capyup/pi-goal](https://github.com/capyup/pi-goal)** — this repository extends the upstream with quality-of-life features for the completion auditor, lifecycle reliability improvements, and drafting UX refinements. Upstream changes can be merged from the original repository.
4
4
 
5
5
  `pi-goal-x` is a long-running goal extension for [pi](https://github.com/earendil-works/pi-coding-agent). It gives the agent a durable objective, a visible lifecycle, and schema-gated tools for drafting, executing, pausing, resuming, and completing work.
6
6
 
7
7
  The extension is designed around one rule: **the user owns intent; the agent executes only after the goal is explicit and confirmed**.
8
8
 
9
- ## What's new in pi-goal-x
9
+ ## What's different from upstream
10
10
 
11
- pi-goal-x adds a handful of quality-of-life features on top of the original:
11
+ All core features of [@capyup/pi-goal](https://github.com/capyup/pi-goal) are preserved. The following changes are specific to pi-goal-x:
12
12
 
13
- - **See what the auditor is doing.** When a goal completes and the auditor kicks in, you get a live view — a spinner, the tool it's running, and recent output lines. No more wondering if anything is happening.
14
- - **Skip the audit with Escape.** If you're in a hurry or just don't need an audit right now, press Escape. The goal completes immediately without waiting.
15
- - **Turn off the auditor entirely.** If you never want audits, set it once in `/goal-settings` and you're done. The agent can bypass it by asking you to confirm.
16
- - **Know why an audit was skipped.** Whether you pressed Escape or had it disabled, the ledger records the reason.
17
- - **Faster, safer audit lifecycle.** The audit starts and stops cleanly no more stuck goals, ghost states, or having to kill the session.
13
+ ### Completion auditor
14
+
15
+ - **Live progress widget** — when the auditor runs, the TUI shows a spinner, the current tool being executed, and recent output lines. No more wondering if anything is happening.
16
+ - **Escape to skip** — press Escape during an audit to abort it and complete the goal immediately. The skip is recorded in the ledger as `audit_skipped` with reason `user_aborted` and auditor model metadata.
17
+ - **Disable the auditor entirely** set `disabled: true` in `.pi/goal-auditor.json` (or toggle it via `/goal-settings` `disabled`). The agent can still bypass with user confirmation by passing `confirmBypassAuditor: true` to `update_goal`.
18
+ - **Skipped audits are recorded** — every skip (whether disabled or Escape-aborted) is logged to the ledger with the reason, provider, model, and thinking level for full traceability.
19
+ - **Robust abort detection** — the auditor detects aborts both from exceptions *and* from `session.prompt()` returning after an abort signal, preventing stuck goals or ghost states.
20
+ - **Cleaner lifecycle** — `AbortSignal` is properly wired to `session.abort()`, animation timers are cleaned up, and the unsubscribe path is always executed. No more having to kill the session.
21
+ - **Completion report includes full auditor output** — the auditor's full report is included in the goal completion conversation message upon approval, not just a verdict.
22
+ - **Session factory injection** — `runGoalCompletionAuditor` accepts an optional `createSession` parameter for testability, enabling mock auditor sessions in tests.
23
+
24
+ ### Drafting & UX
25
+
26
+ - **Normalized proposal-refinement language** — consistent terminology ("keep refining through normal proposal cycles") across all drafting prompts and tools.
27
+ - **`PI_GOAL_AUTO_CONFIRM=0` opt-out** — explicitly set the env var to `0` to disable auto-confirm even in headless contexts (useful for benchmarking).
28
+
29
+ ### Testing
30
+
31
+ - **Comprehensive abort/skip coverage** — unit tests for `audit_skipped` ledger events, disabled auditor config, Esc-to-skip widget behaviour, post-prompt abort detection, and the `confirmBypassAuditor` parameter.
18
32
 
19
33
  ## What it provides
20
34
 
@@ -104,7 +104,7 @@ export function goalDraftingPrompt(topic: string, focus: GoalDraftingFocus): str
104
104
  "- If the topic is already concrete, you may proceed directly to propose_goal_draft.",
105
105
  "- The goal contract should make the objective, success criteria, boundaries, constraints, and blocker rule explicit.",
106
106
  "- Keep grilling assumptions until the objective, success criteria, boundaries, constraints, and blocker rule are clear enough to confirm.",
107
- "- propose_goal_draft opens the user's Confirm / Continue Chatting dialog. Confirm creates and focuses the goal; Continue Chatting means keep clarifying.",
107
+ "- propose_goal_draft opens the user's Confirm / Continue Chatting dialog. Confirm creates and focuses the goal; Continue Chatting means keep refining through normal proposal cycles.",
108
108
  "- create_goal is not a shortcut. Direct create_goal calls are rejected so the user keeps explicit say in goal creation.",
109
109
  ];
110
110
 
@@ -116,11 +116,14 @@ export function abortGoalCommandMessage(args: { archived: boolean; wasDrafting:
116
116
  return args.archived ? "Goal aborted and archived." : args.wasDrafting ? "Drafting cancelled." : "No goal is set.";
117
117
  }
118
118
 
119
- export function buildCompletionReport(args: { detailedSummary: string; completionSummary?: string | null; auditorReport?: string | null }): string {
119
+ export function buildCompletionReport(args: { detailedSummary: string; completionSummary?: string | null; auditorReport?: string | null; auditSkippedReason?: string | null }): string {
120
+ const auditSkipped = args.auditSkippedReason?.trim();
120
121
  const auditorReport = args.auditorReport?.trim();
121
- const lines = auditorReport
122
- ? ["Goal audit approved.", "", "Auditor approval:", auditorReport, "", "Goal complete."]
123
- : ["Goal complete."];
122
+ const lines = auditSkipped
123
+ ? ["Goal audit skipped.", "", "Reason: " + auditSkipped, "", "Goal complete."]
124
+ : auditorReport
125
+ ? ["Goal audit approved.", "", "Auditor approval:", auditorReport, "", "Goal complete."]
126
+ : ["Goal complete."];
124
127
  const summary = args.completionSummary?.trim();
125
128
  if (summary) {
126
129
  lines.push("", "Completion summary:", summary);
@@ -56,6 +56,7 @@ export function formatQuestionnaireAnswers(result: GoalQuestionnaireResult): str
56
56
  }
57
57
 
58
58
  export function shouldAutoConfirmProposal(args: { hasUI: boolean; autoConfirmEnv?: string }): boolean {
59
+ if (args.autoConfirmEnv === "0") return false; // explicit opt-out (benchmarking)
59
60
  return !args.hasUI || args.autoConfirmEnv === "1";
60
61
  }
61
62
 
@@ -667,7 +667,8 @@ export default function goalExtension(pi: ExtensionAPI): void {
667
667
  clearActiveAccounting();
668
668
  return;
669
669
  }
670
- if (state.goal?.activePath && !reconcileFocusedGoalFromDisk(ctx, { preserveMemoryUsage: true })) return;
670
+ // Skip disk reconciliation for complete goals they are pending archival at turn_end.
671
+ if (state.goal?.activePath && state.goal?.status !== "complete" && !reconcileFocusedGoalFromDisk(ctx, { preserveMemoryUsage: true })) return;
671
672
  if (!state.goal || state.goal.status !== "active" || accounting.activeGoalId !== state.goal.id) {
672
673
  beginAccounting();
673
674
  return;
@@ -1677,7 +1678,7 @@ export default function goalExtension(pi: ExtensionAPI): void {
1677
1678
  return {
1678
1679
  content: [{
1679
1680
  type: "text",
1680
- text: "User clicked 'Continue Chatting'. The goal was NOT created. Ask the user what they want to change about the draft (objective, scope, criteria, steps), then revise and call propose_goal_draft again. Do not call propose_goal_draft again with the same content — wait for the user's input first.",
1681
+ text: "Goal draft refinement requested (Continue Chatting). The goal was not created — drafting remains active. Ask the user what they want changed about the draft (objective, scope, criteria, steps), then revise and call propose_goal_draft again. Do not re-propose the same content — wait for the user's input first.",
1681
1682
  }],
1682
1683
  details: goalDetails(state.goal),
1683
1684
  };
@@ -1751,13 +1752,16 @@ export default function goalExtension(pi: ExtensionAPI): void {
1751
1752
  details: goalDetails(state.goal),
1752
1753
  };
1753
1754
  }
1754
- // Auditor disabled and confirmed — skip audit, complete immediately
1755
- await pi.sendMessage<GoalAuditEventDetails>({
1755
+ // Auditor disabled and confirmed — skip audit.
1756
+ // Defer archival: set goal complete in-memory + write active file WITHOUT
1757
+ // archiving. Archival happens at turn_end so the agent has a chance to
1758
+ // recognise the skipped audit before the goal is archived.
1759
+ pi.sendMessage<GoalAuditEventDetails>({
1756
1760
  customType: GOAL_AUDIT_ENTRY,
1757
- content: `Auditor disabledcompletion bypassed for goal ${auditTarget.id}.`,
1761
+ content: `Goal completedauditor disabled in settings.`,
1758
1762
  display: true,
1759
1763
  details: { phase: "skipped", goalId: auditTarget.id, auditor: auditorLabel },
1760
- }, { triggerTurn: true });
1764
+ });
1761
1765
  try {
1762
1766
  appendGoalEvent(ctx, {
1763
1767
  type: "audit_skipped",
@@ -1771,41 +1775,32 @@ export default function goalExtension(pi: ExtensionAPI): void {
1771
1775
  } catch {
1772
1776
  // Ledger append failure should not block completion
1773
1777
  }
1774
- // Mark goal complete directly (skip audit entirely)
1778
+ // Set goal complete in memory (defer archival to turn_end)
1775
1779
  accountProgress(ctx);
1776
- state.goal = auditTarget;
1777
- stopActiveGoal("complete", "agent", ctx);
1778
- const completedGoal = state.goal;
1779
- turnStoppedFor = completedGoal?.id ?? null;
1780
1780
  auditProgress = null;
1781
1781
  goalWidgetComponent?.invalidate();
1782
- if (completedGoal) {
1783
- resetGetGoalNudgeState(completedGoal.id);
1784
- goalsById.delete(completedGoal.id);
1785
- focusedGoalId = null;
1786
- appendFocusEntry(null, "completed");
1787
- syncGoalTools();
1788
- updateUI(ctx);
1789
- try {
1790
- appendGoalEvent(ctx, {
1791
- type: "goal_completed",
1792
- goalId: completedGoal.id,
1793
- archivePath: completedGoal.archivedPath,
1794
- at: nowIso(),
1795
- });
1796
- } catch {
1797
- // Ledger append failure should not crash completion
1798
- }
1799
- }
1782
+ state.goal = {
1783
+ ...auditTarget,
1784
+ status: "complete",
1785
+ stopReason: "agent",
1786
+ updatedAt: nowIso(),
1787
+ };
1788
+ state.goal = writeActiveGoalFile(ctx, state.goal);
1789
+ pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
1790
+ turnStoppedFor = state.goal?.id ?? null;
1791
+ resetGetGoalNudgeState(state.goal?.id);
1792
+ syncGoalTools();
1793
+ updateUI(ctx);
1800
1794
  return {
1801
1795
  content: [{
1802
1796
  type: "text",
1803
1797
  text: buildCompletionReport({
1804
- detailedSummary: detailedSummary(completedGoal),
1798
+ detailedSummary: detailedSummary(state.goal),
1805
1799
  completionSummary: params.completionSummary,
1800
+ auditSkippedReason: "auditor disabled in settings",
1806
1801
  }),
1807
1802
  }],
1808
- details: goalDetails(completedGoal),
1803
+ details: goalDetails(state.goal),
1809
1804
  terminate: true,
1810
1805
  };
1811
1806
  }
@@ -1884,47 +1879,39 @@ export default function goalExtension(pi: ExtensionAPI): void {
1884
1879
  // skip notification is exposed exactly once to the agent as part of the
1885
1880
  // update_goal tool execution, matching the disabled-flow pattern exactly.
1886
1881
  if (auditor.error === "Auditor aborted.") {
1887
- await pi.sendMessage<GoalAuditEventDetails>({
1882
+ // Esc-skip: same deferred archival pattern as disabled bypass.
1883
+ pi.sendMessage<GoalAuditEventDetails>({
1888
1884
  customType: GOAL_AUDIT_ENTRY,
1889
- content: `Auditor abortedcompletion bypassed for goal ${auditTarget.id}.`,
1885
+ content: `Goal completedauditor bypassed (user pressed Escape during audit).`,
1890
1886
  display: true,
1891
1887
  details: { phase: "skipped", goalId: auditTarget.id, auditor: auditorLabel },
1892
- }, { triggerTurn: true });
1893
- // Mark goal complete directly (skip audit entirely)
1888
+ });
1889
+ // Set goal complete in memory (defer archival to turn_end)
1894
1890
  accountProgress(ctx);
1895
- state.goal = auditTarget;
1896
- stopActiveGoal("complete", "agent", ctx);
1897
- const completedGoal = state.goal;
1898
- turnStoppedFor = completedGoal?.id ?? null;
1899
1891
  auditProgress = null;
1900
1892
  goalWidgetComponent?.invalidate();
1901
- if (completedGoal) {
1902
- resetGetGoalNudgeState(completedGoal.id);
1903
- goalsById.delete(completedGoal.id);
1904
- focusedGoalId = null;
1905
- appendFocusEntry(null, "completed");
1906
- syncGoalTools();
1907
- updateUI(ctx);
1908
- try {
1909
- appendGoalEvent(ctx, {
1910
- type: "goal_completed",
1911
- goalId: completedGoal.id,
1912
- archivePath: completedGoal.archivedPath,
1913
- at: nowIso(),
1914
- });
1915
- } catch {
1916
- // Ledger append failure should not crash completion
1917
- }
1918
- }
1893
+ state.goal = {
1894
+ ...auditTarget,
1895
+ status: "complete",
1896
+ stopReason: "agent",
1897
+ updatedAt: nowIso(),
1898
+ };
1899
+ state.goal = writeActiveGoalFile(ctx, state.goal);
1900
+ pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
1901
+ turnStoppedFor = state.goal?.id ?? null;
1902
+ resetGetGoalNudgeState(state.goal?.id);
1903
+ syncGoalTools();
1904
+ updateUI(ctx);
1919
1905
  return {
1920
1906
  content: [{
1921
1907
  type: "text",
1922
1908
  text: buildCompletionReport({
1923
- detailedSummary: detailedSummary(completedGoal),
1909
+ detailedSummary: detailedSummary(state.goal),
1924
1910
  completionSummary: params.completionSummary,
1911
+ auditSkippedReason: "auditor bypassed (user pressed Escape during audit)",
1925
1912
  }),
1926
1913
  }],
1927
- details: goalDetails(completedGoal),
1914
+ details: goalDetails(state.goal),
1928
1915
  terminate: true,
1929
1916
  };
1930
1917
  }
@@ -1989,45 +1976,35 @@ export default function goalExtension(pi: ExtensionAPI): void {
1989
1976
  display: true,
1990
1977
  details: { phase: "approved", goalId: auditTarget.id, auditor: auditor.model },
1991
1978
  });
1992
- // Account for any remaining elapsed time before stopping.
1979
+ // Account for any remaining elapsed time.
1980
+ // Defer archival: set goal complete in-memory + write active file WITHOUT
1981
+ // archiving. Archival happens at turn_end so the agent can see the auditor
1982
+ // approval before the goal is archived.
1993
1983
  accountProgress(ctx);
1994
- state.goal = auditTarget;
1995
- stopActiveGoal("complete", "agent", ctx);
1996
- const completedGoal = state.goal;
1997
- // C9 fix: mark turn-stopped so subsequent in-turn tool calls are blocked.
1998
- turnStoppedFor = completedGoal?.id ?? null;
1999
- // Clear auditor progress to restore normal widget state
2000
1984
  auditProgress = null;
2001
1985
  goalWidgetComponent?.invalidate();
2002
- if (completedGoal) {
2003
- resetGetGoalNudgeState(completedGoal.id);
2004
- goalsById.delete(completedGoal.id);
2005
- focusedGoalId = null;
2006
- appendFocusEntry(null, "completed");
2007
- syncGoalTools();
2008
- updateUI(ctx);
2009
- // Append ledger: goal completed
2010
- try {
2011
- appendGoalEvent(ctx, {
2012
- type: "goal_completed",
2013
- goalId: completedGoal.id,
2014
- archivePath: completedGoal.archivedPath,
2015
- at: nowIso(),
2016
- });
2017
- } catch {
2018
- // Ledger append failure should not crash completion
2019
- }
2020
- }
1986
+ state.goal = {
1987
+ ...auditTarget,
1988
+ status: "complete",
1989
+ stopReason: "agent",
1990
+ updatedAt: nowIso(),
1991
+ };
1992
+ state.goal = writeActiveGoalFile(ctx, state.goal);
1993
+ pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
1994
+ turnStoppedFor = state.goal?.id ?? null;
1995
+ resetGetGoalNudgeState(state.goal?.id);
1996
+ syncGoalTools();
1997
+ updateUI(ctx);
2021
1998
  return {
2022
1999
  content: [{
2023
2000
  type: "text",
2024
2001
  text: buildCompletionReport({
2025
- detailedSummary: detailedSummary(completedGoal),
2002
+ detailedSummary: detailedSummary(state.goal),
2026
2003
  completionSummary: params.completionSummary,
2027
2004
  auditorReport: auditor.output,
2028
2005
  }),
2029
2006
  }],
2030
- details: goalDetails(completedGoal),
2007
+ details: goalDetails(state.goal),
2031
2008
  terminate: true,
2032
2009
  };
2033
2010
  },
@@ -2403,6 +2380,31 @@ export default function goalExtension(pi: ExtensionAPI): void {
2403
2380
  return;
2404
2381
  }
2405
2382
  refreshGoalDisplayFromDisk(ctx);
2383
+
2384
+ // Archive a goal that was marked complete by update_goal but whose archival
2385
+ // was deferred so the agent could see/recognize the audit result first.
2386
+ // This runs after the agent's turn ends — the agent has now seen the result.
2387
+ if (state.goal?.status === "complete" && !state.goal?.archivedPath) {
2388
+ const completedGoal = state.goal;
2389
+ const archived = archiveGoalFile(ctx, completedGoal);
2390
+ resetGetGoalNudgeState(completedGoal.id);
2391
+ goalsById.delete(completedGoal.id);
2392
+ focusedGoalId = null;
2393
+ appendFocusEntry(null, "completed");
2394
+ syncGoalTools();
2395
+ updateUI(ctx);
2396
+ try {
2397
+ appendGoalEvent(ctx, {
2398
+ type: "goal_completed",
2399
+ goalId: completedGoal.id,
2400
+ archivePath: archived.archivedPath,
2401
+ at: nowIso(),
2402
+ });
2403
+ } catch {
2404
+ // Ledger append failure should not crash completion
2405
+ }
2406
+ }
2407
+
2406
2408
  // If the assistant ended a turn without queuing more tool calls, push a continuation right away.
2407
2409
  // #4: only queue if some real work was done this turn — otherwise the model is
2408
2410
  // just chatting and we should not keep firing turns on noise.
@@ -205,7 +205,6 @@ export function parseGoalFile(filePath: string): GoalRecord | null {
205
205
  }
206
206
 
207
207
  export function writeActiveGoalFile(ctx: GoalFileContext, current: GoalRecord): GoalRecord {
208
- if (current.status === "complete") return archiveGoalFile(ctx, current);
209
208
  const activePath = activePathForGoal(ctx, current);
210
209
  const next = sanitizeGoalPaths(ctx, { ...current, activePath, updatedAt: nowIso() });
211
210
  atomicWriteGoalFile(ctx, GOALS_DIR, activePath, serializeGoalFile(next));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-x",
3
- "version": "0.7.2",
3
+ "version": "0.8.2",
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",