opencode-goal-mode 0.3.10 → 0.3.11

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,14 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.3.11
4
+
5
+ - Fixed Goal sidebar/status isolation so an explicit Build or other non-Goal
6
+ session never falls back to another active Goal session in the same worktree.
7
+ - Blocked mutating `goal_*` tools from activating Goal Guard state in non-Goal
8
+ sessions; read-only tools remain strictly scoped to the current session.
9
+ - Added regression coverage for mixed Goal/Build persisted snapshots, session-scoped
10
+ status/evidence/memory reads, and Build-mode tool calls.
11
+
3
12
  ## v0.3.10
4
13
 
5
14
  - Clarified the recommended install command to use a persistent global npm install
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-goal-mode",
3
- "version": "0.3.10",
3
+ "version": "0.3.11",
4
4
  "description": "Strict Goal Mode agents, commands, and guard plugin for OpenCode.",
5
5
  "type": "module",
6
6
  "main": "plugins/goal-sidebar.tsx",
@@ -30,8 +30,10 @@ function normalize(record) {
30
30
  }
31
31
 
32
32
  /**
33
- * Choose which session's goal to show: the most-recently-touched ACTIVE session
34
- * (optionally preferring an explicit sessionId when it is present and active).
33
+ * Choose which session's goal to show. When OpenCode gives us a concrete session
34
+ * id, never fall back to another session: Build/non-Goal sessions must not show a
35
+ * Goal from the same worktree. The most-recent active fallback is only for
36
+ * no-session contexts such as initial sidebar registration polling.
35
37
  */
36
38
  export function pickSession(snapshot, sessionId) {
37
39
  if (!snapshot || !Array.isArray(snapshot.sessions)) return null;
@@ -41,6 +43,7 @@ export function pickSession(snapshot, sessionId) {
41
43
  if (sessionId) {
42
44
  const direct = records.find(([key, st]) => key === sessionId && st.active);
43
45
  if (direct) return direct[1];
46
+ return null;
44
47
  }
45
48
  const active = records.filter(([, st]) => st.active);
46
49
  if (active.length === 0) return null;
@@ -16,6 +16,7 @@ import { evidenceMapReport, reviewerMemoryReport, statusReport } from "./summary
16
16
  import { recordEvidence } from "./events.js";
17
17
  import { refreshStickyGates } from "./gates.js";
18
18
  import { createState } from "./state.js";
19
+ import { isPrimaryAgent } from "./agents.js";
19
20
 
20
21
  const s = tool.schema;
21
22
 
@@ -28,6 +29,18 @@ const s = tool.schema;
28
29
  export function createGoalTools({ store, config, persist }) {
29
30
  const save = typeof persist === "function" ? persist : () => {};
30
31
 
32
+ function requireGoalMode(state) {
33
+ return Boolean(state?.active || isPrimaryAgent(state?.currentAgent));
34
+ }
35
+
36
+ function goalModeOnlyResult() {
37
+ return {
38
+ title: "Goal Mode required",
39
+ output: "This goal_* tool can only mutate Goal Guard state from an active Goal session. Switch to the `goal` agent or start with /goal.",
40
+ metadata: { blocked: true, reason: "not_goal_mode" },
41
+ };
42
+ }
43
+
31
44
  return {
32
45
  goal_status: tool({
33
46
  description:
@@ -109,6 +122,7 @@ export function createGoalTools({ store, config, persist }) {
109
122
  },
110
123
  async execute(args, ctx) {
111
124
  const state = store.stateFor(ctx.sessionID);
125
+ if (!requireGoalMode(state)) return goalModeOnlyResult();
112
126
  state.active = true;
113
127
  state.contract = {
114
128
  title: String(args.title || "").replace(/\s+/g, " ").trim(),
@@ -145,6 +159,7 @@ export function createGoalTools({ store, config, persist }) {
145
159
  },
146
160
  async execute(args, ctx) {
147
161
  const state = store.stateFor(ctx.sessionID);
162
+ if (!requireGoalMode(state)) return goalModeOnlyResult();
148
163
  state.active = true;
149
164
  recordEvidence(store, state, args.command, args.result, args.criteria);
150
165
  save();
@@ -164,6 +179,8 @@ export function createGoalTools({ store, config, persist }) {
164
179
  confirm: s.boolean().describe("Must be true to actually reset."),
165
180
  },
166
181
  async execute(args, ctx) {
182
+ const state = store.stateFor(ctx.sessionID);
183
+ if (!requireGoalMode(state)) return goalModeOnlyResult();
167
184
  if (!args.confirm) {
168
185
  return { title: "Reset not confirmed", output: "Pass confirm=true to reset Goal Guard state." };
169
186
  }