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 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
  [![node](https://img.shields.io/node/v/opencode-goal-mode?color=2da44e)](package.json)
9
9
 
10
10
  Strict Goal Mode for OpenCode: a primary `goal` agent, a matrix of specialized
11
- review subagents, slash commands, and a `goal-guard` plugin that enforces review
12
- discipline, blocks destructive shell commands, and preserves goal state across
13
- compaction **and** restarts.
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 -g opencode-goal-mode && opencode-goal-mode-install --global
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
  ![OpenCode Goal Mode sidebar banner](docs/sidebar-demo.svg)
20
49
 
21
- <sub>↑ Illustrative mockup of the **experimental** sidebar banner. The enforcement
22
- core (guard + agents) is the verified product; the TUI sidebar is opt-in and its
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) · [Install](#install) · [Why it's different](#why-its-different) · [Benchmarks](#benchmarks-honest-edition) · [TUI integration](#tui-integration) · [Configuration](#configuration) · [Releasing](#releasing) · [Architecture](ARCHITECTURE.md)**
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 the original
15
- request, explicit/inferred requirements, non-goals, and concrete acceptance
16
- criteria. This activates enforcement, fixes the required specialist review
17
- gates, and lights up the goal banner in the sidebar. Ask only essential
18
- clarifying questions before recording it.
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.6",
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.js",
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 human label for the current goal — preferring the
10
- * recorded Goal Contract's original request, falling back to the captured goal
11
- * text. Collapses whitespace and truncates to `max` chars for compact display
12
- * (status reports, the TUI sidebar banner).
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: "", detail: "" });
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` so the sidebar renders unconditionally:
30
- * - `state: "none"` no active goal: grey "No goal available".
31
- * - `state: "running"` goal in progress: yellow, with a generated status line.
32
- * - `state: "done"` goal complete (all required gates pass, tree clean):
33
- * red, with a generated completion line.
34
- * `goal` is the short goal label; `detail` is generated descriptive text derived
35
- * from the current goal's gate/cycle/dirty state.
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
- detail: `completed · ${passing}/${required.length} gates passed · ${cycles} review cycle${cycles === 1 ? "" : "s"}`,
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
- detail: `in progress · ${bits.join(" · ")}`,
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
- * This is a TUI plugin module (companion to the server-side goal-guard plugin).
6
- * It renders, in the sidebar's content area, the current goal with generated
7
- * status text and a colour that tracks the goal's lifecycle:
8
- * - RUNNING (goal set, in progress) → yellow
9
- * - DONE (all required gates pass, clean) red
10
- * - NONE (a task is running, no goal set) → grey "No goal available"
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
- * IMPORTANT — how OpenCode loads this: TUI plugins are NOT loaded from the
13
- * regular `plugin` array / plugins dir (that is server plugins). They are listed
14
- * in `~/.config/opencode/tui.json`:
15
- * { "$schema": "https://opencode.ai/tui.json", "plugin": ["opencode-goal-mode"] }
16
- * The installer writes that automatically. OpenCode then loads this module
17
- * (the package `main`) and provides the `@opentui/solid` + `solid-js` runtime
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 constraints (from working OpenCode TUI plugins):
21
- * - the module exports a single `export default { id, tui }`;
22
- * - the Bun TUI runtime does not support top-level ESM imports of Node built-ins,
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"; // shining yellow running
33
- const DEFAULT_DONE = "#FF5555"; // redcompleted
34
- const DEFAULT_MUTED = "#808080"; // greyno goal
28
+ const DEFAULT_COLOR = "#FFD700"; // runningyellow
29
+ const DEFAULT_DONE = "#FF5555"; // donered
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
- // Always render: grey "No goal available" when none, else the goal in
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().detail}</text>
126
+ <text fg={fg()}>{model().gates}</text>
127
+ <text fg={fg()}>{model().status}</text>
136
128
  </Show>
137
129
  </box>
138
130
  );