opencode-goal-mode 0.3.6 → 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 +20 -0
- package/README.md +36 -42
- package/agents/goal.md +1 -1
- package/commands/goal.md +6 -5
- package/package.json +8 -2
- package/plugins/goal-guard/summary.js +23 -17
- package/plugins/goal-guard/tools.js +8 -0
- package/plugins/{goal-sidebar.js → goal-sidebar.tsx} +22 -30
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
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
|
+
|
|
3
23
|
## v0.3.6
|
|
4
24
|
|
|
5
25
|
- **FIX: the sidebar never loaded.** TUI plugins are loaded from
|
package/README.md
CHANGED
|
@@ -8,21 +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)):
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx opencode-goal-mode --global
|
|
21
|
+
```
|
|
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>
|
|
14
30
|
|
|
15
31
|
```bash
|
|
16
|
-
npm install
|
|
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
|
|
17
42
|
```
|
|
18
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
|
-
<sub>↑
|
|
22
|
-
|
|
23
|
-
live render depends on your OpenCode build — see [TUI integration](#tui-integration).</sub>
|
|
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>
|
|
24
52
|
|
|
25
|
-
**[Quick start](#quick-start) · [
|
|
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)**
|
|
26
54
|
|
|
27
55
|
## Quick start
|
|
28
56
|
|
|
@@ -205,40 +233,6 @@ enforcement and writes its state to disk, and an experimental TUI plugin
|
|
|
205
233
|
(`toastOnReview`), and blocked destructive commands / premature completions
|
|
206
234
|
toast as before (`toastOnBlock`).
|
|
207
235
|
|
|
208
|
-
## Install
|
|
209
|
-
|
|
210
|
-
### From npm (recommended)
|
|
211
|
-
|
|
212
|
-
```bash
|
|
213
|
-
npm install -g opencode-goal-mode
|
|
214
|
-
opencode-goal-mode-install --global # installs into ~/.config/opencode
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
Then restart OpenCode (it loads agents, commands, and plugins at startup). In the
|
|
218
|
-
agent picker you will see **only the `goal` agent** — the specialist reviewers are
|
|
219
|
-
subagents the Goal agent drives for you; they are never selectable by the user.
|
|
220
|
-
|
|
221
|
-
Install into a single project instead of globally:
|
|
222
|
-
|
|
223
|
-
```bash
|
|
224
|
-
npm install -D opencode-goal-mode
|
|
225
|
-
npx opencode-goal-mode-install # writes to ./.opencode
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
Upgrade later by re-running the same install command after `npm install -g
|
|
229
|
-
opencode-goal-mode@latest`; the installer replaces only the files it owns and
|
|
230
|
-
leaves your local edits alone (see [Installer options](#installer-options)).
|
|
231
|
-
|
|
232
|
-
### From source
|
|
233
|
-
|
|
234
|
-
```bash
|
|
235
|
-
git clone https://github.com/devinoldenburg/opencode-goal-mode
|
|
236
|
-
cd opencode-goal-mode
|
|
237
|
-
npm ci
|
|
238
|
-
npm run validate
|
|
239
|
-
npm run install:global # or: npm run install:local
|
|
240
|
-
```
|
|
241
|
-
|
|
242
236
|
## Installer options
|
|
243
237
|
|
|
244
238
|
```bash
|
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,14 +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.
|
|
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
|
+
},
|
|
7
12
|
"engines": {
|
|
8
13
|
"node": ">=20.11"
|
|
9
14
|
},
|
|
10
15
|
"packageManager": "npm@10",
|
|
11
16
|
"bin": {
|
|
17
|
+
"opencode-goal-mode": "scripts/install.mjs",
|
|
12
18
|
"opencode-goal-mode-install": "scripts/install.mjs"
|
|
13
19
|
},
|
|
14
20
|
"files": [
|
|
@@ -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.
|
|
@@ -22,17 +28,17 @@ export function shortGoalLabel(state, max = 80) {
|
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
/** Sentinel for "a task is running but no goal is set" — the sidebar shows a muted "No goal available". */
|
|
25
|
-
export const NO_GOAL = Object.freeze({ state: "none", goal: "",
|
|
31
|
+
export const NO_GOAL = Object.freeze({ state: "none", goal: "", gates: "", status: "" });
|
|
26
32
|
|
|
27
33
|
/**
|
|
28
34
|
* Compact projection for the TUI sidebar banner. ALWAYS returns an object with a
|
|
29
|
-
* three-way `state
|
|
30
|
-
* - `
|
|
31
|
-
* - `
|
|
32
|
-
* - `
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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").
|
|
36
42
|
*/
|
|
37
43
|
export function sidebarView(state, config) {
|
|
38
44
|
if (!state || !state.active) return NO_GOAL;
|
|
@@ -42,24 +48,24 @@ export function sidebarView(state, config) {
|
|
|
42
48
|
const missing = missingGates(state, config);
|
|
43
49
|
const passing = required.length - missing.length;
|
|
44
50
|
const cycles = Number(state.reviewCycles) || 0;
|
|
51
|
+
const gates = `${passing}/${required.length} gates`;
|
|
45
52
|
const done = required.length > 0 && missing.length === 0 && !state.dirty;
|
|
46
53
|
if (done) {
|
|
47
54
|
return {
|
|
48
55
|
state: "done",
|
|
49
56
|
goal,
|
|
50
|
-
|
|
57
|
+
gates,
|
|
58
|
+
status: `completed · ${cycles} review cycle${cycles === 1 ? "" : "s"}`,
|
|
51
59
|
passing,
|
|
52
60
|
required: required.length,
|
|
53
61
|
reviewCycles: cycles,
|
|
54
62
|
};
|
|
55
63
|
}
|
|
56
|
-
const bits = [`${passing}/${required.length} gates`];
|
|
57
|
-
if (state.dirty) bits.push("changes pending");
|
|
58
|
-
if (cycles) bits.push(`cycle ${cycles}`);
|
|
59
64
|
return {
|
|
60
65
|
state: "running",
|
|
61
66
|
goal,
|
|
62
|
-
|
|
67
|
+
gates,
|
|
68
|
+
status: `in progress${state.dirty ? " · changes pending" : ""}`,
|
|
63
69
|
passing,
|
|
64
70
|
required: required.length,
|
|
65
71
|
reviewCycles: cycles,
|
|
@@ -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 || [],
|
|
@@ -2,36 +2,32 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Goal Mode — TUI sidebar goal banner.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* (declared here as peer deps), so its slot shares OpenCode's renderer.
|
|
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.
|
|
19
18
|
*
|
|
20
|
-
* Runtime
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* so node:fs/path/os/crypto are require()d lazily inside functions;
|
|
24
|
-
* - it is never imported by the Node test suite (the pure projection it uses,
|
|
25
|
-
* summary.sidebarView, is tested via goal-guard/sidebar-data.js).
|
|
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.
|
|
26
22
|
*/
|
|
27
23
|
|
|
28
24
|
import { createSignal, onCleanup, Show } from "solid-js";
|
|
29
25
|
import { sidebarView, NO_GOAL } from "./goal-guard/summary.js";
|
|
30
26
|
import { DEFAULT_CONFIG } from "./goal-guard/config.js";
|
|
31
27
|
|
|
32
|
-
const DEFAULT_COLOR = "#FFD700"; //
|
|
33
|
-
const DEFAULT_DONE = "#FF5555"; //
|
|
34
|
-
const DEFAULT_MUTED = "#808080"; //
|
|
28
|
+
const DEFAULT_COLOR = "#FFD700"; // running — yellow
|
|
29
|
+
const DEFAULT_DONE = "#FF5555"; // done — red
|
|
30
|
+
const DEFAULT_MUTED = "#808080"; // no goal — grey
|
|
35
31
|
const POLL_MS = 1500;
|
|
36
32
|
|
|
37
33
|
function resolveOptions(options, env) {
|
|
@@ -119,20 +115,16 @@ const tui = async (api, options) => {
|
|
|
119
115
|
const timer = setInterval(() => setModel(read()), POLL_MS);
|
|
120
116
|
onCleanup(() => clearInterval(timer));
|
|
121
117
|
const fg = () => (model().state === "done" ? doneColor : color);
|
|
122
|
-
//
|
|
123
|
-
// yellow (running) / red (done) with generated status text.
|
|
118
|
+
// Three stacked lines: GOAL+title, gates, status — or grey "No goal available".
|
|
124
119
|
return (
|
|
125
120
|
<box flexDirection="column" paddingTop={1}>
|
|
126
|
-
<Show
|
|
127
|
-
when={model().state !== "none"}
|
|
128
|
-
fallback={<text fg={muted}>No goal available</text>}
|
|
129
|
-
>
|
|
121
|
+
<Show when={model().state !== "none"} fallback={<text fg={muted}>No goal available</text>}>
|
|
130
122
|
<text fg={fg()}>
|
|
131
|
-
{model().state === "done" ? "✓ " : "◆ "}
|
|
132
123
|
<b>GOAL</b>
|
|
133
124
|
{` ${model().goal}`}
|
|
134
125
|
</text>
|
|
135
|
-
<text fg={fg()}>{model().
|
|
126
|
+
<text fg={fg()}>{model().gates}</text>
|
|
127
|
+
<text fg={fg()}>{model().status}</text>
|
|
136
128
|
</Show>
|
|
137
129
|
</box>
|
|
138
130
|
);
|