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 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.1.14",
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
+ }
@@ -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,