pi-goal-x 0.10.1 → 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 +8 -7
- package/extensions/goal-draft.ts +27 -0
- package/extensions/goal-tool-names.ts +2 -3
- package/extensions/goal.ts +159 -176
- package/extensions/prompts/goal-prompts.ts +9 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,10 +10,10 @@ The extension is designed around one rule: **the user owns intent; the agent exe
|
|
|
10
10
|
|
|
11
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
|
-
###
|
|
13
|
+
### Goal objective is immutable
|
|
14
14
|
|
|
15
|
-
-
|
|
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,
|
|
@@ -106,7 +107,6 @@ import {
|
|
|
106
107
|
shouldInjectPostCompactReminder,
|
|
107
108
|
validateGoalAbort,
|
|
108
109
|
validateGoalCompletion,
|
|
109
|
-
validateGoalUpdate,
|
|
110
110
|
validatePauseGoal,
|
|
111
111
|
validateResumeGoal,
|
|
112
112
|
} from "./goal-policy.ts";
|
|
@@ -128,14 +128,14 @@ const GOAL_PROGRESS_TOOL_SET = new Set<string>(GOAL_PROGRESS_TOOL_NAMES);
|
|
|
128
128
|
|
|
129
129
|
/**
|
|
130
130
|
* Tools that are NEVER blocked by the post-stop in-turn block. After pause_goal,
|
|
131
|
-
* abort_goal, update_goal=complete
|
|
131
|
+
* abort_goal, or update_goal=complete fires, the agent should
|
|
132
132
|
* yield the turn; we block all subsequent tool calls except these read-only inspections.
|
|
133
133
|
*/
|
|
134
134
|
const POST_STOP_ALLOWED_TOOL_SET = new Set<string>(POST_STOP_ALLOWED_TOOLS);
|
|
135
135
|
|
|
136
136
|
/**
|
|
137
137
|
* When non-null, /goal-tweak drafting is in progress for this goal id and the
|
|
138
|
-
* agent is allowed to call
|
|
138
|
+
* agent is allowed to call propose_goal_tweak. Cleared after the tweak is applied
|
|
139
139
|
* or when a user-driven turn arrives without a tweak follow-through. This is
|
|
140
140
|
* the schema-level affordance gate that prevents the agent from "tweaking" via
|
|
141
141
|
* arbitrary write/edit calls.
|
|
@@ -376,7 +376,7 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
376
376
|
// Per-turn flags reset in turn_start (#4, C9 fix).
|
|
377
377
|
// goalWorkToolCalledThisTurn: tracks whether a real goal-work tool was called.
|
|
378
378
|
// If false at turn_end, we don't queue another autoContinue (empty chat turn).
|
|
379
|
-
// turnStoppedFor: set by pause_goal / update_goal(complete) /
|
|
379
|
+
// turnStoppedFor: set by pause_goal / update_goal(complete) / propose_goal_tweak
|
|
380
380
|
// after their successful execute. Once set, pi.on("tool_call") blocks all
|
|
381
381
|
// subsequent in-turn tool calls except POST_STOP_ALLOWED_TOOLS. This is the
|
|
382
382
|
// schema fix for "agent keeps writing files after pause_goal".
|
|
@@ -393,18 +393,6 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
393
393
|
lastAccountedAt: null as number | null,
|
|
394
394
|
};
|
|
395
395
|
|
|
396
|
-
const draftingHiddenWorkTools = [
|
|
397
|
-
"bash",
|
|
398
|
-
"read",
|
|
399
|
-
"write",
|
|
400
|
-
"edit",
|
|
401
|
-
"grep",
|
|
402
|
-
"find",
|
|
403
|
-
"ls",
|
|
404
|
-
SISYPHUS_STEP_TOOL_NAME,
|
|
405
|
-
TWEAK_APPLY_TOOL_NAME,
|
|
406
|
-
CREATE_GOAL_TOOL_NAME,
|
|
407
|
-
] as const;
|
|
408
396
|
const goalExecutionWorkTools = ["read", "bash", "edit", "write"] as const;
|
|
409
397
|
|
|
410
398
|
function syncGoalTools(): void {
|
|
@@ -421,15 +409,16 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
421
409
|
// mechanism. Keep step_complete registered for legacy transcripts, but do
|
|
422
410
|
// not expose it as an active work tool.
|
|
423
411
|
active.delete(SISYPHUS_STEP_TOOL_NAME);
|
|
424
|
-
//
|
|
412
|
+
// propose_goal_tweak is only available during a /goal-tweak drafting flow.
|
|
425
413
|
// Note: tweak drafting can run against active OR paused goals.
|
|
426
414
|
if (state.goal && tweakDraftingFor === state.goal.id) {
|
|
427
|
-
active.add(
|
|
415
|
+
active.add(PROPOSE_TWEAK_TOOL_NAME);
|
|
428
416
|
active.add(QUESTION_TOOL_NAME);
|
|
429
417
|
active.add(QUESTIONNAIRE_TOOL_NAME);
|
|
430
418
|
} else {
|
|
431
|
-
active.delete(
|
|
419
|
+
active.delete(PROPOSE_TWEAK_TOOL_NAME);
|
|
432
420
|
}
|
|
421
|
+
|
|
433
422
|
// Keep the commit tool available and let its validator enforce that a
|
|
434
423
|
// drafting flow is active. This avoids fragile hidden-tool drift after
|
|
435
424
|
// question turns, compaction, or active-tool resync.
|
|
@@ -1054,11 +1043,11 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
1054
1043
|
if (!focused) return;
|
|
1055
1044
|
const sisyphusOn = focused.sisyphus;
|
|
1056
1045
|
const label = sisyphusOn ? "Sisyphus tweak drafting" : "Goal tweak drafting";
|
|
1057
|
-
// Activate the tweak edit-gate so
|
|
1046
|
+
// Activate the tweak edit-gate so propose_goal_tweak is callable.
|
|
1058
1047
|
tweakDraftingFor = focused.id;
|
|
1059
1048
|
syncGoalTools();
|
|
1060
1049
|
ctx.ui.notify(
|
|
1061
|
-
`${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.`,
|
|
1062
1051
|
"info",
|
|
1063
1052
|
);
|
|
1064
1053
|
const draftId = `tweak-${focused.id}-${Date.now().toString(36)}`;
|
|
@@ -1696,84 +1685,182 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
1696
1685
|
}));
|
|
1697
1686
|
|
|
1698
1687
|
pi.registerTool(defineTool({
|
|
1699
|
-
name:
|
|
1700
|
-
label: "
|
|
1701
|
-
description: "
|
|
1702
|
-
promptSnippet: "
|
|
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.",
|
|
1703
1692
|
promptGuidelines: [
|
|
1704
|
-
"
|
|
1705
|
-
"
|
|
1706
|
-
"
|
|
1707
|
-
"
|
|
1708
|
-
"
|
|
1709
|
-
"
|
|
1710
|
-
"
|
|
1711
|
-
"If you have just run the test suite successfully and the tests all pass, include a testResults object with the exit code (0) and relevant output. The auditor will see this evidence and can skip re-running the tests.",
|
|
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.",
|
|
1712
1700
|
],
|
|
1713
1701
|
parameters: Type.Object({
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
confirmBypassAuditor: Type.Optional(Type.Boolean({ description: "Set to true to confirm bypassing the independent auditor when it is disabled in settings." })),
|
|
1717
|
-
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." })),
|
|
1718
|
-
testResults: Type.Optional(Type.Object({
|
|
1719
|
-
exitCode: Type.Number({ description: "Exit code of the test run (0 = success)" }),
|
|
1720
|
-
suiteName: Type.Optional(Type.String({ description: "Test suite name, e.g. 'npm test'" })),
|
|
1721
|
-
output: Type.Optional(Type.String({ description: "Last lines of test output showing results" })),
|
|
1722
|
-
timestamp: Type.Optional(Type.String({ description: "ISO timestamp of when tests were run" })),
|
|
1723
|
-
}, { description: "Structured test evidence passed to the auditor so it can skip redundant test re-runs. If you have just run the test suite successfully, include this so the auditor accepts the results without re-running." })),
|
|
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)." }),
|
|
1724
1704
|
}),
|
|
1725
1705
|
executionMode: "sequential",
|
|
1726
|
-
async execute(_toolCallId, params,
|
|
1706
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
1727
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 });
|
|
1728
1745
|
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
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");
|
|
1737
1755
|
return {
|
|
1738
|
-
content: [{ type: "text", text:
|
|
1756
|
+
content: [{ type: "text", text: message }],
|
|
1739
1757
|
details: goalDetails(state.goal),
|
|
1740
1758
|
};
|
|
1741
1759
|
}
|
|
1742
|
-
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
if (decision === "confirm") {
|
|
1763
|
+
// Apply the tweak: write the new objective to disk authoritatively.
|
|
1743
1764
|
const next: GoalRecord = {
|
|
1744
1765
|
...state.goal,
|
|
1745
1766
|
objective: newObjective,
|
|
1746
1767
|
updatedAt: nowIso(),
|
|
1768
|
+
// Clear any prior agent pause reason — the user has redefined the work.
|
|
1769
|
+
pauseReason: undefined,
|
|
1770
|
+
pauseSuggestedAction: undefined,
|
|
1747
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
|
|
1748
1782
|
state.goal = writeActiveGoalFile(ctx, next);
|
|
1749
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
|
|
1750
1793
|
try {
|
|
1751
1794
|
appendGoalEvent(ctx, {
|
|
1752
1795
|
type: "goal_tweaked",
|
|
1753
1796
|
goalId: state.goal.id,
|
|
1754
|
-
changeSummary
|
|
1797
|
+
changeSummary,
|
|
1755
1798
|
at: state.goal.updatedAt,
|
|
1756
1799
|
});
|
|
1757
1800
|
} catch {
|
|
1758
|
-
// Ledger append failure should not
|
|
1759
|
-
}
|
|
1760
|
-
updateUI(ctx);
|
|
1761
|
-
|
|
1762
|
-
// Quick sync only (no status=complete) — return without terminating
|
|
1763
|
-
if (params.status !== COMPLETE_STATUS) {
|
|
1764
|
-
return {
|
|
1765
|
-
content: [{ type: "text", text: `Goal objective updated.` }],
|
|
1766
|
-
details: goalDetails(state.goal),
|
|
1767
|
-
};
|
|
1801
|
+
// Ledger append failure should not crash tweak
|
|
1768
1802
|
}
|
|
1769
|
-
|
|
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
|
+
};
|
|
1770
1811
|
}
|
|
1771
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
|
+
|
|
1831
|
+
pi.registerTool(defineTool({
|
|
1832
|
+
name: "update_goal",
|
|
1833
|
+
label: "Update Goal",
|
|
1834
|
+
description: "Mark the current active or paused pi goal complete when the objective is actually achieved.",
|
|
1835
|
+
promptSnippet: "Mark the active or paused pi goal complete when the objective is achieved.",
|
|
1836
|
+
promptGuidelines: [
|
|
1837
|
+
"Use update_goal with status=complete only when the pi goal objective has actually been achieved and no required work remains.",
|
|
1838
|
+
"Before calling update_goal, summarize the evidence you believe proves completion; the tool will launch an independent pi auditor agent to inspect the workspace and judge the claim.",
|
|
1839
|
+
"The auditor is authoritative: completion is archived only if the auditor report ends with <approved/>. If it ends with <disapproved/> or no approval marker, update_goal is rejected and the goal remains open.",
|
|
1840
|
+
"Do not call update_goal merely because work is stopping, substantial progress was made, or tests passed without covering every requirement.",
|
|
1841
|
+
"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.",
|
|
1842
|
+
"For sisyphus goals, do not mark complete until every numbered step has been executed and individually verified against its done criterion.",
|
|
1843
|
+
"The goal objective is immutable. The agent MUST NOT modify the goal objective on its own initiative. If the user gives requirements, feedback, or corrections that differ from the goal objective, ask the user to run /goal-tweak to revise the goal. Use goal_question to confirm when the change is ambiguous.",
|
|
1844
|
+
"If you have just run the test suite successfully and the tests all pass, include a testResults object with the exit code (0) and relevant output. The auditor will see this evidence and can skip re-running the tests.",
|
|
1845
|
+
],
|
|
1846
|
+
parameters: Type.Object({
|
|
1847
|
+
status: Type.Optional(StringEnum([COMPLETE_STATUS] as const, { description: "Set to complete only when the objective is achieved." })),
|
|
1848
|
+
completionSummary: Type.Optional(Type.String({ description: "Concise completion claim and evidence summary passed to the independent auditor agent." })),
|
|
1849
|
+
confirmBypassAuditor: Type.Optional(Type.Boolean({ description: "Set to true to confirm bypassing the independent auditor when it is disabled in settings." })),
|
|
1850
|
+
|
|
1851
|
+
testResults: Type.Optional(Type.Object({
|
|
1852
|
+
exitCode: Type.Number({ description: "Exit code of the test run (0 = success)" }),
|
|
1853
|
+
suiteName: Type.Optional(Type.String({ description: "Test suite name, e.g. 'npm test'" })),
|
|
1854
|
+
output: Type.Optional(Type.String({ description: "Last lines of test output showing results" })),
|
|
1855
|
+
timestamp: Type.Optional(Type.String({ description: "ISO timestamp of when tests were run" })),
|
|
1856
|
+
}, { description: "Structured test evidence passed to the auditor so it can skip redundant test re-runs. If you have just run the test suite successfully, include this so the auditor accepts the results without re-running." })),
|
|
1857
|
+
}, { additionalProperties: false }),
|
|
1858
|
+
executionMode: "sequential",
|
|
1859
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
1860
|
+
reconcileFocusedGoalFromDisk(ctx);
|
|
1861
|
+
|
|
1772
1862
|
// -- Phase 2: Status validation --
|
|
1773
1863
|
if (params.status !== COMPLETE_STATUS) {
|
|
1774
|
-
if (params.updatedObjective === undefined) {
|
|
1775
|
-
throw new Error("update_goal requires either status=complete or updatedObjective.");
|
|
1776
|
-
}
|
|
1777
1864
|
throw new Error("update_goal requires status=complete when marking a goal complete.");
|
|
1778
1865
|
}
|
|
1779
1866
|
|
|
@@ -2074,7 +2161,7 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
2074
2161
|
};
|
|
2075
2162
|
},
|
|
2076
2163
|
renderCall(args, theme) {
|
|
2077
|
-
const label = args?.status ??
|
|
2164
|
+
const label = args?.status ?? "";
|
|
2078
2165
|
return new Text(theme.fg("toolTitle", "update_goal ") + theme.fg("success", label), 0, 0);
|
|
2079
2166
|
},
|
|
2080
2167
|
renderResult(result, _options, theme) {
|
|
@@ -2247,110 +2334,6 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
2247
2334
|
},
|
|
2248
2335
|
}));
|
|
2249
2336
|
|
|
2250
|
-
pi.registerTool(defineTool({
|
|
2251
|
-
name: TWEAK_APPLY_TOOL_NAME,
|
|
2252
|
-
label: "Apply Goal Tweak",
|
|
2253
|
-
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.",
|
|
2254
|
-
promptSnippet: "Apply the revised goal objective produced by a /goal-tweak drafting interview.",
|
|
2255
|
-
promptGuidelines: [
|
|
2256
|
-
"Only call apply_goal_tweak inside a /goal-tweak drafting flow (the prompt makes that explicit). It is rejected at any other time.",
|
|
2257
|
-
"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.",
|
|
2258
|
-
"For Sisyphus goals: preserve the Sisyphus style and ordered-plan wording unless the user explicitly asks to remove it.",
|
|
2259
|
-
"changeSummary is a one-sentence description of WHAT changed (for the activity log and pause messages).",
|
|
2260
|
-
"Do NOT use write/edit/bash to modify the active goal file directly. apply_goal_tweak is the only sanctioned channel.",
|
|
2261
|
-
"After apply_goal_tweak returns, stop. Do not begin new task work in the same turn. The system will queue the next continuation.",
|
|
2262
|
-
],
|
|
2263
|
-
parameters: Type.Object({
|
|
2264
|
-
newObjective: Type.String({ description: "The complete revised objective text. For Sisyphus goals, preserve the Sisyphus style unless the user explicitly changes it." }),
|
|
2265
|
-
changeSummary: Type.String({ description: "One-sentence description of what was changed (used in UI notification and tweak log)." }),
|
|
2266
|
-
}),
|
|
2267
|
-
executionMode: "sequential",
|
|
2268
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
2269
|
-
reconcileFocusedGoalFromDisk(ctx);
|
|
2270
|
-
if (!state.goal) {
|
|
2271
|
-
return {
|
|
2272
|
-
content: [{ type: "text", text: "No goal is set; apply_goal_tweak is a no-op." }],
|
|
2273
|
-
details: goalDetails(state.goal),
|
|
2274
|
-
};
|
|
2275
|
-
}
|
|
2276
|
-
if (tweakDraftingFor !== state.goal.id) {
|
|
2277
|
-
return {
|
|
2278
|
-
content: [{
|
|
2279
|
-
type: "text",
|
|
2280
|
-
text: "apply_goal_tweak REJECTED: no /goal-tweak drafting flow is active for this goal. " +
|
|
2281
|
-
"This tool can only be called during a /goal-tweak drafting interview that the user initiated. " +
|
|
2282
|
-
"If you want to change the goal, ask the user to run /goal-tweak.",
|
|
2283
|
-
}],
|
|
2284
|
-
details: goalDetails(state.goal),
|
|
2285
|
-
};
|
|
2286
|
-
}
|
|
2287
|
-
if (state.goal.status !== "active" && state.goal.status !== "paused") {
|
|
2288
|
-
return {
|
|
2289
|
-
content: [{ type: "text", text: `Goal is ${statusLabel(state.goal)}; cannot apply a tweak.` }],
|
|
2290
|
-
details: goalDetails(state.goal),
|
|
2291
|
-
};
|
|
2292
|
-
}
|
|
2293
|
-
const newObjective = params.newObjective.trim();
|
|
2294
|
-
if (!newObjective) throw new Error("apply_goal_tweak requires a non-empty newObjective.");
|
|
2295
|
-
const changeSummary = params.changeSummary.trim();
|
|
2296
|
-
if (!changeSummary) throw new Error("apply_goal_tweak requires a non-empty changeSummary.");
|
|
2297
|
-
const next: GoalRecord = {
|
|
2298
|
-
...state.goal,
|
|
2299
|
-
objective: newObjective,
|
|
2300
|
-
updatedAt: nowIso(),
|
|
2301
|
-
// Clear any prior agent pause reason — the user has redefined the work.
|
|
2302
|
-
pauseReason: undefined,
|
|
2303
|
-
pauseSuggestedAction: undefined,
|
|
2304
|
-
};
|
|
2305
|
-
// IMPORTANT: bypass setGoal() / persist() here. persist() calls
|
|
2306
|
-
// syncGoalPromptFromDisk() which would RE-READ the stale objective
|
|
2307
|
-
// from the still-old goal file on disk and clobber our new objective
|
|
2308
|
-
// before writing. apply_goal_tweak is the authoritative source for
|
|
2309
|
-
// objective changes — the disk is downstream, not upstream. Do the
|
|
2310
|
-
// minimal state update manually:
|
|
2311
|
-
// 1) write the new record to disk authoritatively
|
|
2312
|
-
// 2) update in-memory `goal` to the canonical post-write record
|
|
2313
|
-
// 3) append the state entry and re-sync tools
|
|
2314
|
-
// 4) clear the tweak drafting gate so apply_goal_tweak can't be re-used
|
|
2315
|
-
state.goal = writeActiveGoalFile(ctx, next);
|
|
2316
|
-
pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
|
|
2317
|
-
tweakDraftingFor = null;
|
|
2318
|
-
// Reset autoContinue counter — plan changed, agent gets a fresh chain.
|
|
2319
|
-
resetGetGoalNudgeState(state.goal.id);
|
|
2320
|
-
// C9 fix: mark turn-stopped so subsequent in-turn tool calls are blocked.
|
|
2321
|
-
turnStoppedFor = state.goal.id;
|
|
2322
|
-
syncGoalTools();
|
|
2323
|
-
updateUI(ctx);
|
|
2324
|
-
ctx.ui.notify(`Goal tweaked: ${truncateText(changeSummary, 160)}`, "info");
|
|
2325
|
-
// Append ledger event for tweak
|
|
2326
|
-
try {
|
|
2327
|
-
appendGoalEvent(ctx, {
|
|
2328
|
-
type: "goal_tweaked",
|
|
2329
|
-
goalId: state.goal.id,
|
|
2330
|
-
changeSummary,
|
|
2331
|
-
at: state.goal.updatedAt,
|
|
2332
|
-
});
|
|
2333
|
-
} catch {
|
|
2334
|
-
// Ledger append failure should not crash tweak
|
|
2335
|
-
}
|
|
2336
|
-
return {
|
|
2337
|
-
content: [{
|
|
2338
|
-
type: "text",
|
|
2339
|
-
text: `Goal tweak applied. ${changeSummary}\nStop now; the next continuation will arrive automatically if the goal is active.`,
|
|
2340
|
-
}],
|
|
2341
|
-
details: goalDetails(state.goal),
|
|
2342
|
-
terminate: true,
|
|
2343
|
-
};
|
|
2344
|
-
},
|
|
2345
|
-
renderCall(args, theme) {
|
|
2346
|
-
const summary = typeof args?.changeSummary === "string" ? truncateText(args.changeSummary, 80) : "";
|
|
2347
|
-
return new Text(theme.fg("toolTitle", "apply_goal_tweak ") + theme.fg("muted", summary), 0, 0);
|
|
2348
|
-
},
|
|
2349
|
-
renderResult(result, _options, theme) {
|
|
2350
|
-
return renderGoalResult(result, theme);
|
|
2351
|
-
},
|
|
2352
|
-
}));
|
|
2353
|
-
|
|
2354
2337
|
syncGoalTools();
|
|
2355
2338
|
|
|
2356
2339
|
pi.on("context", async (event): Promise<{ messages: typeof event.messages } | undefined> => {
|
|
@@ -2400,7 +2383,7 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
2400
2383
|
// #4 + C9 fix + Phase 5 C3: gate in-turn tool calls based on lifecycle state.
|
|
2401
2384
|
pi.on("tool_call", async (event, ctx) => {
|
|
2402
2385
|
// Post-stop in-turn block (C9 0ad8 fix): after pause_goal / abort_goal /
|
|
2403
|
-
// update_goal=complete /
|
|
2386
|
+
// update_goal=complete / propose_goal_tweak fires in this turn, block all subsequent tool calls except
|
|
2404
2387
|
// read-only inspection. Forces the agent to yield the turn instead of "fixing"
|
|
2405
2388
|
// the situation by creating extra files etc.
|
|
2406
2389
|
if (turnStoppedFor !== null && !POST_STOP_ALLOWED_TOOL_SET.has(event.toolName)) {
|
|
@@ -48,7 +48,7 @@ If the user explicitly asks to abandon/cancel this goal, or the objective is obs
|
|
|
48
48
|
|
|
49
49
|
Do NOT silently invent workarounds, fake completion, or quietly redefine the objective. Do NOT call update_goal=complete to escape a blocker.
|
|
50
50
|
|
|
51
|
-
Goal evolution: if the user gives requirements, feedback, or corrections that differ from the goal objective, the goal is stale.
|
|
51
|
+
Goal evolution: if the user gives requirements, feedback, or corrections that differ from the goal objective, the goal is stale. The goal objective is immutable — the agent must NOT modify it autonomously. Propose the updated objective concisely and ask the user to run /goal-tweak to revise it. Do NOT mark the goal complete with a stale objective.${sisyphusDisciplineBlock(goal) ? `\n${sisyphusDisciplineBlock(goal)}` : ""}`;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export function continuationPrompt(goal: GoalRecord): string {
|
|
@@ -83,7 +83,7 @@ export function continuationPrompt(goal: GoalRecord): string {
|
|
|
83
83
|
"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.",
|
|
84
84
|
"Do not ask the user for confirmation unless there is a real blocker.",
|
|
85
85
|
"",
|
|
86
|
-
"Goal evolution: if the user gives requirements, feedback, or corrections that differ from the goal objective, the goal is stale.
|
|
86
|
+
"Goal evolution: if the user gives requirements, feedback, or corrections that differ from the goal objective, the goal is stale. The goal objective is immutable — the agent must NOT modify it autonomously. Propose the updated objective concisely and ask the user to run /goal-tweak to revise it. Do NOT mark the goal complete with a stale objective.",
|
|
87
87
|
"",
|
|
88
88
|
"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.",
|
|
89
89
|
...(goal.sisyphus ? ["", sisyphusDisciplineBlock(goal)] : []),
|
|
@@ -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",
|