pi-goal-x 0.8.1 → 0.9.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 +37 -8
- package/extensions/goal-policy.ts +15 -4
- package/extensions/goal.ts +145 -87
- package/extensions/prompts/goal-prompts.ts +5 -1
- package/extensions/storage/goal-files.ts +0 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,20 +1,49 @@
|
|
|
1
1
|
# pi-goal-x
|
|
2
2
|
|
|
3
|
-
> **Fork of [@capyup/pi-goal](https://github.com/capyup/pi-goal)
|
|
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, mid-flight objective updates, deferred archival, 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
|
|
9
|
+
## What's different from upstream
|
|
10
10
|
|
|
11
|
-
pi-goal-
|
|
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
|
-
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
13
|
+
### Mid-flight objective updates
|
|
14
|
+
|
|
15
|
+
- **`update_goal({updatedObjective})`** — the agent can now sync the goal objective mid-flight when user requirements change, *without* completing the goal. This ensures the completion auditor evaluates against the latest requirements. The combined path (`updatedObjective` + `status: "complete"`) applies the update first, then runs the normal completion+audit flow.
|
|
16
|
+
- **`apply_goal_tweak`** remains available for `/goal-tweak` drafting revisions; the new parameter is the lightest possible touch on the existing `update_goal` tool.
|
|
17
|
+
|
|
18
|
+
### Deferred archival
|
|
19
|
+
|
|
20
|
+
- **No more premature archiving**: previously, `update_goal` archived the goal file inline within the tool handler before the agent could see the audit result (or skip notification). Archival is now deferred until `turn_end` — after the agent has received the audit/skip result in the conversation. The goal remains visible in the active pool through the entire completion flow.
|
|
21
|
+
- **Cleaner lifecycle**: completed goals are archived by the `turn_end` lifecycle hook, not by the tool handler. The `accountProgress` guard skips disk reconciliation for completed goals.
|
|
22
|
+
|
|
23
|
+
### E2e test infrastructure
|
|
24
|
+
|
|
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).
|
|
27
|
+
|
|
28
|
+
### Completion auditor
|
|
29
|
+
|
|
30
|
+
- **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.
|
|
31
|
+
- **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.
|
|
32
|
+
- **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`.
|
|
33
|
+
- **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.
|
|
34
|
+
- **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.
|
|
35
|
+
- **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.
|
|
36
|
+
- **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.
|
|
37
|
+
- **Session factory injection** — `runGoalCompletionAuditor` accepts an optional `createSession` parameter for testability, enabling mock auditor sessions in tests.
|
|
38
|
+
|
|
39
|
+
### Drafting & UX
|
|
40
|
+
|
|
41
|
+
- **Normalized proposal-refinement language** — consistent terminology ("keep refining through normal proposal cycles") across all drafting prompts and tools.
|
|
42
|
+
- **`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).
|
|
43
|
+
|
|
44
|
+
### Testing
|
|
45
|
+
|
|
46
|
+
- **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
47
|
|
|
19
48
|
## What it provides
|
|
20
49
|
|
|
@@ -43,6 +43,14 @@ export function validateGoalCompletion(args: {
|
|
|
43
43
|
return { ok: true };
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export function validateGoalUpdate(args: {
|
|
47
|
+
goal: GoalPolicyRecordLike | null;
|
|
48
|
+
}): PolicyValidation {
|
|
49
|
+
if (!args.goal) return { ok: false, message: "No goal is set; cannot update objective." };
|
|
50
|
+
if (args.goal.status === "complete") return { ok: false, message: "Goal is already complete; cannot update objective." };
|
|
51
|
+
return { ok: true };
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
export function validateGoalAbort(args: {
|
|
47
55
|
goal: GoalPolicyRecordLike | null;
|
|
48
56
|
runningGoalId?: string | null;
|
|
@@ -116,11 +124,14 @@ export function abortGoalCommandMessage(args: { archived: boolean; wasDrafting:
|
|
|
116
124
|
return args.archived ? "Goal aborted and archived." : args.wasDrafting ? "Drafting cancelled." : "No goal is set.";
|
|
117
125
|
}
|
|
118
126
|
|
|
119
|
-
export function buildCompletionReport(args: { detailedSummary: string; completionSummary?: string | null; auditorReport?: string | null }): string {
|
|
127
|
+
export function buildCompletionReport(args: { detailedSummary: string; completionSummary?: string | null; auditorReport?: string | null; auditSkippedReason?: string | null }): string {
|
|
128
|
+
const auditSkipped = args.auditSkippedReason?.trim();
|
|
120
129
|
const auditorReport = args.auditorReport?.trim();
|
|
121
|
-
const lines =
|
|
122
|
-
? ["Goal audit
|
|
123
|
-
:
|
|
130
|
+
const lines = auditSkipped
|
|
131
|
+
? ["Goal audit skipped.", "", "Reason: " + auditSkipped, "", "Goal complete."]
|
|
132
|
+
: auditorReport
|
|
133
|
+
? ["Goal audit approved.", "", "Auditor approval:", auditorReport, "", "Goal complete."]
|
|
134
|
+
: ["Goal complete."];
|
|
124
135
|
const summary = args.completionSummary?.trim();
|
|
125
136
|
if (summary) {
|
|
126
137
|
lines.push("", "Completion summary:", summary);
|
package/extensions/goal.ts
CHANGED
|
@@ -106,6 +106,7 @@ import {
|
|
|
106
106
|
shouldInjectPostCompactReminder,
|
|
107
107
|
validateGoalAbort,
|
|
108
108
|
validateGoalCompletion,
|
|
109
|
+
validateGoalUpdate,
|
|
109
110
|
validatePauseGoal,
|
|
110
111
|
validateResumeGoal,
|
|
111
112
|
} from "./goal-policy.ts";
|
|
@@ -667,7 +668,8 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
667
668
|
clearActiveAccounting();
|
|
668
669
|
return;
|
|
669
670
|
}
|
|
670
|
-
|
|
671
|
+
// Skip disk reconciliation for complete goals — they are pending archival at turn_end.
|
|
672
|
+
if (state.goal?.activePath && state.goal?.status !== "complete" && !reconcileFocusedGoalFromDisk(ctx, { preserveMemoryUsage: true })) return;
|
|
671
673
|
if (!state.goal || state.goal.status !== "active" || accounting.activeGoalId !== state.goal.id) {
|
|
672
674
|
beginAccounting();
|
|
673
675
|
return;
|
|
@@ -1703,16 +1705,70 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
1703
1705
|
"Do not call update_goal merely because work is stopping, substantial progress was made, or tests passed without covering every requirement.",
|
|
1704
1706
|
"Do not use update_goal=complete as an escape hatch when you are blocked. If you are blocked, call pause_goal({reason, suggestedAction?}) instead so the user can intervene.",
|
|
1705
1707
|
"For sisyphus goals, do not mark complete until every numbered step has been executed and individually verified against its done criterion.",
|
|
1708
|
+
"If the user gives requirements, feedback, or corrections that differ from the goal objective, the goal is stale. Use update_goal with updatedObjective to sync the objective before continuing work or before marking the goal complete. This ensures the auditor evaluates against the latest requirements.",
|
|
1706
1709
|
],
|
|
1707
1710
|
parameters: Type.Object({
|
|
1708
|
-
status: StringEnum([COMPLETE_STATUS] as const, { description: "Set to complete only when the objective is achieved." }),
|
|
1711
|
+
status: Type.Optional(StringEnum([COMPLETE_STATUS] as const, { description: "Set to complete only when the objective is achieved." })),
|
|
1709
1712
|
completionSummary: Type.Optional(Type.String({ description: "Concise completion claim and evidence summary passed to the independent auditor agent." })),
|
|
1710
1713
|
confirmBypassAuditor: Type.Optional(Type.Boolean({ description: "Set to true to confirm bypassing the independent auditor when it is disabled in settings." })),
|
|
1714
|
+
updatedObjective: Type.Optional(Type.String({ description: "Revised goal objective. Use when the user's requirements have changed mid-flight. The goal remains active so the agent can continue working toward the new objective. Can be combined with status=complete to update the objective before the completion audit." })),
|
|
1711
1715
|
}),
|
|
1712
1716
|
executionMode: "sequential",
|
|
1713
1717
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
1714
1718
|
reconcileFocusedGoalFromDisk(ctx);
|
|
1715
|
-
|
|
1719
|
+
|
|
1720
|
+
// -- Phase 1: Objective update (quick sync) --
|
|
1721
|
+
// Apply updatedObjective before any completion logic so the completion
|
|
1722
|
+
// flow (if status=complete is also set) reads the latest objective.
|
|
1723
|
+
if (params.updatedObjective !== undefined) {
|
|
1724
|
+
const newObjective = params.updatedObjective.trim();
|
|
1725
|
+
if (!newObjective) throw new Error("update_goal requires a non-empty updatedObjective.");
|
|
1726
|
+
const updateGate = validateGoalUpdate({ goal: state.goal });
|
|
1727
|
+
if (!updateGate.ok) {
|
|
1728
|
+
return {
|
|
1729
|
+
content: [{ type: "text", text: updateGate.message }],
|
|
1730
|
+
details: goalDetails(state.goal),
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
if (!state.goal) throw new Error("Goal disappeared during objective update.");
|
|
1734
|
+
const next: GoalRecord = {
|
|
1735
|
+
...state.goal,
|
|
1736
|
+
objective: newObjective,
|
|
1737
|
+
updatedAt: nowIso(),
|
|
1738
|
+
};
|
|
1739
|
+
state.goal = writeActiveGoalFile(ctx, next);
|
|
1740
|
+
pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
|
|
1741
|
+
try {
|
|
1742
|
+
appendGoalEvent(ctx, {
|
|
1743
|
+
type: "goal_tweaked",
|
|
1744
|
+
goalId: state.goal.id,
|
|
1745
|
+
changeSummary: "Objective updated via update_goal",
|
|
1746
|
+
at: state.goal.updatedAt,
|
|
1747
|
+
});
|
|
1748
|
+
} catch {
|
|
1749
|
+
// Ledger append failure should not block update
|
|
1750
|
+
}
|
|
1751
|
+
updateUI(ctx);
|
|
1752
|
+
|
|
1753
|
+
// Quick sync only (no status=complete) — return without terminating
|
|
1754
|
+
if (params.status !== COMPLETE_STATUS) {
|
|
1755
|
+
return {
|
|
1756
|
+
content: [{ type: "text", text: `Goal objective updated.` }],
|
|
1757
|
+
details: goalDetails(state.goal),
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
// Fall through: status=complete also set, proceed with completion below
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
// -- Phase 2: Status validation --
|
|
1764
|
+
if (params.status !== COMPLETE_STATUS) {
|
|
1765
|
+
if (params.updatedObjective === undefined) {
|
|
1766
|
+
throw new Error("update_goal requires either status=complete or updatedObjective.");
|
|
1767
|
+
}
|
|
1768
|
+
throw new Error("update_goal requires status=complete when marking a goal complete.");
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// -- Phase 3: Completion --
|
|
1716
1772
|
const completionGate = validateGoalCompletion({ goal: state.goal, runningGoalId });
|
|
1717
1773
|
if (!completionGate.ok) {
|
|
1718
1774
|
return {
|
|
@@ -1751,13 +1807,16 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
1751
1807
|
details: goalDetails(state.goal),
|
|
1752
1808
|
};
|
|
1753
1809
|
}
|
|
1754
|
-
// Auditor disabled and confirmed — skip audit
|
|
1755
|
-
|
|
1810
|
+
// Auditor disabled and confirmed — skip audit.
|
|
1811
|
+
// Defer archival: set goal complete in-memory + write active file WITHOUT
|
|
1812
|
+
// archiving. Archival happens at turn_end so the agent has a chance to
|
|
1813
|
+
// recognise the skipped audit before the goal is archived.
|
|
1814
|
+
pi.sendMessage<GoalAuditEventDetails>({
|
|
1756
1815
|
customType: GOAL_AUDIT_ENTRY,
|
|
1757
1816
|
content: `Goal completed — auditor disabled in settings.`,
|
|
1758
1817
|
display: true,
|
|
1759
1818
|
details: { phase: "skipped", goalId: auditTarget.id, auditor: auditorLabel },
|
|
1760
|
-
}
|
|
1819
|
+
});
|
|
1761
1820
|
try {
|
|
1762
1821
|
appendGoalEvent(ctx, {
|
|
1763
1822
|
type: "audit_skipped",
|
|
@@ -1771,41 +1830,32 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
1771
1830
|
} catch {
|
|
1772
1831
|
// Ledger append failure should not block completion
|
|
1773
1832
|
}
|
|
1774
|
-
//
|
|
1833
|
+
// Set goal complete in memory (defer archival to turn_end)
|
|
1775
1834
|
accountProgress(ctx);
|
|
1776
|
-
state.goal = auditTarget;
|
|
1777
|
-
stopActiveGoal("complete", "agent", ctx);
|
|
1778
|
-
const completedGoal = state.goal;
|
|
1779
|
-
turnStoppedFor = completedGoal?.id ?? null;
|
|
1780
1835
|
auditProgress = null;
|
|
1781
1836
|
goalWidgetComponent?.invalidate();
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
at: nowIso(),
|
|
1795
|
-
});
|
|
1796
|
-
} catch {
|
|
1797
|
-
// Ledger append failure should not crash completion
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1837
|
+
state.goal = {
|
|
1838
|
+
...auditTarget,
|
|
1839
|
+
status: "complete",
|
|
1840
|
+
stopReason: "agent",
|
|
1841
|
+
updatedAt: nowIso(),
|
|
1842
|
+
};
|
|
1843
|
+
state.goal = writeActiveGoalFile(ctx, state.goal);
|
|
1844
|
+
pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
|
|
1845
|
+
turnStoppedFor = state.goal?.id ?? null;
|
|
1846
|
+
resetGetGoalNudgeState(state.goal?.id);
|
|
1847
|
+
syncGoalTools();
|
|
1848
|
+
updateUI(ctx);
|
|
1800
1849
|
return {
|
|
1801
1850
|
content: [{
|
|
1802
1851
|
type: "text",
|
|
1803
1852
|
text: buildCompletionReport({
|
|
1804
|
-
detailedSummary: detailedSummary(
|
|
1853
|
+
detailedSummary: detailedSummary(state.goal),
|
|
1805
1854
|
completionSummary: params.completionSummary,
|
|
1855
|
+
auditSkippedReason: "auditor disabled in settings",
|
|
1806
1856
|
}),
|
|
1807
1857
|
}],
|
|
1808
|
-
details: goalDetails(
|
|
1858
|
+
details: goalDetails(state.goal),
|
|
1809
1859
|
terminate: true,
|
|
1810
1860
|
};
|
|
1811
1861
|
}
|
|
@@ -1884,47 +1934,39 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
1884
1934
|
// skip notification is exposed exactly once to the agent as part of the
|
|
1885
1935
|
// update_goal tool execution, matching the disabled-flow pattern exactly.
|
|
1886
1936
|
if (auditor.error === "Auditor aborted.") {
|
|
1887
|
-
|
|
1937
|
+
// Esc-skip: same deferred archival pattern as disabled bypass.
|
|
1938
|
+
pi.sendMessage<GoalAuditEventDetails>({
|
|
1888
1939
|
customType: GOAL_AUDIT_ENTRY,
|
|
1889
1940
|
content: `Goal completed — auditor bypassed (user pressed Escape during audit).`,
|
|
1890
1941
|
display: true,
|
|
1891
1942
|
details: { phase: "skipped", goalId: auditTarget.id, auditor: auditorLabel },
|
|
1892
|
-
}
|
|
1893
|
-
//
|
|
1943
|
+
});
|
|
1944
|
+
// Set goal complete in memory (defer archival to turn_end)
|
|
1894
1945
|
accountProgress(ctx);
|
|
1895
|
-
state.goal = auditTarget;
|
|
1896
|
-
stopActiveGoal("complete", "agent", ctx);
|
|
1897
|
-
const completedGoal = state.goal;
|
|
1898
|
-
turnStoppedFor = completedGoal?.id ?? null;
|
|
1899
1946
|
auditProgress = null;
|
|
1900
1947
|
goalWidgetComponent?.invalidate();
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
at: nowIso(),
|
|
1914
|
-
});
|
|
1915
|
-
} catch {
|
|
1916
|
-
// Ledger append failure should not crash completion
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1948
|
+
state.goal = {
|
|
1949
|
+
...auditTarget,
|
|
1950
|
+
status: "complete",
|
|
1951
|
+
stopReason: "agent",
|
|
1952
|
+
updatedAt: nowIso(),
|
|
1953
|
+
};
|
|
1954
|
+
state.goal = writeActiveGoalFile(ctx, state.goal);
|
|
1955
|
+
pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
|
|
1956
|
+
turnStoppedFor = state.goal?.id ?? null;
|
|
1957
|
+
resetGetGoalNudgeState(state.goal?.id);
|
|
1958
|
+
syncGoalTools();
|
|
1959
|
+
updateUI(ctx);
|
|
1919
1960
|
return {
|
|
1920
1961
|
content: [{
|
|
1921
1962
|
type: "text",
|
|
1922
1963
|
text: buildCompletionReport({
|
|
1923
|
-
detailedSummary: detailedSummary(
|
|
1964
|
+
detailedSummary: detailedSummary(state.goal),
|
|
1924
1965
|
completionSummary: params.completionSummary,
|
|
1966
|
+
auditSkippedReason: "auditor bypassed (user pressed Escape during audit)",
|
|
1925
1967
|
}),
|
|
1926
1968
|
}],
|
|
1927
|
-
details: goalDetails(
|
|
1969
|
+
details: goalDetails(state.goal),
|
|
1928
1970
|
terminate: true,
|
|
1929
1971
|
};
|
|
1930
1972
|
}
|
|
@@ -1989,50 +2031,41 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
1989
2031
|
display: true,
|
|
1990
2032
|
details: { phase: "approved", goalId: auditTarget.id, auditor: auditor.model },
|
|
1991
2033
|
});
|
|
1992
|
-
// Account for any remaining elapsed time
|
|
2034
|
+
// Account for any remaining elapsed time.
|
|
2035
|
+
// Defer archival: set goal complete in-memory + write active file WITHOUT
|
|
2036
|
+
// archiving. Archival happens at turn_end so the agent can see the auditor
|
|
2037
|
+
// approval before the goal is archived.
|
|
1993
2038
|
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
2039
|
auditProgress = null;
|
|
2001
2040
|
goalWidgetComponent?.invalidate();
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
archivePath: completedGoal.archivedPath,
|
|
2015
|
-
at: nowIso(),
|
|
2016
|
-
});
|
|
2017
|
-
} catch {
|
|
2018
|
-
// Ledger append failure should not crash completion
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2041
|
+
state.goal = {
|
|
2042
|
+
...auditTarget,
|
|
2043
|
+
status: "complete",
|
|
2044
|
+
stopReason: "agent",
|
|
2045
|
+
updatedAt: nowIso(),
|
|
2046
|
+
};
|
|
2047
|
+
state.goal = writeActiveGoalFile(ctx, state.goal);
|
|
2048
|
+
pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
|
|
2049
|
+
turnStoppedFor = state.goal?.id ?? null;
|
|
2050
|
+
resetGetGoalNudgeState(state.goal?.id);
|
|
2051
|
+
syncGoalTools();
|
|
2052
|
+
updateUI(ctx);
|
|
2021
2053
|
return {
|
|
2022
2054
|
content: [{
|
|
2023
2055
|
type: "text",
|
|
2024
2056
|
text: buildCompletionReport({
|
|
2025
|
-
detailedSummary: detailedSummary(
|
|
2057
|
+
detailedSummary: detailedSummary(state.goal),
|
|
2026
2058
|
completionSummary: params.completionSummary,
|
|
2027
2059
|
auditorReport: auditor.output,
|
|
2028
2060
|
}),
|
|
2029
2061
|
}],
|
|
2030
|
-
details: goalDetails(
|
|
2062
|
+
details: goalDetails(state.goal),
|
|
2031
2063
|
terminate: true,
|
|
2032
2064
|
};
|
|
2033
2065
|
},
|
|
2034
2066
|
renderCall(args, theme) {
|
|
2035
|
-
|
|
2067
|
+
const label = args?.status ?? args?.updatedObjective ? "sync" : "";
|
|
2068
|
+
return new Text(theme.fg("toolTitle", "update_goal ") + theme.fg("success", label), 0, 0);
|
|
2036
2069
|
},
|
|
2037
2070
|
renderResult(result, _options, theme) {
|
|
2038
2071
|
return renderGoalResult(result, theme);
|
|
@@ -2403,6 +2436,31 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
2403
2436
|
return;
|
|
2404
2437
|
}
|
|
2405
2438
|
refreshGoalDisplayFromDisk(ctx);
|
|
2439
|
+
|
|
2440
|
+
// Archive a goal that was marked complete by update_goal but whose archival
|
|
2441
|
+
// was deferred so the agent could see/recognize the audit result first.
|
|
2442
|
+
// This runs after the agent's turn ends — the agent has now seen the result.
|
|
2443
|
+
if (state.goal?.status === "complete" && !state.goal?.archivedPath) {
|
|
2444
|
+
const completedGoal = state.goal;
|
|
2445
|
+
const archived = archiveGoalFile(ctx, completedGoal);
|
|
2446
|
+
resetGetGoalNudgeState(completedGoal.id);
|
|
2447
|
+
goalsById.delete(completedGoal.id);
|
|
2448
|
+
focusedGoalId = null;
|
|
2449
|
+
appendFocusEntry(null, "completed");
|
|
2450
|
+
syncGoalTools();
|
|
2451
|
+
updateUI(ctx);
|
|
2452
|
+
try {
|
|
2453
|
+
appendGoalEvent(ctx, {
|
|
2454
|
+
type: "goal_completed",
|
|
2455
|
+
goalId: completedGoal.id,
|
|
2456
|
+
archivePath: archived.archivedPath,
|
|
2457
|
+
at: nowIso(),
|
|
2458
|
+
});
|
|
2459
|
+
} catch {
|
|
2460
|
+
// Ledger append failure should not crash completion
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2406
2464
|
// If the assistant ended a turn without queuing more tool calls, push a continuation right away.
|
|
2407
2465
|
// #4: only queue if some real work was done this turn — otherwise the model is
|
|
2408
2466
|
// just chatting and we should not keep firing turns on noise.
|
|
@@ -44,7 +44,9 @@ If you hit a real blocker that you cannot resolve with one more reasonable next
|
|
|
44
44
|
|
|
45
45
|
If the user explicitly asks to abandon/cancel this goal, or the objective is obsolete, impossible, or unsafe to continue and should not be marked complete, call abort_goal({reason}) with a non-empty reason and stop.
|
|
46
46
|
|
|
47
|
-
Do NOT silently invent workarounds, fake completion, or quietly redefine the objective. Do NOT call update_goal=complete to escape a blocker
|
|
47
|
+
Do NOT silently invent workarounds, fake completion, or quietly redefine the objective. Do NOT call update_goal=complete to escape a blocker.
|
|
48
|
+
|
|
49
|
+
Goal evolution: if the user gives requirements, feedback, or corrections that differ from the goal objective, the goal is stale. Propose the updated objective concisely and wait for the user to confirm before continuing. Use update_goal with updatedObjective for narrow focus-area changes, or suggest /goal-tweak for broader revisions (boundaries, constraints, multiple sections). Do NOT mark the goal complete with a stale objective.${sisyphusDisciplineBlock(goal) ? `\n${sisyphusDisciplineBlock(goal)}` : ""}`;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export function continuationPrompt(goal: GoalRecord): string {
|
|
@@ -77,6 +79,8 @@ export function continuationPrompt(goal: GoalRecord): string {
|
|
|
77
79
|
"Do not call update_goal unless the goal is complete enough to survive independent semantic auditing. Do not mark a goal complete merely because work is stopping.",
|
|
78
80
|
"Do not ask the user for confirmation unless there is a real blocker.",
|
|
79
81
|
"",
|
|
82
|
+
"Goal evolution: if the user gives requirements, feedback, or corrections that differ from the goal objective, the goal is stale. Propose the updated objective concisely and wait for the user to confirm before continuing. Use update_goal with updatedObjective for narrow focus-area changes, or suggest /goal-tweak for broader revisions (boundaries, constraints, multiple sections). Do NOT mark the goal complete with a stale objective.",
|
|
83
|
+
"",
|
|
80
84
|
"If you hit a real blocker (missing credentials, contradictory spec, file/permission you cannot access, dangerous operation pending user approval, or an unclear Sisyphus-style ordered plan), call pause_goal({reason, suggestedAction?}) and stop. If the user explicitly asks to abandon/cancel, or the objective is obsolete, impossible, or unsafe to continue, call abort_goal({reason}) and stop. Do not silently invent workarounds. Do not fake completion. pause_goal and abort_goal are structured lifecycle exits; update_goal=complete is not an escape hatch for blockers.",
|
|
81
85
|
...(goal.sisyphus ? ["", sisyphusDisciplineBlock(goal)] : []),
|
|
82
86
|
].join("\n");
|
|
@@ -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.
|
|
3
|
+
"version": "0.9.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",
|