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 +34 -0
- package/README.md +5 -3
- package/package.json +4 -2
- package/plugins/goal-guard/gates.js +1 -1
- package/plugins/goal-guard/guard.js +17 -9
- package/plugins/goal-sidebar.tsx +26 -0
- package/scripts/postinstall.mjs +53 -0
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
|
-
-
|
|
249
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
|
|
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.`,
|
package/plugins/goal-sidebar.tsx
CHANGED
|
@@ -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);
|