opencode-goal-mode 0.3.0 → 0.3.2
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/ARCHITECTURE.md +9 -1
- package/CHANGELOG.md +27 -0
- package/README.md +61 -38
- package/package.json +2 -1
- package/plugins/goal-guard/agents.js +20 -0
- package/plugins/goal-guard/config.js +3 -0
- package/plugins/goal-guard/sidebar-data.js +7 -5
- package/plugins/goal-guard/summary.js +10 -6
- package/plugins/goal-guard.js +3 -3
- package/plugins/goal-sidebar.js +17 -14
package/ARCHITECTURE.md
CHANGED
|
@@ -170,7 +170,9 @@ hooks still load.
|
|
|
170
170
|
`{ tui }` module, mutually exclusive with `{ server }`). It registers a
|
|
171
171
|
`sidebar_content` slot via `api.slots.register({ slots: { sidebar_content } })`
|
|
172
172
|
and renders, in the configured colour (`#FFD700` by default), the short goal
|
|
173
|
-
label plus a `passing/total gates · dirty/ready` line.
|
|
173
|
+
label plus a `passing/total gates · dirty/ready` line. It renders
|
|
174
|
+
unconditionally: when a task is running with no goal set, it shows a muted grey
|
|
175
|
+
`No goal` (`sidebarView` returns `{ hasGoal: false }`) rather than a blank slot.
|
|
174
176
|
|
|
175
177
|
It is *paired* with the server plugin only through the persisted state file:
|
|
176
178
|
`sidebar-data.js` recomputes the same `stateBaseDir`/`projectKey` path the guard
|
|
@@ -182,6 +184,12 @@ error degrades to rendering nothing — it can never break the TUI. The server p
|
|
|
182
184
|
also emits review-verdict and completion-unlock toasts (`toastOnReview`) so review
|
|
183
185
|
progress is visible even without the banner.
|
|
184
186
|
|
|
187
|
+
The JSX renderer is verified headlessly with `@opentui/solid`'s `testRender` in
|
|
188
|
+
`tools/visual-test/sidebar-visual.jsx` (`npm run test:visual`, needs Bun + the
|
|
189
|
+
OpenTUI stack): it asserts the rendered text, the exact foreground colours, and
|
|
190
|
+
the bold attribute for goal / "No goal" / ready states. That tool is excluded from
|
|
191
|
+
the npm package and from `node --test`/CI.
|
|
192
|
+
|
|
185
193
|
## Configuration
|
|
186
194
|
|
|
187
195
|
`config.js` merges, in increasing precedence: built-in defaults, environment
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.3.2
|
|
4
|
+
|
|
5
|
+
- Only the `goal` agent is user-selectable. The structural validator now requires
|
|
6
|
+
every other agent to be `mode: subagent` (no `all`/extra `primary`), so the
|
|
7
|
+
specialist reviewers can only be invoked by the Goal agent via the task tool,
|
|
8
|
+
never picked by the user.
|
|
9
|
+
- Friendlier subagent names in the TUI: review-verdict toasts now read
|
|
10
|
+
"Security Reviewer → PASS" / "API Reviewer → PASS" instead of raw hyphenated ids
|
|
11
|
+
(`prettyAgentName` drops the `goal-` prefix, de-hyphenates, keeps acronyms).
|
|
12
|
+
- Release pipeline: a single `vX.Y.Z` tag push now publishes to npm AND creates
|
|
13
|
+
the matching GitHub Release (versions stay in sync). `publish:check` fails the
|
|
14
|
+
release if the tag does not match `package.json` or the version already exists.
|
|
15
|
+
- README: npm-first install instructions and a documented release flow.
|
|
16
|
+
|
|
17
|
+
## v0.3.1
|
|
18
|
+
|
|
19
|
+
- Sidebar: when a task is running but no goal is set, show a clean grey `No goal`
|
|
20
|
+
(nothing else) instead of a blank/absent banner. New `sidebarMutedColor` option
|
|
21
|
+
(`GOAL_GUARD_SIDEBAR_MUTED_COLOR`, default `#808080`).
|
|
22
|
+
- `summary.sidebarView` now always returns a model (`{ hasGoal: false }` vs
|
|
23
|
+
`{ hasGoal: true, … }`) so the sidebar renders unconditionally.
|
|
24
|
+
- Add a headless visual test (`npm run test:visual`, `tools/visual-test/`) that
|
|
25
|
+
renders the real component with @opentui/solid and asserts text + exact colours
|
|
26
|
+
+ bold attributes across goal / no-goal / ready / custom-colour / truncation /
|
|
27
|
+
disabled / no-API / resize scenarios. Excluded from the npm package and CI.
|
|
28
|
+
- Hardened the sidebar projection against malformed/partial persisted state.
|
|
29
|
+
|
|
3
30
|
## v0.3.0
|
|
4
31
|
|
|
5
32
|
- Honest benchmarks: add an EXTERNAL corpus of 704 real third-party commands from
|
package/README.md
CHANGED
|
@@ -90,7 +90,11 @@ second) — negligible for a per-tool-call guard:
|
|
|
90
90
|
## What it adds
|
|
91
91
|
|
|
92
92
|
- A primary `goal` agent that owns implementation but delegates research,
|
|
93
|
-
discovery, verification planning, and reviews to subagents.
|
|
93
|
+
discovery, verification planning, and reviews to subagents. **`goal` is the only
|
|
94
|
+
user-selectable agent** — every specialist (security, diff, verifier, …) is a
|
|
95
|
+
`mode: subagent` that the Goal agent invokes via the task tool; the user never
|
|
96
|
+
picks one directly. They surface with friendly names (e.g. "Security Reviewer",
|
|
97
|
+
"API Reviewer") rather than raw ids.
|
|
94
98
|
- Strict review gates for prompt compliance, diff review, verification, security,
|
|
95
99
|
UX, operations, data, API, performance, tests, docs, quality, and final audit.
|
|
96
100
|
- Slash commands: `/goal`, `/goal-contract`, `/goal-review`,
|
|
@@ -111,8 +115,9 @@ second) — negligible for a per-tool-call guard:
|
|
|
111
115
|
`goal_reviewer_memory`, `goal_status`, `goal_reset`.
|
|
112
116
|
- **Live state injection** into the system prompt so the model always knows
|
|
113
117
|
what the guard requires.
|
|
114
|
-
- **TUI toasts**: a toast on each review verdict (PASS/FAIL)
|
|
115
|
-
"completion unlocked" toast the moment
|
|
118
|
+
- **TUI toasts**: a toast on each review verdict (PASS/FAIL), with the
|
|
119
|
+
reviewer's friendly name, and a single "completion unlocked" toast the moment
|
|
120
|
+
the last required gate clears.
|
|
116
121
|
- An **experimental** companion TUI plugin (`plugins/goal-sidebar.js`) that shows
|
|
117
122
|
the active goal as a shining-yellow banner in the sidebar with a compact gate
|
|
118
123
|
status line. See [TUI integration](#tui-integration).
|
|
@@ -127,38 +132,53 @@ enforcement and writes its state to disk, and an experimental TUI plugin
|
|
|
127
132
|
|
|
128
133
|
- **Sidebar goal banner (experimental).** The current goal renders in shining
|
|
129
134
|
yellow in the sidebar (`sidebar_content` slot), with a `passing/total gates ·
|
|
130
|
-
dirty/ready` status line, and updates as reviews land.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
`
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
dirty/ready` status line, and updates as reviews land. When a task is running
|
|
136
|
+
but **no goal is set**, it shows a clean grey `No goal` and nothing else. It
|
|
137
|
+
requires a TUI-plugin-capable OpenCode (one exposing `api.slots.register`); on
|
|
138
|
+
any older runtime it silently no-ops, so it can never break your TUI. Set
|
|
139
|
+
`sidebarBanner: false` (or `GOAL_GUARD_SIDEBAR_BANNER=0`) to disable,
|
|
140
|
+
`sidebarColor` to recolour the goal, or `sidebarMutedColor` for the "No goal"
|
|
141
|
+
line. It is rendered-and-asserted headlessly by the
|
|
142
|
+
[visual test](tools/visual-test/README.md) (`npm run test:visual`); still worth
|
|
143
|
+
a glance in your own TUI.
|
|
137
144
|
- **Toasts.** Review verdicts and completion-unlock events surface as toasts
|
|
138
145
|
(`toastOnReview`), and blocked destructive commands / premature completions
|
|
139
146
|
toast as before (`toastOnBlock`).
|
|
140
147
|
|
|
141
|
-
## Install
|
|
148
|
+
## Install
|
|
149
|
+
|
|
150
|
+
### From npm (recommended)
|
|
142
151
|
|
|
143
152
|
```bash
|
|
144
|
-
npm
|
|
145
|
-
|
|
146
|
-
npm run install:global
|
|
153
|
+
npm install -g opencode-goal-mode
|
|
154
|
+
opencode-goal-mode-install --global # installs into ~/.config/opencode
|
|
147
155
|
```
|
|
148
156
|
|
|
149
|
-
|
|
150
|
-
|
|
157
|
+
Then restart OpenCode (it loads agents, commands, and plugins at startup). In the
|
|
158
|
+
agent picker you will see **only the `goal` agent** — the specialist reviewers are
|
|
159
|
+
subagents the Goal agent drives for you; they are never selectable by the user.
|
|
151
160
|
|
|
152
|
-
|
|
161
|
+
Install into a single project instead of globally:
|
|
153
162
|
|
|
154
163
|
```bash
|
|
164
|
+
npm install -D opencode-goal-mode
|
|
165
|
+
npx opencode-goal-mode-install # writes to ./.opencode
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Upgrade later by re-running the same install command after `npm install -g
|
|
169
|
+
opencode-goal-mode@latest`; the installer replaces only the files it owns and
|
|
170
|
+
leaves your local edits alone (see [Installer options](#installer-options)).
|
|
171
|
+
|
|
172
|
+
### From source
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
git clone https://github.com/devinoldenburg/opencode-goal-mode
|
|
176
|
+
cd opencode-goal-mode
|
|
155
177
|
npm ci
|
|
156
178
|
npm run validate
|
|
157
|
-
npm run install:local
|
|
179
|
+
npm run install:global # or: npm run install:local
|
|
158
180
|
```
|
|
159
181
|
|
|
160
|
-
This writes to `./.opencode` in the current project.
|
|
161
|
-
|
|
162
182
|
## Installer options
|
|
163
183
|
|
|
164
184
|
```bash
|
|
@@ -202,6 +222,7 @@ Or via environment variables (`GOAL_GUARD_*`):
|
|
|
202
222
|
| `toastOnReview` / `GOAL_GUARD_TOAST_ON_REVIEW` | `true` | Toast on each review verdict and when completion unlocks. |
|
|
203
223
|
| `sidebarBanner` / `GOAL_GUARD_SIDEBAR_BANNER` | `true` | Show the experimental yellow goal banner in the TUI sidebar. |
|
|
204
224
|
| `sidebarColor` / `GOAL_GUARD_SIDEBAR_COLOR` | `#FFD700` | Foreground colour of the sidebar goal banner. |
|
|
225
|
+
| `sidebarMutedColor` / `GOAL_GUARD_SIDEBAR_MUTED_COLOR` | `#808080` | Colour of the muted "No goal" line when no goal is set. |
|
|
205
226
|
|
|
206
227
|
## Custom tools
|
|
207
228
|
|
|
@@ -250,32 +271,34 @@ keeps read-only inspection from dirtying the session, preserves goal state durin
|
|
|
250
271
|
compaction and across restarts, and blocks premature `Goal Completed` responses
|
|
251
272
|
when review gates are missing or stale.
|
|
252
273
|
|
|
253
|
-
##
|
|
274
|
+
## Releasing
|
|
254
275
|
|
|
255
|
-
|
|
276
|
+
Releases are fully automated and **version-synced**: one pushed tag publishes to
|
|
277
|
+
npm *and* creates the matching GitHub Release. The pipeline lives in
|
|
278
|
+
[`.github/workflows/publish.yml`](.github/workflows/publish.yml) (Node 24).
|
|
256
279
|
|
|
257
280
|
```bash
|
|
258
|
-
npm
|
|
259
|
-
|
|
281
|
+
npm version patch # bumps package.json + package-lock.json and creates the vX.Y.Z tag
|
|
282
|
+
git push --follow-tags # pushes main + the tag → the Release workflow runs
|
|
260
283
|
```
|
|
261
284
|
|
|
262
|
-
|
|
263
|
-
and publishes with the `NPM_TOKEN` repository secret. The workflow validates the
|
|
264
|
-
package, checks the tag matches `package.json`, verifies the version is not
|
|
265
|
-
already on npm, then publishes. Manual workflow dispatch defaults to
|
|
266
|
-
`npm publish --dry-run`.
|
|
285
|
+
On a `vX.Y.Z` tag push the workflow:
|
|
267
286
|
|
|
268
|
-
|
|
287
|
+
1. installs and runs the full CI gate (`npm run ci` — tests, audit, structural
|
|
288
|
+
validation, `npm pack --dry-run`);
|
|
289
|
+
2. runs `npm run publish:check`, which **fails if the tag does not match
|
|
290
|
+
`package.json`** or if that version already exists on npm;
|
|
291
|
+
3. publishes with `npm publish --access public` using the `NPM_TOKEN` repository
|
|
292
|
+
secret;
|
|
293
|
+
4. creates the GitHub Release for the tag with auto-generated notes.
|
|
269
294
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
```
|
|
295
|
+
So the git tag, the `package.json` version, the npm version, and the GitHub
|
|
296
|
+
Release version are always identical. A manual `workflow_dispatch` is available
|
|
297
|
+
and defaults to a safe `npm publish --dry-run`.
|
|
274
298
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
the release.
|
|
299
|
+
**One-time setup:** add a publish-scoped npm token as the `NPM_TOKEN` repository
|
|
300
|
+
secret (`gh secret set NPM_TOKEN`). Treat that token as sensitive — never commit
|
|
301
|
+
it.
|
|
279
302
|
|
|
280
303
|
## Goal Completion Contract
|
|
281
304
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-goal-mode",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Strict Goal Mode agents, commands, and guard plugin for OpenCode.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"test:unit": "node --test tests/state.test.mjs tests/gates.test.mjs tests/verdicts.test.mjs tests/config.test.mjs tests/persistence.test.mjs",
|
|
32
32
|
"test:agents": "node --test tests/agents.test.mjs tests/commands.test.mjs",
|
|
33
33
|
"test:install": "node --test tests/install.test.mjs",
|
|
34
|
+
"test:visual": "bun tools/visual-test/sidebar-visual.jsx",
|
|
34
35
|
"bench": "node benchmarks/run.mjs",
|
|
35
36
|
"bench:external": "node benchmarks/external.mjs",
|
|
36
37
|
"bench:corpus": "node benchmarks/build-external-corpus.mjs",
|
|
@@ -130,3 +130,23 @@ export const CONTEXTUAL_GATES = Object.freeze({
|
|
|
130
130
|
|
|
131
131
|
/** The reviewer that, when it returns a verdict, closes one review cycle. */
|
|
132
132
|
export const CYCLE_CLOSING_AGENT = "goal-final-auditor";
|
|
133
|
+
|
|
134
|
+
/** Acronyms that should stay upper-case in display names. */
|
|
135
|
+
const ACRONYMS = new Set(["api", "ux", "ui", "sql", "ops", "qa"]);
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Human-friendly display name for an agent id: drops the `goal-` namespace
|
|
139
|
+
* prefix, turns hyphens into spaces, Title-Cases words, and keeps known acronyms
|
|
140
|
+
* upper-case. e.g. "goal-security-reviewer" → "Security Reviewer",
|
|
141
|
+
* "goal-api-reviewer" → "API Reviewer", "goal-final-auditor" → "Final Auditor".
|
|
142
|
+
*/
|
|
143
|
+
export function prettyAgentName(id) {
|
|
144
|
+
const raw = String(id || "").trim();
|
|
145
|
+
if (!raw) return "";
|
|
146
|
+
return raw
|
|
147
|
+
.replace(/^goal-/, "")
|
|
148
|
+
.split(/[-_\s]+/)
|
|
149
|
+
.filter(Boolean)
|
|
150
|
+
.map((w) => (ACRONYMS.has(w.toLowerCase()) ? w.toUpperCase() : w.charAt(0).toUpperCase() + w.slice(1)))
|
|
151
|
+
.join(" ");
|
|
152
|
+
}
|
|
@@ -32,6 +32,8 @@ export const DEFAULT_CONFIG = Object.freeze({
|
|
|
32
32
|
sidebarBanner: true,
|
|
33
33
|
/** Foreground colour (hex) for the sidebar goal banner. */
|
|
34
34
|
sidebarColor: "#FFD700",
|
|
35
|
+
/** Foreground colour (hex) for the muted "No goal" sidebar line. */
|
|
36
|
+
sidebarMutedColor: "#808080",
|
|
35
37
|
/** Phrase that, at the start of an assistant message, claims completion. */
|
|
36
38
|
completionMarker: "Goal Completed",
|
|
37
39
|
/** Replacement marker when completion is blocked. */
|
|
@@ -68,6 +70,7 @@ function fromEnv(env) {
|
|
|
68
70
|
GOAL_GUARD_TOAST_ON_REVIEW: ["toastOnReview", coerceBool],
|
|
69
71
|
GOAL_GUARD_SIDEBAR_BANNER: ["sidebarBanner", coerceBool],
|
|
70
72
|
GOAL_GUARD_SIDEBAR_COLOR: ["sidebarColor", (v) => (v == null ? undefined : String(v))],
|
|
73
|
+
GOAL_GUARD_SIDEBAR_MUTED_COLOR: ["sidebarMutedColor", (v) => (v == null ? undefined : String(v))],
|
|
71
74
|
};
|
|
72
75
|
for (const [key, [field, coerce]] of Object.entries(map)) {
|
|
73
76
|
if (env[key] !== undefined) out[field] = coerce(env[key], DEFAULT_CONFIG[field]);
|
|
@@ -13,7 +13,7 @@ import { readFileSync } from "node:fs";
|
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { stateBaseDir, projectKey } from "./persistence.js";
|
|
15
15
|
import { DEFAULT_CONFIG } from "./config.js";
|
|
16
|
-
import { sidebarView } from "./summary.js";
|
|
16
|
+
import { sidebarView, NO_GOAL } from "./summary.js";
|
|
17
17
|
|
|
18
18
|
/** Absolute path of the guard's state file for a given worktree. */
|
|
19
19
|
export function sidebarStateFile(worktree, env = process.env) {
|
|
@@ -49,8 +49,10 @@ export function pickSession(snapshot, sessionId) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* Build the sidebar banner model for a worktree
|
|
53
|
-
*
|
|
52
|
+
* Build the sidebar banner model for a worktree. ALWAYS returns an object so the
|
|
53
|
+
* sidebar renders unconditionally: `{ hasGoal: false }` when there is no state,
|
|
54
|
+
* no active session, or no goal (render a muted "No goal"); otherwise
|
|
55
|
+
* `{ hasGoal: true, goal, status, … }` (see summary.sidebarView).
|
|
54
56
|
*
|
|
55
57
|
* @param {object} opts
|
|
56
58
|
* @param {string} opts.worktree Project worktree root (same key the guard uses).
|
|
@@ -63,9 +65,9 @@ export function readSidebarModel({ worktree, sessionId, config = DEFAULT_CONFIG,
|
|
|
63
65
|
try {
|
|
64
66
|
snapshot = JSON.parse(readFileSync(sidebarStateFile(worktree, env), "utf8"));
|
|
65
67
|
} catch {
|
|
66
|
-
return
|
|
68
|
+
return NO_GOAL; // no state yet, or unreadable.
|
|
67
69
|
}
|
|
68
70
|
const record = pickSession(snapshot, sessionId);
|
|
69
|
-
if (!record) return
|
|
71
|
+
if (!record) return NO_GOAL;
|
|
70
72
|
return sidebarView(record, config);
|
|
71
73
|
}
|
|
@@ -21,21 +21,25 @@ export function shortGoalLabel(state, max = 80) {
|
|
|
21
21
|
return `${base.slice(0, max - 1).trimEnd()}…`;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/** Sentinel for "a task is running but no goal is set" — the sidebar shows a muted "No goal". */
|
|
25
|
+
export const NO_GOAL = Object.freeze({ hasGoal: false });
|
|
26
|
+
|
|
24
27
|
/**
|
|
25
|
-
* Compact projection for the TUI sidebar banner
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
+
* Compact projection for the TUI sidebar banner. ALWAYS returns an object so the
|
|
29
|
+
* sidebar can render unconditionally:
|
|
30
|
+
* - `{ hasGoal: false }` when no active goal is set (render a muted "No goal").
|
|
31
|
+
* - `{ hasGoal: true, goal, status, … }` when a goal is active (render in colour).
|
|
28
32
|
*/
|
|
29
33
|
export function sidebarView(state, config) {
|
|
30
|
-
if (!state || !state.active) return
|
|
34
|
+
if (!state || !state.active) return NO_GOAL;
|
|
31
35
|
const goal = shortGoalLabel(state);
|
|
32
|
-
if (!goal) return
|
|
36
|
+
if (!goal) return NO_GOAL;
|
|
33
37
|
const required = requiredGates(state, config);
|
|
34
38
|
const missing = missingGates(state, config);
|
|
35
39
|
const passing = required.length - missing.length;
|
|
36
40
|
const allowed = required.length > 0 && missing.length === 0 && !state.dirty;
|
|
37
41
|
const status = `${passing}/${required.length} gates` + (state.dirty ? " · dirty" : "") + (allowed ? " · ready" : "");
|
|
38
|
-
return { goal, status, allowed, reviewCycles: state.reviewCycles, passing, required: required.length, dirty: Boolean(state.dirty) };
|
|
42
|
+
return { hasGoal: true, goal, status, allowed, reviewCycles: state.reviewCycles, passing, required: required.length, dirty: Boolean(state.dirty) };
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
export function summarizeState(state, config) {
|
package/plugins/goal-guard.js
CHANGED
|
@@ -23,7 +23,7 @@ import { createStore, createState } from "./goal-guard/state.js";
|
|
|
23
23
|
import { createPersistence } from "./goal-guard/persistence.js";
|
|
24
24
|
import { createLogger } from "./goal-guard/logger.js";
|
|
25
25
|
import { analyzeCommand, looksLikeDestructiveBash, looksLikeMutatingBash, isVerification } from "./goal-guard/shell.js";
|
|
26
|
-
import { isPrimaryAgent, isReviewAgent, CYCLE_CLOSING_AGENT } from "./goal-guard/agents.js";
|
|
26
|
+
import { isPrimaryAgent, isReviewAgent, CYCLE_CLOSING_AGENT, prettyAgentName } from "./goal-guard/agents.js";
|
|
27
27
|
import { textOf, parseVerdict, recordVerdict } from "./goal-guard/verdicts.js";
|
|
28
28
|
import { completionAllowed, missingGates, refreshStickyGates } from "./goal-guard/gates.js";
|
|
29
29
|
import { evaluateCompletionClaim } from "./goal-guard/completion.js";
|
|
@@ -210,9 +210,9 @@ export function createGuard(input = {}, options = {}, overrides = {}) {
|
|
|
210
210
|
// Surface review progress in the TUI: a toast per recorded verdict, and a
|
|
211
211
|
// single celebratory toast the moment the last required gate clears.
|
|
212
212
|
if (recordedAgent && recordedVerdict && config.toastOnReview) {
|
|
213
|
-
logger.toast(
|
|
213
|
+
logger.toast(`${prettyAgentName(recordedAgent)} → ${recordedVerdict}`, recordedVerdict === "PASS" ? "success" : "warning");
|
|
214
214
|
if (!wasAllowed && completionAllowed(state, config)) {
|
|
215
|
-
logger.toast("
|
|
215
|
+
logger.toast("All required gates passed — completion unlocked", "success");
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
persist();
|
package/plugins/goal-sidebar.js
CHANGED
|
@@ -28,10 +28,11 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { createSignal, onCleanup, Show } from "solid-js";
|
|
31
|
-
import { sidebarView } from "./goal-guard/summary.js";
|
|
31
|
+
import { sidebarView, NO_GOAL } from "./goal-guard/summary.js";
|
|
32
32
|
import { DEFAULT_CONFIG } from "./goal-guard/config.js";
|
|
33
33
|
|
|
34
34
|
const DEFAULT_COLOR = "#FFD700"; // shining yellow
|
|
35
|
+
const DEFAULT_MUTED = "#808080"; // clean grey for "No goal"
|
|
35
36
|
const POLL_MS = 1500;
|
|
36
37
|
|
|
37
38
|
function resolveOptions(options, env) {
|
|
@@ -41,7 +42,8 @@ function resolveOptions(options, env) {
|
|
|
41
42
|
const disabled =
|
|
42
43
|
enabledOpt === false || enabledEnv === "0" || enabledEnv === "false" || enabledEnv === "off";
|
|
43
44
|
const color = options?.sidebarColor || e.GOAL_GUARD_SIDEBAR_COLOR || DEFAULT_COLOR;
|
|
44
|
-
|
|
45
|
+
const muted = options?.sidebarMutedColor || e.GOAL_GUARD_SIDEBAR_MUTED_COLOR || DEFAULT_MUTED;
|
|
46
|
+
return { enabled: !disabled, color, muted };
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
/**
|
|
@@ -82,14 +84,14 @@ function pickSession(snapshot, sessionId) {
|
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
function readModel(worktree, sessionId) {
|
|
85
|
-
const snapshot = readSnapshot(worktree);
|
|
86
|
-
if (!snapshot) return null;
|
|
87
|
-
const record = pickSession(snapshot, sessionId);
|
|
88
|
-
if (!record) return null;
|
|
89
87
|
try {
|
|
88
|
+
const snapshot = readSnapshot(worktree);
|
|
89
|
+
if (!snapshot) return NO_GOAL;
|
|
90
|
+
const record = pickSession(snapshot, sessionId);
|
|
91
|
+
if (!record) return NO_GOAL;
|
|
90
92
|
return sidebarView(record, DEFAULT_CONFIG);
|
|
91
93
|
} catch {
|
|
92
|
-
return
|
|
94
|
+
return NO_GOAL;
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
|
|
@@ -98,7 +100,7 @@ export const id = "goal-mode-sidebar";
|
|
|
98
100
|
/** @type {import("@opencode-ai/plugin/tui").TuiPlugin} */
|
|
99
101
|
export const tui = async (api, options) => {
|
|
100
102
|
try {
|
|
101
|
-
const { enabled, color } = resolveOptions(options, typeof process !== "undefined" ? process.env : {});
|
|
103
|
+
const { enabled, color, muted } = resolveOptions(options, typeof process !== "undefined" ? process.env : {});
|
|
102
104
|
if (!enabled) return;
|
|
103
105
|
if (!api?.slots?.register) return; // runtime without the slot API → no-op.
|
|
104
106
|
|
|
@@ -110,25 +112,26 @@ export const tui = async (api, options) => {
|
|
|
110
112
|
sidebar_content(_ctx, props) {
|
|
111
113
|
const read = () => {
|
|
112
114
|
try {
|
|
113
|
-
return readModel(worktree, props?.session_id);
|
|
115
|
+
return readModel(worktree, props?.session_id) || NO_GOAL;
|
|
114
116
|
} catch {
|
|
115
|
-
return
|
|
117
|
+
return NO_GOAL;
|
|
116
118
|
}
|
|
117
119
|
};
|
|
118
120
|
const [model, setModel] = createSignal(read());
|
|
119
121
|
const timer = setInterval(() => setModel(read()), POLL_MS);
|
|
120
122
|
onCleanup(() => clearInterval(timer));
|
|
123
|
+
// Always render: a muted "No goal" when none is set, the goal in colour otherwise.
|
|
121
124
|
return (
|
|
122
|
-
<
|
|
123
|
-
<
|
|
125
|
+
<box flexDirection="column">
|
|
126
|
+
<Show when={model().hasGoal} fallback={<text fg={muted}>No goal</text>}>
|
|
124
127
|
<text fg={color}>
|
|
125
128
|
{"◆ "}
|
|
126
129
|
<b>GOAL</b>
|
|
127
130
|
{` ${model().goal}`}
|
|
128
131
|
</text>
|
|
129
132
|
<text fg={color}>{model().status}</text>
|
|
130
|
-
</
|
|
131
|
-
</
|
|
133
|
+
</Show>
|
|
134
|
+
</box>
|
|
132
135
|
);
|
|
133
136
|
},
|
|
134
137
|
},
|