opencode-goal-plugin 0.1.14 → 0.2.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/CHANGELOG.md +6 -0
- package/README.md +9 -0
- package/package.json +2 -2
- package/src/goal-plugin.js +83 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.2.0 — 2026-06-14
|
|
6
|
+
|
|
7
|
+
- **Add `/goal edit <new objective>`.** Revise the active goal's objective in place while preserving its turn/token/time budget and lifecycle history. Any pause/blocked state is cleared and `noProgressTurns` resets so the revised goal can continue; a goal already at a hard limit re-pauses on the next idle (use `/goal resume` for a fresh budget window). Ported from prevalentWare/opencode-goal-plugin's `update_goal_objective` tool, adapted to the marker-based command model.
|
|
8
|
+
- **Preserve the goal across session compaction.** A new `experimental.session.compacting` hook injects the goal objective, status, budget usage, elapsed time, and latest checkpoint into the compaction context so a compaction no longer drops the goal thread mid-run. Ported from prevalentWare/opencode-goal-plugin's `compactionContext` injection.
|
|
9
|
+
- **Disable generic post-compaction auto-continue while a goal is active.** A new `experimental.compaction.autocontinue` hook sets `enabled = false` whenever an active (non-stopped) goal is present, so OpenCode's native post-compaction continuation does not race the plugin's own idle-triggered continuation. Paused/stopped goals leave the native behavior untouched. Ported from prevalentWare/opencode-goal-plugin.
|
|
10
|
+
|
|
5
11
|
## 0.1.14 — 2026-06-12
|
|
6
12
|
|
|
7
13
|
- **Count cached context tokens in the budget.** `totalTokensForMessage` now includes `tokens.cache.read` / `cache.write` alongside `input + output + reasoning`. On providers with prompt caching (e.g. Anthropic) most of the conversation context arrives as cache reads with a tiny `input`, so the prior estimate undercounted the context window and the token budget / wrap-up could effectively never trigger.
|
package/README.md
CHANGED
|
@@ -70,6 +70,14 @@ Resume a paused or stopped goal:
|
|
|
70
70
|
/goal resume
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
Edit the active goal's objective without losing its budget or history:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
/goal edit fix the failing tests and also update the docs
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`/goal edit <new objective>` revises the goal in place: the turn, token, and time budget plus the lifecycle history are preserved, and any pause/blocked state is cleared so the revised goal can continue. A goal that already hit a hard limit will re-pause on the next idle — run `/goal resume` for a fresh budget window.
|
|
80
|
+
|
|
73
81
|
Pause without clearing the active goal:
|
|
74
82
|
|
|
75
83
|
```
|
|
@@ -89,6 +97,7 @@ Clear the active goal:
|
|
|
89
97
|
1. When you set a goal, the plugin stores it in session memory and injects it into the system prompt so the assistant keeps it in view on every turn.
|
|
90
98
|
2. Each time the session goes idle, the plugin sends a continuation prompt containing the goal, the remaining budget, and a completion audit asking the assistant to verify the current state before declaring done.
|
|
91
99
|
3. The plugin stops auto-continuing when the assistant ends a response with `[goal:complete]` or `[goal:blocked]`, or when a safety limit is reached.
|
|
100
|
+
4. If OpenCode compacts the session, the plugin injects the goal objective, budget usage, and latest checkpoint into the compaction context so the goal survives the compaction and the assistant keeps the thread. While a goal is active, the plugin also disables OpenCode's generic post-compaction auto-continue so it does not race the plugin's own continuation.
|
|
92
101
|
|
|
93
102
|
## Completion markers
|
|
94
103
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-goal-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Session-scoped /goal workflow for OpenCode.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/goal-plugin.js",
|
|
@@ -47,4 +47,4 @@
|
|
|
47
47
|
"author": {
|
|
48
48
|
"name": "willytop8"
|
|
49
49
|
}
|
|
50
|
-
}
|
|
50
|
+
}
|
package/src/goal-plugin.js
CHANGED
|
@@ -783,6 +783,24 @@ function buildContinueMessage(goal, { budgetWrapup = false } = {}) {
|
|
|
783
783
|
return lines.filter(Boolean).join("\n")
|
|
784
784
|
}
|
|
785
785
|
|
|
786
|
+
function buildCompactionContext(goal) {
|
|
787
|
+
// Preserve the active goal across an OpenCode session compaction. Without
|
|
788
|
+
// this, a compaction can drop the goal objective and budget state from the
|
|
789
|
+
// working context, so the assistant loses the thread mid-run even though the
|
|
790
|
+
// plugin still re-injects via system.transform afterward.
|
|
791
|
+
const elapsedSeconds = Math.round((Date.now() - goal.startedAt) / 1000)
|
|
792
|
+
return [
|
|
793
|
+
"An OpenCode goal is active for this session. Preserve it across compaction.",
|
|
794
|
+
buildGoalBlock(goal),
|
|
795
|
+
`Goal status: ${goal.stopped ? goal.stopReason || "stopped" : "active"}.`,
|
|
796
|
+
`Auto-continues used: ${goal.turnCount}/${goal.options.maxTurns}. Context tokens: ${goal.totalTokens}/${goal.options.maxTokens}. Elapsed: ${elapsedSeconds}s.`,
|
|
797
|
+
goal.lastCheckpoint ? `Latest checkpoint: ${goal.lastCheckpoint.summary}` : null,
|
|
798
|
+
"After compaction, continue from the next concrete unfinished step while the goal is active. Verify the result against the goal objective before ending; output [goal:complete] only when fully satisfied, or [goal:blocked] only if user input is required.",
|
|
799
|
+
]
|
|
800
|
+
.filter(Boolean)
|
|
801
|
+
.join("\n")
|
|
802
|
+
}
|
|
803
|
+
|
|
786
804
|
function extractBlockedReason(text) {
|
|
787
805
|
const lines = text.trimEnd().split("\n")
|
|
788
806
|
const markerIndex = lines.findIndex((line) => {
|
|
@@ -1031,6 +1049,47 @@ export const GoalPlugin = async ({ client }, pluginOptions = {}) => {
|
|
|
1031
1049
|
return
|
|
1032
1050
|
}
|
|
1033
1051
|
|
|
1052
|
+
if (args === "edit" || args.toLowerCase().startsWith("edit ")) {
|
|
1053
|
+
const goal = goalStates.get(sessionID)
|
|
1054
|
+
if (!goal) {
|
|
1055
|
+
output.parts = [
|
|
1056
|
+
makeTextPart("No active goal to edit. Set one with `/goal <condition>`."),
|
|
1057
|
+
]
|
|
1058
|
+
return
|
|
1059
|
+
}
|
|
1060
|
+
const newObjective = stripWrappingQuotes(args.slice("edit".length).trim())
|
|
1061
|
+
if (!newObjective) {
|
|
1062
|
+
output.parts = [
|
|
1063
|
+
makeTextPart("No new objective provided. Use `/goal edit <new objective>`."),
|
|
1064
|
+
]
|
|
1065
|
+
return
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
goal.condition = newObjective
|
|
1069
|
+
// Editing the objective revises the goal in place: keep the turn,
|
|
1070
|
+
// token, and time budget plus history, but clear soft-stop state so the
|
|
1071
|
+
// revised goal can continue. A goal that hit a hard limit will re-pause
|
|
1072
|
+
// on the next idle (use /goal resume for a fresh budget window).
|
|
1073
|
+
goal.stopped = false
|
|
1074
|
+
goal.stopReason = ""
|
|
1075
|
+
goal.blockedReason = ""
|
|
1076
|
+
goal.budgetWrapupSent = false
|
|
1077
|
+
goal.noProgressTurns = 0
|
|
1078
|
+
goal.lastStatus = "Goal objective updated."
|
|
1079
|
+
pushHistory(goal, "edited", `Objective updated to: ${summarizeText(newObjective, 400)}`)
|
|
1080
|
+
await persist()
|
|
1081
|
+
output.parts = [
|
|
1082
|
+
makeTextPart(
|
|
1083
|
+
[
|
|
1084
|
+
`Goal objective updated: ${goal.condition}`,
|
|
1085
|
+
"",
|
|
1086
|
+
"Budgets and history are preserved. Run `/goal resume` for a fresh budget window, or `/goal status` to review.",
|
|
1087
|
+
].join("\n"),
|
|
1088
|
+
),
|
|
1089
|
+
]
|
|
1090
|
+
return
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1034
1093
|
const parsed = parseGoalArguments(args, defaultGoalOptions)
|
|
1035
1094
|
if (parsed.errors.length > 0) {
|
|
1036
1095
|
output.parts = [makeTextPart(formatArgumentErrors(parsed.errors))]
|
|
@@ -1362,6 +1421,29 @@ export const GoalPlugin = async ({ client }, pluginOptions = {}) => {
|
|
|
1362
1421
|
}
|
|
1363
1422
|
output.system = systemBlocks
|
|
1364
1423
|
},
|
|
1424
|
+
|
|
1425
|
+
"experimental.session.compacting": async (input, output) => {
|
|
1426
|
+
if (!input?.sessionID || !output) return
|
|
1427
|
+
const goal = goalStates.get(input.sessionID)
|
|
1428
|
+
if (!goal) return
|
|
1429
|
+
const context = buildCompactionContext(goal)
|
|
1430
|
+
if (Array.isArray(output.context)) {
|
|
1431
|
+
output.context.push(context)
|
|
1432
|
+
} else {
|
|
1433
|
+
output.context = [context]
|
|
1434
|
+
}
|
|
1435
|
+
},
|
|
1436
|
+
|
|
1437
|
+
"experimental.compaction.autocontinue": async (input, output) => {
|
|
1438
|
+
// When a goal is active the plugin drives its own idle-triggered
|
|
1439
|
+
// continuation, so disable OpenCode's generic post-compaction
|
|
1440
|
+
// auto-continue to avoid two continuations racing after a compaction.
|
|
1441
|
+
// Paused/stopped goals leave the native behavior untouched.
|
|
1442
|
+
if (!input?.sessionID || !output) return
|
|
1443
|
+
const goal = goalStates.get(input.sessionID)
|
|
1444
|
+
if (!goal || goal.stopped) return
|
|
1445
|
+
output.enabled = false
|
|
1446
|
+
},
|
|
1365
1447
|
}
|
|
1366
1448
|
}
|
|
1367
1449
|
|
|
@@ -1373,6 +1455,7 @@ export default {
|
|
|
1373
1455
|
export const testInternals = {
|
|
1374
1456
|
activeGoal,
|
|
1375
1457
|
buildLimitWarning,
|
|
1458
|
+
buildCompactionContext,
|
|
1376
1459
|
buildContinueMessage,
|
|
1377
1460
|
buildGoalBlock,
|
|
1378
1461
|
budgetWrapupNeeded,
|