opencode-goal-mode 0.4.2 → 0.4.3

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
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.4.3
4
+
5
+ ### Build mode no longer behaves like a goal
6
+
7
+ - Switching a session from the `goal` agent to Build (or any non-goal agent) now
8
+ **deactivates** it: `state.active` tracks the current agent (`isPrimaryAgent`),
9
+ instead of latching `true` forever. The sidebar also gates on the session's live
10
+ current agent (its latest message), so when you switch to Build the Goal section
11
+ disappears and OpenCode's native todos return — and a Build session can no longer
12
+ invoke the `goal-*` subagents or have its completion claims policed.
13
+ - Goal **worker** subagents (e.g. `goal-implementer`) are no longer activated by
14
+ their own edits, so Goal completion enforcement is never injected into a worker's
15
+ prompt. Bookkeeping runs only for active goal sessions and review subagents.
16
+
17
+ ### `npm install -g` now updates everything, automatically
18
+
19
+ - A global-install `postinstall` runs the installer for you — it copies the
20
+ components into `~/.config/opencode`, registers the Goal sidebar, and clears
21
+ OpenCode's stale plugin cache — so `npm install -g opencode-goal-mode` **alone**
22
+ fully installs or upgrades Goal Mode and the new version actually loads on the
23
+ next restart. It runs only for global installs, never for repo/dev/dependency
24
+ installs, and never fails the npm install (it prints a hint if it can't finish).
25
+
26
+ ### Fixes
27
+
28
+ - **CI green again:** the headless visual test no longer requires
29
+ `@opentui/solid/jsx-dev-runtime` (the CI visual job was failing).
30
+ - Hardened `gatePassedFresh` against partial/legacy snapshots so the TUI never
31
+ silently fails to render an active goal.
32
+ - Compaction context is only added for active goal sessions (no stray Goal state in
33
+ a Build session's compaction summary).
34
+ - Docs: corrected the sidebar description (separate gate/status lines; the label is
35
+ `GOAL`, not "Goal todos").
36
+
3
37
  ## v0.4.2
4
38
 
5
39
  ### The sidebar Goal section now actually renders in the live TUI
package/README.md CHANGED
@@ -244,9 +244,11 @@ enforcement and writes its state to disk, and an experimental TUI plugin
244
244
  slot, stacked on separate lines, each in its own colour so it never reads as one
245
245
  run of text:
246
246
  - a bold **`GOAL`** label (yellow while running, red when done);
247
- - the short goal title;
248
- - a `passing/total gates · status` line (lifecycle only no "changes pending"
249
- noise; pending work shows as a todo row instead);
247
+ - the short goal title (white);
248
+ - the gate count `passing/total gates` (cyan), on its own line;
249
+ - the lifecycle status (orange) on its own line — `in progress`, or
250
+ `completed · N review cycles`. No "changes pending" noise; pending work shows
251
+ as a todo row instead;
250
252
  - structured todo rows derived from real guard state: one per acceptance
251
253
  criterion (✓ when fresh evidence covers it), a re-verify row when the tree
252
254
  changed, and one row per still-missing review gate by friendly name
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-goal-mode",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Strict Goal Mode agents, commands, and guard plugin for OpenCode.",
5
5
  "type": "module",
6
6
  "main": "plugins/goal-sidebar.tsx",
@@ -25,6 +25,7 @@
25
25
  "plugins/",
26
26
  "research/",
27
27
  "scripts/install.mjs",
28
+ "scripts/postinstall.mjs",
28
29
  "ARCHITECTURE.md",
29
30
  "CHANGELOG.md",
30
31
  "LICENSE",
@@ -51,7 +52,8 @@
51
52
  "ci": "npm run validate && npm run audit",
52
53
  "prepublishOnly": "npm run ci && npm run publish:check",
53
54
  "install:local": "node scripts/install.mjs",
54
- "install:global": "node scripts/install.mjs --global"
55
+ "install:global": "node scripts/install.mjs --global",
56
+ "postinstall": "node scripts/postinstall.mjs"
55
57
  },
56
58
  "keywords": [
57
59
  "opencode",
@@ -71,7 +71,7 @@ export function requiredGates(state, config) {
71
71
 
72
72
  /** A gate is satisfied when its latest verdict is PASS and newer than the last edit. */
73
73
  export function gatePassedFresh(state, agent) {
74
- const v = state.latestVerdict[agent];
74
+ const v = state.latestVerdict?.[agent];
75
75
  if (!v || v.verdict !== "PASS") return false;
76
76
  return v.seq > (state.lastEditSeq || 0);
77
77
  }
@@ -93,7 +93,10 @@ export function createGuard(input = {}, options = {}, overrides = {}) {
93
93
  try {
94
94
  if (!inp?.sessionID) return;
95
95
  const state = store.stateFor(inp.sessionID);
96
- if (isPrimaryAgent(inp.agent)) state.active = true;
96
+ // `active` reflects whether this session is CURRENTLY a Goal session. Switching
97
+ // the session's agent to Build (or anything non-goal) must deactivate it, or
98
+ // the sidebar/guard would keep treating an explicit Build session as a goal.
99
+ if (inp.agent) state.active = isPrimaryAgent(inp.agent);
97
100
  const text = partsText(out?.parts);
98
101
  if (text && state.active) {
99
102
  // Accumulate goal text (bounded) so contextual gates can be derived.
@@ -115,7 +118,10 @@ export function createGuard(input = {}, options = {}, overrides = {}) {
115
118
  if (!normalized) return;
116
119
  const state = store.stateFor(normalized);
117
120
  state.currentAgent = inp.agent;
118
- if (isPrimaryAgent(inp.agent)) state.active = true;
121
+ // Track the current mode: a session is active (a goal) only while its agent
122
+ // is the goal primary. Switching to Build/Plan/etc. deactivates it so the
123
+ // Goal sidebar and enforcement stop treating it as a goal.
124
+ if (inp.agent) state.active = isPrimaryAgent(inp.agent);
119
125
  } catch {
120
126
  /* ignore */
121
127
  }
@@ -178,13 +184,14 @@ export function createGuard(input = {}, options = {}, overrides = {}) {
178
184
  async "tool.execute.after"(inp, out) {
179
185
  try {
180
186
  const state = store.stateFor(inp?.sessionID);
181
- // Goal bookkeeping is GOAL-ONLY. A Build/Plan/custom session must never have
182
- // its edits, mutations, verification, or verdicts recorded as goal state
183
- // otherwise the guard would treat a non-Goal message/task as if it were a
184
- // goal. Only an active Goal session (or a goal-namespace subagent's own
185
- // session, for verdict capture) is tracked. Destructive-command blocking is
186
- // handled in tool.execute.before and still applies in every mode.
187
- if (!state.active && !isGoalAgent(state.currentAgent)) return;
187
+ // Goal bookkeeping runs only for an active Goal session, or for a REVIEW
188
+ // subagent's own child session (so the agent-path can capture its verdict).
189
+ // It must NOT run for a Build/Plan/custom session, nor for a non-review goal
190
+ // WORKER child session (e.g. goal-implementer/goal-explorer) otherwise that
191
+ // worker's edits would mark it dirty and activate it, leaking goal completion
192
+ // enforcement into a worker's prompt. Destructive-command blocking lives in
193
+ // tool.execute.before and still applies in every mode.
194
+ if (!state.active && !isReviewAgent(state.currentAgent)) return;
188
195
  const tool = inp?.tool;
189
196
  const isReviewing = isReviewAgent(state.currentAgent);
190
197
 
@@ -278,6 +285,7 @@ export function createGuard(input = {}, options = {}, overrides = {}) {
278
285
  try {
279
286
  if (!inp?.sessionID || !out || !Array.isArray(out.context)) return;
280
287
  const state = store.stateFor(inp.sessionID);
288
+ if (!state.active) return; // only preserve goal state for active Goal sessions
281
289
  out.context.push(
282
290
  `Goal Guard state: ${summarizeState(state, config)}. Preserve Goal Contract, Verification Ledger, ` +
283
291
  `Review Ledger, Reviewer Memory, review cycle count, dirty state, and open findings across compaction.`,
@@ -35,6 +35,7 @@ const META_COLOR = "#8BE9FD"; // gates line (running) — cyan accent
35
35
  const STATUS_COLOR = "#FFB86C"; // status line (running) — orange, distinct from the cyan gates line
36
36
  const TODO_DONE_COLOR = "#50FA7B"; // ✓ done todo rows — green
37
37
  const POLL_MS = 1500;
38
+ const GOAL_AGENT = "goal"; // the primary Goal agent id (mirrors agents.js PRIMARY_AGENT)
38
39
  const RAINBOW = ["#FF5555", "#FFAA00", "#FFFF55", "#55FF55", "#55FFFF", "#5599FF", "#FF55FF"];
39
40
 
40
41
  function resolveOptions(options, env) {
@@ -127,8 +128,33 @@ const tui = async (api, options) => {
127
128
  slots: {
128
129
  sidebar_content(_ctx, props) {
129
130
  if (!props?.session_id) return undefined;
131
+ // The session's CURRENT agent, from its latest message (mirrors the
132
+ // reference OpenCode TUI plugin). This is the authoritative, immediate
133
+ // signal of whether the session is in Goal mode right now — so when the
134
+ // user switches to Build (or any non-goal agent) the Goal section vanishes
135
+ // and OpenCode's native todos return, without waiting for persisted state.
136
+ const currentAgent = () => {
137
+ try {
138
+ const msgs = api?.state?.session?.messages?.(props.session_id);
139
+ if (Array.isArray(msgs)) {
140
+ for (let i = msgs.length - 1; i >= 0; i--) {
141
+ const a = msgs[i] && msgs[i].agent;
142
+ if (a) return String(a).toLowerCase();
143
+ }
144
+ }
145
+ } catch {
146
+ /* messages API unavailable — fall back to persisted goal state */
147
+ }
148
+ return undefined;
149
+ };
130
150
  const read = () => {
131
151
  try {
152
+ const agent = currentAgent();
153
+ // Render only in Goal mode. "goal" and its `goal-*` subagents count as
154
+ // Goal mode (so the section doesn't flicker out while reviewers run);
155
+ // any other primary agent (build/plan/custom) renders nothing here.
156
+ const goalMode = !agent || agent === GOAL_AGENT || agent.startsWith(`${GOAL_AGENT}-`);
157
+ if (!goalMode) return NO_GOAL;
132
158
  return readModel(worktrees, props?.session_id) || NO_GOAL;
133
159
  } catch {
134
160
  return NO_GOAL;
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Auto-setup on a GLOBAL install.
4
+ *
5
+ * When a user runs `npm install -g opencode-goal-mode` (a fresh install OR an
6
+ * upgrade), this runs the installer for them so that, with a single command, the
7
+ * package's components are (re)copied into `~/.config/opencode`, the Goal sidebar
8
+ * is registered in `tui.json`, and OpenCode's stale plugin cache is cleared so the
9
+ * just-installed version actually loads. That makes `npm install -g` "really
10
+ * update" — no separate `opencode-goal-mode --global` step required.
11
+ *
12
+ * Safety:
13
+ * - Runs ONLY for global installs (npm sets `npm_config_global`). Repo development
14
+ * (`npm ci` / `npm install`) and installs as a project dependency are no-ops, so
15
+ * this never writes to a user's config when it shouldn't.
16
+ * - Best-effort: it NEVER fails the npm install. Any problem prints a one-line
17
+ * hint to run `opencode-goal-mode --global` manually and exits 0.
18
+ */
19
+ import { spawnSync } from "node:child_process";
20
+ import { fileURLToPath } from "node:url";
21
+ import { join, dirname } from "node:path";
22
+
23
+ function isGlobalInstall() {
24
+ const g = process.env.npm_config_global;
25
+ return g === "true" || g === "1" || g === true;
26
+ }
27
+
28
+ if (!isGlobalInstall()) {
29
+ // Local/dev/dependency install — do not touch the user's OpenCode config.
30
+ process.exit(0);
31
+ }
32
+
33
+ try {
34
+ const installer = join(dirname(fileURLToPath(import.meta.url)), "install.mjs");
35
+ console.log("opencode-goal-mode: global install detected — updating ~/.config/opencode…");
36
+ const res = spawnSync(process.execPath, [installer, "--global"], { stdio: "inherit" });
37
+ // spawnSync does not throw on a non-zero exit (e.g. the installer refused to
38
+ // overwrite a file you edited). Surface a clear, actionable hint instead of
39
+ // leaving only the child's raw error.
40
+ if (res.error || res.status !== 0) {
41
+ console.warn(
42
+ "opencode-goal-mode: auto-setup didn't fully complete. " +
43
+ "Run `opencode-goal-mode --global` manually (add --force to replace files you've edited), then restart OpenCode.",
44
+ );
45
+ }
46
+ } catch (err) {
47
+ console.warn(
48
+ `opencode-goal-mode: auto-setup skipped (${(err && err.message) || err}). ` +
49
+ "Run `opencode-goal-mode --global` to finish, then restart OpenCode.",
50
+ );
51
+ }
52
+ // Never fail `npm install`, regardless of the installer's outcome.
53
+ process.exit(0);