opencode-goal-mode 0.3.5 → 0.3.7
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 +39 -0
- package/README.md +65 -59
- package/agents/goal.md +1 -1
- package/commands/goal.md +6 -5
- package/package.json +8 -1
- package/plugins/goal-guard/config.js +4 -1
- package/plugins/goal-guard/summary.js +44 -13
- package/plugins/goal-guard/tools.js +8 -0
- package/plugins/{goal-sidebar.js → goal-sidebar.tsx} +34 -39
- package/scripts/install.mjs +35 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.3.7
|
|
4
|
+
|
|
5
|
+
- **FIX: the sidebar now actually loads.** OpenCode loads a TUI plugin via the
|
|
6
|
+
package's `exports["./tui"]` subpath (verified against the OpenCode binary), and
|
|
7
|
+
ours had no `exports`, so it silently never loaded. The package now maps
|
|
8
|
+
`"./tui"` (and `main`) to `plugins/goal-sidebar.tsx`, and the entry is shipped as
|
|
9
|
+
`.tsx` so OpenCode transpiles it. Confirmed working in a real OpenCode 1.17.6 TUI.
|
|
10
|
+
- **Sidebar layout, as requested:** three stacked lines — `GOAL <title>`, then the
|
|
11
|
+
gate count (`n/m gates`), then the status (`in progress` / `… · changes pending`
|
|
12
|
+
/ `completed · k review cycles`). The leading orb (◆) is removed.
|
|
13
|
+
- **AI-generated goal title:** `goal_contract` takes a short `title` the Goal agent
|
|
14
|
+
writes (the objective, like a session title); the sidebar shows it, falling back
|
|
15
|
+
to the raw goal text until titled.
|
|
16
|
+
- **One-command install:** `npx opencode-goal-mode --global` (new `opencode-goal-mode`
|
|
17
|
+
bin alias). The installer registers the sidebar in `tui.json` (merge-safe) and
|
|
18
|
+
`--uninstall` removes that entry too. Install instructions moved to the top of the
|
|
19
|
+
README.
|
|
20
|
+
- Colours configurable: `sidebarColor` (running), `sidebarDoneColor` (done),
|
|
21
|
+
`sidebarMutedColor` (no goal).
|
|
22
|
+
|
|
23
|
+
## v0.3.6
|
|
24
|
+
|
|
25
|
+
- **FIX: the sidebar never loaded.** TUI plugins are loaded from
|
|
26
|
+
`~/.config/opencode/tui.json` — NOT the `plugins/` dir (that is server plugins
|
|
27
|
+
only). The installer never created tui.json, so OpenCode never loaded the
|
|
28
|
+
sidebar (it only showed the session title + context). The installer now
|
|
29
|
+
registers the sidebar in `tui.json` (`plugin: ["opencode-goal-mode"]`,
|
|
30
|
+
merge-safe), and the package `main` points at the TUI plugin so OpenCode loads
|
|
31
|
+
it with its `@opentui/solid` runtime. Confirmed OpenCode reads tui.json.
|
|
32
|
+
- **Sidebar behaviour, as requested:** under the sidebar's content/"context"
|
|
33
|
+
area it now shows the goal with generated status text, colour-coded by
|
|
34
|
+
lifecycle: **yellow** while the goal is running, **red** when it is done (all
|
|
35
|
+
required gates pass, tree clean), and **grey "No goal available"** when a task
|
|
36
|
+
is running with no goal set. New `sidebarDoneColor` option
|
|
37
|
+
(`GOAL_GUARD_SIDEBAR_DONE_COLOR`, default `#FF5555`).
|
|
38
|
+
- `summary.sidebarView` now returns `{ state: "none"|"running"|"done", goal,
|
|
39
|
+
detail }`; the headless visual test renders and asserts all three states (text
|
|
40
|
+
+ exact colours), 18/18.
|
|
41
|
+
|
|
3
42
|
## v0.3.5
|
|
4
43
|
|
|
5
44
|
- Verified the package against the **current** OpenCode plugin API
|
package/README.md
CHANGED
|
@@ -8,17 +8,49 @@
|
|
|
8
8
|
[](package.json)
|
|
9
9
|
|
|
10
10
|
Strict Goal Mode for OpenCode: a primary `goal` agent, a matrix of specialized
|
|
11
|
-
review subagents, slash commands,
|
|
12
|
-
discipline
|
|
13
|
-
|
|
11
|
+
review subagents, slash commands, a `goal-guard` plugin that enforces review
|
|
12
|
+
discipline and blocks destructive shell commands, and a live goal banner in the
|
|
13
|
+
TUI sidebar.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
**One command** (needs [Node](https://nodejs.org) 20.11+ and [OpenCode](https://opencode.ai)):
|
|
14
18
|
|
|
15
19
|
```bash
|
|
16
|
-
|
|
20
|
+
npx opencode-goal-mode --global
|
|
17
21
|
```
|
|
18
22
|
|
|
23
|
+
Then **restart OpenCode**. That's the whole install — it copies the Goal agent,
|
|
24
|
+
review subagents, slash commands, and the guard plugin into `~/.config/opencode`,
|
|
25
|
+
and registers the sidebar in `~/.config/opencode/tui.json`. In the agent picker
|
|
26
|
+
you'll see only the **`goal`** agent (the reviewers are subagents it drives).
|
|
27
|
+
|
|
28
|
+
<details>
|
|
29
|
+
<summary>Other ways to install</summary>
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Global npm install, then run the installer
|
|
33
|
+
npm install -g opencode-goal-mode
|
|
34
|
+
opencode-goal-mode --global # alias of opencode-goal-mode-install
|
|
35
|
+
|
|
36
|
+
# Into a single project (writes ./.opencode + ./tui.json)
|
|
37
|
+
npx opencode-goal-mode
|
|
38
|
+
|
|
39
|
+
# From source
|
|
40
|
+
git clone https://github.com/devinoldenburg/opencode-goal-mode
|
|
41
|
+
cd opencode-goal-mode && npm ci && npm run install:global
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`--dry-run` previews changes; `--uninstall` removes only what it installed (and its
|
|
45
|
+
tui.json entry), leaving your edits untouched. See [Installer options](#installer-options).
|
|
46
|
+
</details>
|
|
47
|
+
|
|
19
48
|

|
|
20
49
|
|
|
21
|
-
|
|
50
|
+
<sub>↑ The sidebar goal banner: yellow while a goal runs, red when done, grey "No
|
|
51
|
+
goal available" otherwise — see [TUI integration](#tui-integration).</sub>
|
|
52
|
+
|
|
53
|
+
**[Quick start](#quick-start) · [Why it's different](#why-its-different) · [Benchmarks](#benchmarks-honest-edition) · [TUI integration](#tui-integration) · [Configuration](#configuration) · [Releasing](#releasing) · [Architecture](ARCHITECTURE.md)**
|
|
22
54
|
|
|
23
55
|
## Quick start
|
|
24
56
|
|
|
@@ -172,62 +204,35 @@ Goal Mode is a **plugin pair**: the server-side `goal-guard` plugin owns
|
|
|
172
204
|
enforcement and writes its state to disk, and an experimental TUI plugin
|
|
173
205
|
(`plugins/goal-sidebar.js`) reads that same state to render a live banner.
|
|
174
206
|
|
|
175
|
-
- **Sidebar goal banner
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
OpenCode
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
207
|
+
- **Sidebar goal banner.** In the sidebar's content area, under the session
|
|
208
|
+
title/context, it shows the current goal with generated status text, colour-coded
|
|
209
|
+
by lifecycle:
|
|
210
|
+
- **yellow** — a goal is set and running (`◆ GOAL …` + `in progress · N/M gates`);
|
|
211
|
+
- **red** — the goal is done (all required gates pass, tree clean: `✓ GOAL …` +
|
|
212
|
+
`completed · N/M gates passed · K review cycles`);
|
|
213
|
+
- **grey** — a task is running with no goal set (`No goal available`).
|
|
214
|
+
|
|
215
|
+
Toggle/recolour with `sidebarBanner`, `sidebarColor` (running), `sidebarDoneColor`
|
|
216
|
+
(done), `sidebarMutedColor` (no goal), or the `GOAL_GUARD_SIDEBAR_*` env vars.
|
|
217
|
+
|
|
218
|
+
**How it loads — important.** TUI plugins are **not** loaded from the `plugins/`
|
|
219
|
+
dir; OpenCode loads them from `~/.config/opencode/tui.json`. The installer writes
|
|
220
|
+
that for you (merge-safe):
|
|
221
|
+
|
|
222
|
+
```json
|
|
223
|
+
{ "$schema": "https://opencode.ai/tui.json", "plugin": ["opencode-goal-mode"] }
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Restart OpenCode after install so it picks up the TUI plugin (it resolves the
|
|
227
|
+
package and provides the `@opentui/solid` runtime). The banner appears in a
|
|
228
|
+
**session** view (not the home screen). The three states are rendered and
|
|
229
|
+
asserted — text + exact colours — by a real headless OpenTUI renderer in the
|
|
230
|
+
[visual test](tools/visual-test/README.md) (`npm run test:visual`, 18/18). The
|
|
231
|
+
enforcement core is a separate server plugin and works regardless of the sidebar.
|
|
193
232
|
- **Toasts.** Review verdicts and completion-unlock events surface as toasts
|
|
194
233
|
(`toastOnReview`), and blocked destructive commands / premature completions
|
|
195
234
|
toast as before (`toastOnBlock`).
|
|
196
235
|
|
|
197
|
-
## Install
|
|
198
|
-
|
|
199
|
-
### From npm (recommended)
|
|
200
|
-
|
|
201
|
-
```bash
|
|
202
|
-
npm install -g opencode-goal-mode
|
|
203
|
-
opencode-goal-mode-install --global # installs into ~/.config/opencode
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
Then restart OpenCode (it loads agents, commands, and plugins at startup). In the
|
|
207
|
-
agent picker you will see **only the `goal` agent** — the specialist reviewers are
|
|
208
|
-
subagents the Goal agent drives for you; they are never selectable by the user.
|
|
209
|
-
|
|
210
|
-
Install into a single project instead of globally:
|
|
211
|
-
|
|
212
|
-
```bash
|
|
213
|
-
npm install -D opencode-goal-mode
|
|
214
|
-
npx opencode-goal-mode-install # writes to ./.opencode
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
Upgrade later by re-running the same install command after `npm install -g
|
|
218
|
-
opencode-goal-mode@latest`; the installer replaces only the files it owns and
|
|
219
|
-
leaves your local edits alone (see [Installer options](#installer-options)).
|
|
220
|
-
|
|
221
|
-
### From source
|
|
222
|
-
|
|
223
|
-
```bash
|
|
224
|
-
git clone https://github.com/devinoldenburg/opencode-goal-mode
|
|
225
|
-
cd opencode-goal-mode
|
|
226
|
-
npm ci
|
|
227
|
-
npm run validate
|
|
228
|
-
npm run install:global # or: npm run install:local
|
|
229
|
-
```
|
|
230
|
-
|
|
231
236
|
## Installer options
|
|
232
237
|
|
|
233
238
|
```bash
|
|
@@ -270,8 +275,9 @@ Or via environment variables (`GOAL_GUARD_*`):
|
|
|
270
275
|
| `toastOnBlock` / `GOAL_GUARD_TOAST_ON_BLOCK` | `true` | Toast when something is blocked. |
|
|
271
276
|
| `toastOnReview` / `GOAL_GUARD_TOAST_ON_REVIEW` | `true` | Toast on each review verdict and when completion unlocks. |
|
|
272
277
|
| `sidebarBanner` / `GOAL_GUARD_SIDEBAR_BANNER` | `true` | Show the experimental yellow goal banner in the TUI sidebar. |
|
|
273
|
-
| `sidebarColor` / `GOAL_GUARD_SIDEBAR_COLOR` | `#FFD700` |
|
|
274
|
-
| `
|
|
278
|
+
| `sidebarColor` / `GOAL_GUARD_SIDEBAR_COLOR` | `#FFD700` | Colour of a **running** goal in the sidebar (yellow). |
|
|
279
|
+
| `sidebarDoneColor` / `GOAL_GUARD_SIDEBAR_DONE_COLOR` | `#FF5555` | Colour of a **done** goal in the sidebar (red). |
|
|
280
|
+
| `sidebarMutedColor` / `GOAL_GUARD_SIDEBAR_MUTED_COLOR` | `#808080` | Colour of the "No goal available" line (grey). |
|
|
275
281
|
|
|
276
282
|
## Custom tools
|
|
277
283
|
|
package/agents/goal.md
CHANGED
|
@@ -85,7 +85,7 @@ Required internal artifacts:
|
|
|
85
85
|
|
|
86
86
|
Guard tools (provided by the goal-guard plugin):
|
|
87
87
|
|
|
88
|
-
- Call `goal_contract` once the Goal Contract is settled. This activates strict enforcement and tells the guard which specialist review gates your goal requires (security, data, api, perf, etc., inferred from the contract text).
|
|
88
|
+
- Call `goal_contract` once the Goal Contract is settled. Always include a concise `title` (max ~8 words, no trailing punctuation) that captures what the user ultimately wants — phrased like a session title but about the objective (e.g. "Rate-limit the login endpoint", "Migrate auth to JWT"). This title is shown live in the TUI sidebar, so make it specific and readable. Recording the contract activates strict enforcement and tells the guard which specialist review gates your goal requires (security, data, api, perf, etc., inferred from the contract text).
|
|
89
89
|
- Call `goal_evidence` after each meaningful verification run to record the command and result in the Verification Ledger.
|
|
90
90
|
- Call `goal_status` whenever you are unsure what the guard currently requires; it returns the authoritative list of passing, missing, and stale gates and whether completion is allowed. Trust it over your own recollection.
|
|
91
91
|
- The guard injects a live state block into your context each turn and will rewrite a premature `Goal Completed` into `Goal Not Completed` with the missing gates. Use `goal_status` to avoid that rather than guessing.
|
package/commands/goal.md
CHANGED
|
@@ -11,11 +11,12 @@ $ARGUMENTS
|
|
|
11
11
|
|
|
12
12
|
Run this sequence:
|
|
13
13
|
|
|
14
|
-
1. **Seed the contract first.** Call the `goal_contract` tool with
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
1. **Seed the contract first.** Call the `goal_contract` tool with a concise
|
|
15
|
+
`title` (≤8 words, what the user ultimately wants — like a session title for
|
|
16
|
+
the objective), the original request, explicit/inferred requirements,
|
|
17
|
+
non-goals, and concrete acceptance criteria. This activates enforcement, fixes
|
|
18
|
+
the required specialist review gates, and shows the `title` live in the sidebar
|
|
19
|
+
goal banner. Ask only essential clarifying questions before recording it.
|
|
19
20
|
2. Delegate discovery and research to subagents; implement in the main agent.
|
|
20
21
|
3. Verify, and record each verification with the `goal_evidence` tool so it maps
|
|
21
22
|
to your acceptance criteria.
|
package/package.json
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-goal-mode",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "Strict Goal Mode agents, commands, and guard plugin for OpenCode.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"main": "plugins/goal-sidebar.tsx",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./plugins/goal-sidebar.tsx",
|
|
9
|
+
"./tui": "./plugins/goal-sidebar.tsx",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
6
12
|
"engines": {
|
|
7
13
|
"node": ">=20.11"
|
|
8
14
|
},
|
|
9
15
|
"packageManager": "npm@10",
|
|
10
16
|
"bin": {
|
|
17
|
+
"opencode-goal-mode": "scripts/install.mjs",
|
|
11
18
|
"opencode-goal-mode-install": "scripts/install.mjs"
|
|
12
19
|
},
|
|
13
20
|
"files": [
|
|
@@ -32,7 +32,9 @@ 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
|
|
35
|
+
/** Foreground colour (hex) for a completed goal in the sidebar (running → done turns yellow → red). */
|
|
36
|
+
sidebarDoneColor: "#FF5555",
|
|
37
|
+
/** Foreground colour (hex) for the muted "No goal available" sidebar line. */
|
|
36
38
|
sidebarMutedColor: "#808080",
|
|
37
39
|
/** Phrase that, at the start of an assistant message, claims completion. */
|
|
38
40
|
completionMarker: "Goal Completed",
|
|
@@ -70,6 +72,7 @@ function fromEnv(env) {
|
|
|
70
72
|
GOAL_GUARD_TOAST_ON_REVIEW: ["toastOnReview", coerceBool],
|
|
71
73
|
GOAL_GUARD_SIDEBAR_BANNER: ["sidebarBanner", coerceBool],
|
|
72
74
|
GOAL_GUARD_SIDEBAR_COLOR: ["sidebarColor", (v) => (v == null ? undefined : String(v))],
|
|
75
|
+
GOAL_GUARD_SIDEBAR_DONE_COLOR: ["sidebarDoneColor", (v) => (v == null ? undefined : String(v))],
|
|
73
76
|
GOAL_GUARD_SIDEBAR_MUTED_COLOR: ["sidebarMutedColor", (v) => (v == null ? undefined : String(v))],
|
|
74
77
|
};
|
|
75
78
|
for (const [key, [field, coerce]] of Object.entries(map)) {
|
|
@@ -6,12 +6,18 @@
|
|
|
6
6
|
import { requiredGates, missingGates, gatePassedFresh } from "./gates.js";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* A short, single-line
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* (
|
|
9
|
+
* A short, single-line label for the current goal.
|
|
10
|
+
*
|
|
11
|
+
* Prefers `contract.title` — a concise, AI-generated summary of the objective
|
|
12
|
+
* (what the user wants), written by the Goal agent when it records the contract
|
|
13
|
+
* via `goal_contract` (think "session title", but the goal/objective). Falls back
|
|
14
|
+
* to the contract's original request, then the captured goal text, so something
|
|
15
|
+
* sensible still shows before the agent has titled the goal. Collapses whitespace
|
|
16
|
+
* and truncates to `max` chars for the compact sidebar.
|
|
13
17
|
*/
|
|
14
18
|
export function shortGoalLabel(state, max = 80) {
|
|
19
|
+
const title = String(state?.contract?.title || "").replace(/\s+/g, " ").trim();
|
|
20
|
+
if (title) return title.length <= max ? title : `${title.slice(0, max - 1).trimEnd()}…`;
|
|
15
21
|
const raw = String(state?.contract?.original || state?.goalText || "").replace(/\s+/g, " ").trim();
|
|
16
22
|
if (!raw) return "";
|
|
17
23
|
// Prefer the first sentence/clause if it is reasonably short.
|
|
@@ -21,14 +27,18 @@ export function shortGoalLabel(state, max = 80) {
|
|
|
21
27
|
return `${base.slice(0, max - 1).trimEnd()}…`;
|
|
22
28
|
}
|
|
23
29
|
|
|
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({
|
|
30
|
+
/** Sentinel for "a task is running but no goal is set" — the sidebar shows a muted "No goal available". */
|
|
31
|
+
export const NO_GOAL = Object.freeze({ state: "none", goal: "", gates: "", status: "" });
|
|
26
32
|
|
|
27
33
|
/**
|
|
28
|
-
* Compact projection for the TUI sidebar banner. ALWAYS returns an object
|
|
29
|
-
*
|
|
30
|
-
* - `
|
|
31
|
-
* - `
|
|
34
|
+
* Compact projection for the TUI sidebar banner. ALWAYS returns an object with a
|
|
35
|
+
* three-way `state`, plus three lines that stack vertically in the sidebar:
|
|
36
|
+
* - `goal` → line 1: the short AI goal title.
|
|
37
|
+
* - `gates` → line 2: the gate count, e.g. "0/7 gates".
|
|
38
|
+
* - `status` → line 3: the lifecycle status, e.g. "in progress · changes pending"
|
|
39
|
+
* or "completed · 2 review cycles".
|
|
40
|
+
* State drives colour: "running" = yellow, "done" = red, "none" = grey
|
|
41
|
+
* ("No goal available").
|
|
32
42
|
*/
|
|
33
43
|
export function sidebarView(state, config) {
|
|
34
44
|
if (!state || !state.active) return NO_GOAL;
|
|
@@ -37,9 +47,30 @@ export function sidebarView(state, config) {
|
|
|
37
47
|
const required = requiredGates(state, config);
|
|
38
48
|
const missing = missingGates(state, config);
|
|
39
49
|
const passing = required.length - missing.length;
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
50
|
+
const cycles = Number(state.reviewCycles) || 0;
|
|
51
|
+
const gates = `${passing}/${required.length} gates`;
|
|
52
|
+
const done = required.length > 0 && missing.length === 0 && !state.dirty;
|
|
53
|
+
if (done) {
|
|
54
|
+
return {
|
|
55
|
+
state: "done",
|
|
56
|
+
goal,
|
|
57
|
+
gates,
|
|
58
|
+
status: `completed · ${cycles} review cycle${cycles === 1 ? "" : "s"}`,
|
|
59
|
+
passing,
|
|
60
|
+
required: required.length,
|
|
61
|
+
reviewCycles: cycles,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
state: "running",
|
|
66
|
+
goal,
|
|
67
|
+
gates,
|
|
68
|
+
status: `in progress${state.dirty ? " · changes pending" : ""}`,
|
|
69
|
+
passing,
|
|
70
|
+
required: required.length,
|
|
71
|
+
reviewCycles: cycles,
|
|
72
|
+
dirty: Boolean(state.dirty),
|
|
73
|
+
};
|
|
43
74
|
}
|
|
44
75
|
|
|
45
76
|
export function summarizeState(state, config) {
|
|
@@ -92,6 +92,13 @@ export function createGoalTools({ store, config, persist }) {
|
|
|
92
92
|
"inferred requirements, non-goals, and acceptance criteria). Establishing a contract " +
|
|
93
93
|
"activates strict goal enforcement and drives which specialist review gates are required.",
|
|
94
94
|
args: {
|
|
95
|
+
title: s
|
|
96
|
+
.string()
|
|
97
|
+
.describe(
|
|
98
|
+
"A short (max ~8 words) human-friendly title summarizing the GOAL/objective — what the user " +
|
|
99
|
+
"ultimately wants, phrased like a session title but about the outcome. Shown in the TUI sidebar. " +
|
|
100
|
+
"e.g. 'Rate-limit the login endpoint', 'Migrate auth to JWT'. No trailing punctuation.",
|
|
101
|
+
),
|
|
95
102
|
original: s.string().describe("The original user request, verbatim or faithfully summarized."),
|
|
96
103
|
requirements: s.array(s.string()).optional().describe("Explicit requirements stated by the user."),
|
|
97
104
|
inferred: s.array(s.string()).optional().describe("Reasonable inferred requirements."),
|
|
@@ -104,6 +111,7 @@ export function createGoalTools({ store, config, persist }) {
|
|
|
104
111
|
const state = store.stateFor(ctx.sessionID);
|
|
105
112
|
state.active = true;
|
|
106
113
|
state.contract = {
|
|
114
|
+
title: String(args.title || "").replace(/\s+/g, " ").trim(),
|
|
107
115
|
original: String(args.original || ""),
|
|
108
116
|
requirements: args.requirements || [],
|
|
109
117
|
inferred: args.inferred || [],
|
|
@@ -1,38 +1,33 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
/**
|
|
3
|
-
* Goal Mode —
|
|
3
|
+
* Goal Mode — TUI sidebar goal banner.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Renders, in the sidebar's content area, the current goal as three stacked lines:
|
|
6
|
+
* 1. `GOAL <short AI title>` (the objective, generated by the Goal agent)
|
|
7
|
+
* 2. `<n>/<m> gates` (review-gate progress)
|
|
8
|
+
* 3. `<status>` (in progress / changes pending / completed …)
|
|
9
|
+
* Colour tracks lifecycle: yellow while running, red when done, grey
|
|
10
|
+
* "No goal available" when a task has no goal set.
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* How OpenCode loads this: TUI plugins are listed in `~/.config/opencode/tui.json`
|
|
13
|
+
* (NOT the plugins/ dir) and resolved via the package's `exports["./tui"]`. The
|
|
14
|
+
* installer writes tui.json (`plugin: ["opencode-goal-mode"]`); package.json maps
|
|
15
|
+
* `./tui` → this file; OpenCode supplies the `@opentui/solid` + `solid-js` runtime
|
|
16
|
+
* (declared as peer deps). The pure projection (`summary.sidebarView`) is shared
|
|
17
|
+
* with the server plugin and unit-tested via goal-guard/sidebar-data.js.
|
|
13
18
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* plugin and unit-tested via goal-guard/sidebar-data.js; only the file read and
|
|
18
|
-
* state-path computation are reimplemented here.
|
|
19
|
-
*
|
|
20
|
-
* Runtime constraints (mirrored from working OpenCode TUI plugins):
|
|
21
|
-
* - TUI plugin modules export `export default { id, tui }`.
|
|
22
|
-
* - The Bun TUI plugin runtime does NOT support top-level ESM imports of Node
|
|
23
|
-
* built-ins, so `node:fs`/`node:path`/`node:os`/`node:crypto` are `require()`d
|
|
24
|
-
* lazily inside functions. Top-level imports of regular packages (solid-js)
|
|
25
|
-
* and of our Node-built-in-free local modules are fine.
|
|
26
|
-
* - This file uses Solid/opentui JSX and is loaded only by OpenCode's (Bun) TUI
|
|
27
|
-
* runtime, which transpiles it; it is never imported by the Node test suite.
|
|
19
|
+
* Runtime notes: single `export default { id, tui }`; node built-ins are require()d
|
|
20
|
+
* lazily (the Bun TUI runtime rejects top-level node: imports). Never imported by
|
|
21
|
+
* the Node test suite.
|
|
28
22
|
*/
|
|
29
23
|
|
|
30
24
|
import { createSignal, onCleanup, Show } from "solid-js";
|
|
31
25
|
import { sidebarView, NO_GOAL } from "./goal-guard/summary.js";
|
|
32
26
|
import { DEFAULT_CONFIG } from "./goal-guard/config.js";
|
|
33
27
|
|
|
34
|
-
const DEFAULT_COLOR = "#FFD700"; //
|
|
35
|
-
const
|
|
28
|
+
const DEFAULT_COLOR = "#FFD700"; // running — yellow
|
|
29
|
+
const DEFAULT_DONE = "#FF5555"; // done — red
|
|
30
|
+
const DEFAULT_MUTED = "#808080"; // no goal — grey
|
|
36
31
|
const POLL_MS = 1500;
|
|
37
32
|
|
|
38
33
|
function resolveOptions(options, env) {
|
|
@@ -41,16 +36,15 @@ function resolveOptions(options, env) {
|
|
|
41
36
|
const enabledEnv = e.GOAL_GUARD_SIDEBAR_BANNER;
|
|
42
37
|
const disabled =
|
|
43
38
|
enabledOpt === false || enabledEnv === "0" || enabledEnv === "false" || enabledEnv === "off";
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
return {
|
|
40
|
+
enabled: !disabled,
|
|
41
|
+
color: options?.sidebarColor || e.GOAL_GUARD_SIDEBAR_COLOR || DEFAULT_COLOR,
|
|
42
|
+
doneColor: options?.sidebarDoneColor || e.GOAL_GUARD_SIDEBAR_DONE_COLOR || DEFAULT_DONE,
|
|
43
|
+
muted: options?.sidebarMutedColor || e.GOAL_GUARD_SIDEBAR_MUTED_COLOR || DEFAULT_MUTED,
|
|
44
|
+
};
|
|
47
45
|
}
|
|
48
46
|
|
|
49
|
-
/**
|
|
50
|
-
* Read the guard's persisted snapshot for a worktree. The state-path logic is
|
|
51
|
-
* kept identical to goal-guard/persistence.js (stateBaseDir + projectKey); node
|
|
52
|
-
* built-ins are required lazily to satisfy the TUI runtime.
|
|
53
|
-
*/
|
|
47
|
+
/** Read the guard's persisted snapshot for a worktree (path logic mirrors persistence.js). */
|
|
54
48
|
function readSnapshot(worktree) {
|
|
55
49
|
try {
|
|
56
50
|
const fs = require("node:fs");
|
|
@@ -100,7 +94,7 @@ const id = "goal-mode-sidebar";
|
|
|
100
94
|
/** @type {import("@opencode-ai/plugin/tui").TuiPlugin} */
|
|
101
95
|
const tui = async (api, options) => {
|
|
102
96
|
try {
|
|
103
|
-
const { enabled, color, muted } = resolveOptions(options, typeof process !== "undefined" ? process.env : {});
|
|
97
|
+
const { enabled, color, doneColor, muted } = resolveOptions(options, typeof process !== "undefined" ? process.env : {});
|
|
104
98
|
if (!enabled) return;
|
|
105
99
|
if (!api?.slots?.register) return; // runtime without the slot API → no-op.
|
|
106
100
|
|
|
@@ -120,16 +114,17 @@ const tui = async (api, options) => {
|
|
|
120
114
|
const [model, setModel] = createSignal(read());
|
|
121
115
|
const timer = setInterval(() => setModel(read()), POLL_MS);
|
|
122
116
|
onCleanup(() => clearInterval(timer));
|
|
123
|
-
|
|
117
|
+
const fg = () => (model().state === "done" ? doneColor : color);
|
|
118
|
+
// Three stacked lines: GOAL+title, gates, status — or grey "No goal available".
|
|
124
119
|
return (
|
|
125
|
-
<box flexDirection="column">
|
|
126
|
-
<Show when={model().
|
|
127
|
-
<text fg={
|
|
128
|
-
{"◆ "}
|
|
120
|
+
<box flexDirection="column" paddingTop={1}>
|
|
121
|
+
<Show when={model().state !== "none"} fallback={<text fg={muted}>No goal available</text>}>
|
|
122
|
+
<text fg={fg()}>
|
|
129
123
|
<b>GOAL</b>
|
|
130
124
|
{` ${model().goal}`}
|
|
131
125
|
</text>
|
|
132
|
-
<text fg={
|
|
126
|
+
<text fg={fg()}>{model().gates}</text>
|
|
127
|
+
<text fg={fg()}>{model().status}</text>
|
|
133
128
|
</Show>
|
|
134
129
|
</box>
|
|
135
130
|
);
|
package/scripts/install.mjs
CHANGED
|
@@ -125,6 +125,36 @@ function loadManifest() {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Register (or remove) the TUI sidebar plugin in `<target>/tui.json`.
|
|
130
|
+
*
|
|
131
|
+
* TUI plugins are NOT loaded from the `plugins/` dir (that path is for server
|
|
132
|
+
* plugins); OpenCode loads them only from tui.json. We reference the published
|
|
133
|
+
* npm package by name so OpenCode resolves it with its `@opentui/solid` runtime.
|
|
134
|
+
* Merge-safe: preserves any existing entries and only touches our own.
|
|
135
|
+
*/
|
|
136
|
+
const TUI_PLUGIN_SPEC = pkg.name;
|
|
137
|
+
function ensureTuiPlugin(remove = false) {
|
|
138
|
+
const tuiPath = join(target, "tui.json");
|
|
139
|
+
let data = { $schema: "https://opencode.ai/tui.json", plugin: [] };
|
|
140
|
+
try {
|
|
141
|
+
const existing = JSON.parse(readFileSync(tuiPath, "utf8"));
|
|
142
|
+
if (existing && typeof existing === "object") data = existing;
|
|
143
|
+
} catch {
|
|
144
|
+
/* missing or invalid → start fresh */
|
|
145
|
+
}
|
|
146
|
+
if (!Array.isArray(data.plugin)) data.plugin = [];
|
|
147
|
+
const has = data.plugin.includes(TUI_PLUGIN_SPEC);
|
|
148
|
+
if (remove ? !has : has) return false;
|
|
149
|
+
data.plugin = remove ? data.plugin.filter((p) => p !== TUI_PLUGIN_SPEC) : [...data.plugin, TUI_PLUGIN_SPEC];
|
|
150
|
+
if (!data.$schema) data.$schema = "https://opencode.ai/tui.json";
|
|
151
|
+
if (!values["dry-run"]) {
|
|
152
|
+
mkdirSync(target, { recursive: true });
|
|
153
|
+
writeFileSync(tuiPath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
128
158
|
// ---------------------------------------------------------------------------
|
|
129
159
|
// Uninstall
|
|
130
160
|
// ---------------------------------------------------------------------------
|
|
@@ -144,7 +174,9 @@ if (values.uninstall) {
|
|
|
144
174
|
}
|
|
145
175
|
}
|
|
146
176
|
if (!values["dry-run"] && existsSync(manifestPath)) rmSync(manifestPath, { force: true });
|
|
177
|
+
const tuiRemoved = ensureTuiPlugin(true);
|
|
147
178
|
if (!values["dry-run"]) pruneEmptyDirs(target, Object.keys(manifest.files));
|
|
179
|
+
if (tuiRemoved) console.log(`${values["dry-run"] ? "Would remove" : "Removed"} the sidebar entry from ${join(target, "tui.json")}`);
|
|
148
180
|
const verb = values["dry-run"] ? "Would remove" : "Removed";
|
|
149
181
|
console.log(`${verb} ${removed.length} Goal Mode files from ${target}.`);
|
|
150
182
|
if (kept.length) {
|
|
@@ -234,9 +266,12 @@ if (!values["dry-run"]) {
|
|
|
234
266
|
writeFileSync(manifestPath, JSON.stringify({ version: pkg.version, files: newManifestFiles }, null, 2), "utf8");
|
|
235
267
|
}
|
|
236
268
|
|
|
269
|
+
const tuiAdded = ensureTuiPlugin(false);
|
|
270
|
+
|
|
237
271
|
const verb = values["dry-run"] ? "Would install" : "Installed";
|
|
238
272
|
console.log(`${verb} OpenCode Goal Mode ${pkg.version} into ${target}`);
|
|
239
273
|
console.log(
|
|
240
274
|
`Files copied: ${summary.copied.length}; unchanged: ${summary.unchanged.length}; pruned: ${summary.pruned.length}`,
|
|
241
275
|
);
|
|
276
|
+
if (tuiAdded) console.log(`Registered the experimental sidebar in ${join(target, "tui.json")}`);
|
|
242
277
|
console.log("Restart OpenCode for agents, commands, and plugins to load.");
|