opencode-goal-mode 0.4.0 → 0.4.1
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 +37 -0
- package/README.md +55 -35
- package/docs/sidebar-demo.svg +46 -37
- package/package.json +1 -1
- package/plugins/goal-guard/guard.js +7 -0
- package/plugins/goal-guard/summary.js +44 -19
- package/plugins/goal-sidebar.tsx +37 -17
- package/scripts/install.mjs +13 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.4.1
|
|
4
|
+
|
|
5
|
+
### Restructured Goal sidebar todo section
|
|
6
|
+
|
|
7
|
+
- The sidebar Goal section is now a proper stacked, multi-colour layout instead of
|
|
8
|
+
one run of text: a bold **`GOAL`** label on its own line (yellow while running,
|
|
9
|
+
red when done), then the goal title, then a `passing/total gates · status` line —
|
|
10
|
+
each line in its own highlight colour (GOAL yellow, title white, status cyan) so
|
|
11
|
+
they never blend together. It opens with a first-display per-line rainbow, then
|
|
12
|
+
settles.
|
|
13
|
+
- Removed the noisy `· changes pending` suffix from the status line; pending work
|
|
14
|
+
now surfaces as a structured todo row instead.
|
|
15
|
+
- Better, more structured todos: one row per acceptance criterion (✓ when fresh
|
|
16
|
+
evidence covers it), a re-verify row when the tree changed, and one row per
|
|
17
|
+
still-missing review gate by friendly name (e.g. "Pass Security Reviewer").
|
|
18
|
+
- In a goal session the section takes over the sidebar `sidebar_content` slot;
|
|
19
|
+
because OpenCode renders the native todo list as that slot's fallback, it
|
|
20
|
+
replaces the native list on builds that use replace/single-winner slot mode and
|
|
21
|
+
sits alongside it otherwise. Non-goal and no-goal sessions render nothing, so
|
|
22
|
+
the native todo list stays in place.
|
|
23
|
+
|
|
24
|
+
### Build mode is never treated as a goal
|
|
25
|
+
|
|
26
|
+
- Hardened the guard so a Build/Plan/custom session never accumulates goal state.
|
|
27
|
+
`tool.execute.after` bookkeeping (dirty flag, edits, verification, verdicts) now
|
|
28
|
+
runs only for active Goal sessions (and goal-namespace subagent sessions for
|
|
29
|
+
verdict capture). Destructive-command blocking still applies in every mode.
|
|
30
|
+
|
|
31
|
+
### Installer
|
|
32
|
+
|
|
33
|
+
- `--global` now resolves the home directory via `$HOME` and falls back to the OS
|
|
34
|
+
home dir, so it works in shells/containers where `$HOME` is unset.
|
|
35
|
+
- Verified the full install → idempotent re-run → conflict-protection → uninstall
|
|
36
|
+
lifecycle end-to-end on macOS (Node 24) and in clean Linux containers (Apple
|
|
37
|
+
`container`, Node 20 and Node 24) using the real `npm install -g <tarball>` path.
|
|
38
|
+
- Moved **Install** to the top of the README and documented dry-run/uninstall.
|
|
39
|
+
|
|
3
40
|
## v0.4.0
|
|
4
41
|
|
|
5
42
|
### Goal-only subagents
|
package/README.md
CHANGED
|
@@ -1,40 +1,38 @@
|
|
|
1
1
|
# OpenCode Goal Mode
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/opencode-goal-mode)
|
|
4
|
-
[](https://www.npmjs.com/package/opencode-goal-mode)
|
|
5
|
-
[](https://github.com/devinoldenburg/opencode-goal-mode/actions/workflows/ci.yml)
|
|
6
|
-
[](https://github.com/devinoldenburg/opencode-goal-mode/actions/workflows/publish.yml)
|
|
7
|
-
[](LICENSE)
|
|
8
|
-
[](package.json)
|
|
9
|
-
|
|
10
3
|
Strict Goal Mode for OpenCode: a primary `goal` agent, specialized review
|
|
11
4
|
subagents, slash commands, a `goal-guard` plugin that enforces review discipline
|
|
12
|
-
and blocks destructive shell commands, and a
|
|
13
|
-
TUI sidebar.
|
|
5
|
+
and blocks destructive shell commands, and a structured Goal-owned todo section
|
|
6
|
+
in the TUI sidebar.
|
|
14
7
|
|
|
15
8
|
## Install
|
|
16
9
|
|
|
17
|
-
**One command
|
|
10
|
+
**One command.** Needs [Node](https://nodejs.org) 20.11+ and a working
|
|
11
|
+
[OpenCode](https://opencode.ai). Works on macOS and Linux:
|
|
18
12
|
|
|
19
13
|
```bash
|
|
20
14
|
npm install -g opencode-goal-mode && opencode-goal-mode --global
|
|
21
15
|
```
|
|
22
16
|
|
|
23
|
-
Then **restart OpenCode**. That's the whole install
|
|
17
|
+
Then **restart OpenCode**. That's the whole install — it copies the Goal agent,
|
|
24
18
|
review subagents, slash commands, and guard plugin into `~/.config/opencode`, and
|
|
25
19
|
merge-safely registers the Goal todo sidebar in `~/.config/opencode/tui.json`.
|
|
26
20
|
In the agent picker you'll see only the **`goal`** agent; reviewers are subagents
|
|
27
|
-
it drives automatically. The
|
|
28
|
-
|
|
21
|
+
it drives automatically. The install is **idempotent** (re-run it to upgrade in
|
|
22
|
+
place), never touches files you've edited, and `--uninstall` removes exactly what
|
|
23
|
+
it added. Goal Mode inherits your existing OpenCode model/provider.
|
|
29
24
|
|
|
30
25
|
<details>
|
|
31
26
|
<summary>Other ways to install</summary>
|
|
32
27
|
|
|
33
28
|
```bash
|
|
34
|
-
# Global npm install, then run the installer
|
|
29
|
+
# Global npm install, then run the installer separately
|
|
35
30
|
npm install -g opencode-goal-mode
|
|
36
31
|
opencode-goal-mode --global # alias of opencode-goal-mode-install
|
|
37
32
|
|
|
33
|
+
# Preview first, then install (no writes on --dry-run)
|
|
34
|
+
opencode-goal-mode --global --dry-run
|
|
35
|
+
|
|
38
36
|
# Temporary npx install (server-side components work; for the TUI sidebar,
|
|
39
37
|
# prefer the global install above so OpenCode can resolve the package later)
|
|
40
38
|
npx opencode-goal-mode --global
|
|
@@ -42,6 +40,9 @@ npx opencode-goal-mode --global
|
|
|
42
40
|
# Into a single project (writes ./.opencode, including ./.opencode/tui.json)
|
|
43
41
|
npx opencode-goal-mode
|
|
44
42
|
|
|
43
|
+
# Clean removal of everything it installed (incl. its tui.json entry)
|
|
44
|
+
opencode-goal-mode --global --uninstall
|
|
45
|
+
|
|
45
46
|
# From source
|
|
46
47
|
git clone https://github.com/devinoldenburg/opencode-goal-mode
|
|
47
48
|
cd opencode-goal-mode && npm ci && npm run install:global
|
|
@@ -49,16 +50,23 @@ cd opencode-goal-mode && npm ci && npm run install:global
|
|
|
49
50
|
|
|
50
51
|
Use global install for normal daily use. Use project install only when you want
|
|
51
52
|
Goal Mode scoped to one repo and your OpenCode build reads project `.opencode`
|
|
52
|
-
config, including `.opencode/tui.json`.
|
|
53
|
-
`--uninstall` removes only what it installed (and its `tui.json` entry), leaving
|
|
54
|
-
your edits untouched. See [Installer options](#installer-options).
|
|
53
|
+
config, including `.opencode/tui.json`. See [Installer options](#installer-options).
|
|
55
54
|
</details>
|
|
56
55
|
|
|
56
|
+
[](https://www.npmjs.com/package/opencode-goal-mode)
|
|
57
|
+
[](https://www.npmjs.com/package/opencode-goal-mode)
|
|
58
|
+
[](https://github.com/devinoldenburg/opencode-goal-mode/actions/workflows/ci.yml)
|
|
59
|
+
[](https://github.com/devinoldenburg/opencode-goal-mode/actions/workflows/publish.yml)
|
|
60
|
+
[](LICENSE)
|
|
61
|
+
[](package.json)
|
|
62
|
+
|
|
57
63
|

|
|
58
64
|
|
|
59
|
-
<sub>↑ In
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
<sub>↑ In goal mode, the Goal plugin takes over the sidebar todo section with a
|
|
66
|
+
structured, evidence-aware Goal todo list — a bold `GOAL` label, then the goal
|
|
67
|
+
title, gate progress, and per-acceptance/gate todo rows, each on its own line in
|
|
68
|
+
its own colour, with a first-display rainbow. Build and every other mode keep
|
|
69
|
+
OpenCode's native todo section — see [TUI integration](#tui-integration).</sub>
|
|
62
70
|
|
|
63
71
|
**[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)**
|
|
64
72
|
|
|
@@ -218,9 +226,10 @@ second) — negligible for a per-tool-call guard:
|
|
|
218
226
|
reviewer's friendly name, and a single "completion unlocked" toast the moment
|
|
219
227
|
the last required gate clears.
|
|
220
228
|
- An **experimental** companion TUI plugin (`plugins/goal-sidebar.tsx`) that, in
|
|
221
|
-
Goal sessions only,
|
|
222
|
-
|
|
223
|
-
|
|
229
|
+
Goal sessions only, takes over the sidebar todo area with a structured,
|
|
230
|
+
evidence-aware Goal todo list (`GOAL` label, goal title, gate progress, and
|
|
231
|
+
todo rows — each on its own line in its own colour). It shows a first-display
|
|
232
|
+
rainbow, then normal goal colours. See [TUI integration](#tui-integration).
|
|
224
233
|
- A test suite validating the analyzer, plugin hooks, state store, install
|
|
225
234
|
safety, and config compatibility.
|
|
226
235
|
|
|
@@ -230,18 +239,29 @@ Goal Mode is a **plugin pair**: the server-side `goal-guard` plugin owns
|
|
|
230
239
|
enforcement and writes its state to disk, and an experimental TUI plugin
|
|
231
240
|
(`plugins/goal-sidebar.tsx`) reads that same state to render a live todo section.
|
|
232
241
|
|
|
233
|
-
- **Goal-owned todo section.** In a `goal` session
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
-
|
|
240
|
-
|
|
241
|
-
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
242
|
+
- **Goal-owned todo section.** In a `goal` session with a goal set, the Goal
|
|
243
|
+
plugin renders its own structured todo section into the sidebar's `sidebar_content`
|
|
244
|
+
slot, stacked on separate lines, each in its own colour so it never reads as one
|
|
245
|
+
run of text:
|
|
246
|
+
- a bold **`GOAL`** label (yellow while running, red when done);
|
|
247
|
+
- the short goal title;
|
|
248
|
+
- a `passing/total gates · status` line (lifecycle only — no "changes pending"
|
|
249
|
+
noise; pending work shows as a todo row instead);
|
|
250
|
+
- structured todo rows derived from real guard state: one per acceptance
|
|
251
|
+
criterion (✓ when fresh evidence covers it), a re-verify row when the tree
|
|
252
|
+
changed, and one row per still-missing review gate by friendly name
|
|
253
|
+
(e.g. "Pass Security Reviewer").
|
|
254
|
+
|
|
255
|
+
It opens with a first-display rainbow (`sidebarRainbowMs`) so the takeover is
|
|
256
|
+
visible, then settles to the lifecycle colours (running → yellow label; done →
|
|
257
|
+
red). Because OpenCode renders the native todo list as that slot's *fallback*,
|
|
258
|
+
on builds that render `sidebar_content` in replace/single-winner mode the Goal
|
|
259
|
+
section **replaces** the native todo list while a goal is active; in append mode
|
|
260
|
+
it sits alongside it. In every case:
|
|
261
|
+
- **no render** — Build and every non-Goal mode (and a Goal session before a
|
|
262
|
+
goal is set) render nothing here, so OpenCode's native todo section stays in
|
|
263
|
+
the same position. The section is scoped to the session that owns the goal: a
|
|
264
|
+
Build session in the same worktree never inherits another session's goal.
|
|
245
265
|
|
|
246
266
|
Toggle/recolour with `sidebarBanner`, `sidebarColor` (running), `sidebarDoneColor`
|
|
247
267
|
(done), `sidebarMutedColor`, `sidebarRainbowMs`, or the `GOAL_GUARD_SIDEBAR_*`
|
package/docs/sidebar-demo.svg
CHANGED
|
@@ -1,28 +1,31 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="452" viewBox="0 0 1000 452" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" role="img" aria-label="OpenCode Goal Mode sidebar: in a goal session the plugin renders a stacked Goal todo section — a bold GOAL label, the goal title, a gates/status line, and per-gate todo rows — each on its own line in its own colour, opening with a first-display rainbow then settling (GOAL yellow, title white, status cyan) and turning red when done. Build and other modes render no Goal section and keep OpenCode's native todo list.">
|
|
2
2
|
<defs>
|
|
3
3
|
<style>
|
|
4
4
|
.win { fill: #0d1117; stroke: #30363d; stroke-width: 1; }
|
|
5
5
|
.bar { fill: #161b22; }
|
|
6
6
|
.dot { stroke-width: 0; }
|
|
7
7
|
.title { fill: #8b949e; font-size: 12px; }
|
|
8
|
-
.
|
|
9
|
-
.lbl
|
|
10
|
-
.ln
|
|
11
|
-
.sm
|
|
8
|
+
.hdr { fill: #8b949e; font-size: 11px; letter-spacing: 0.5px; }
|
|
9
|
+
.lbl { font-size: 13px; font-weight: 700; }
|
|
10
|
+
.ln { font-size: 13px; }
|
|
11
|
+
.sm { font-size: 12px; }
|
|
12
12
|
.chat { fill: #c9d1d9; font-size: 12px; }
|
|
13
13
|
.dim { fill: #6e7681; font-size: 12px; }
|
|
14
14
|
.ok { fill: #2da44e; font-size: 12px; }
|
|
15
|
-
.nat { fill: #8b949e; font-size: 12px; }
|
|
16
15
|
.div { stroke: #30363d; stroke-width: 1; }
|
|
17
|
-
/* first-display rainbow
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
.
|
|
16
|
+
/* first-display rainbow — successive hues per line (RAINBOW[index]) */
|
|
17
|
+
.r0 { fill: #FF5555; } .r1 { fill: #FFAA00; } .r2 { fill: #FFFF55; } .r3 { fill: #55FF55; }
|
|
18
|
+
/* settled running palette */
|
|
19
|
+
.label { fill: #FFD700; } /* GOAL — yellow */
|
|
20
|
+
.gtitle { fill: #FFFFFF; } /* goal title — bright white */
|
|
21
|
+
.meta { fill: #8BE9FD; } /* gates · status — cyan */
|
|
22
|
+
.todo { fill: #808080; } /* pending todo rows — grey */
|
|
23
|
+
.done { fill: #FF5555; } /* done state — red */
|
|
24
|
+
.check { fill: #50FA7B; } /* ✓ done rows — green */
|
|
21
25
|
</style>
|
|
22
26
|
</defs>
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
<rect class="win" x="1" y="1" width="998" height="358" rx="8"/>
|
|
28
|
+
<rect class="win" x="1" y="1" width="998" height="450" rx="8"/>
|
|
26
29
|
<rect class="bar" x="1" y="1" width="998" height="30" rx="8"/>
|
|
27
30
|
<rect class="bar" x="1" y="20" width="998" height="11"/>
|
|
28
31
|
<circle class="dot" cx="20" cy="16" r="5" fill="#ff5f56"/>
|
|
@@ -30,8 +33,7 @@
|
|
|
30
33
|
<circle class="dot" cx="56" cy="16" r="5" fill="#27c93f"/>
|
|
31
34
|
<text class="title" x="86" y="20">opencode — goal mode</text>
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
<line class="div" x1="470" y1="31" x2="470" y2="359"/>
|
|
36
|
+
<line class="div" x1="470" y1="31" x2="470" y2="451"/>
|
|
35
37
|
|
|
36
38
|
<!-- chat pane (left) -->
|
|
37
39
|
<text class="dim" x="20" y="58">▌ goal</text>
|
|
@@ -40,30 +42,37 @@
|
|
|
40
42
|
<text class="ok" x="20" y="126">✓ Security Reviewer → PASS</text>
|
|
41
43
|
<text class="ok" x="20" y="148">✓ Verifier → PASS</text>
|
|
42
44
|
<text class="dim" x="20" y="170">▸ Diff Reviewer running…</text>
|
|
43
|
-
<text class="dim" x="20" y="
|
|
44
|
-
<text class="dim" x="20" y="
|
|
45
|
-
<text class="dim" x="20" y="
|
|
45
|
+
<text class="dim" x="20" y="212">Build / Plan / custom agents cannot</text>
|
|
46
|
+
<text class="dim" x="20" y="230">invoke the goal-* reviewers, and never</text>
|
|
47
|
+
<text class="dim" x="20" y="248">get a Goal section in their sidebar.</text>
|
|
46
48
|
|
|
47
|
-
<!--
|
|
48
|
-
|
|
49
|
-
<
|
|
50
|
-
<
|
|
51
|
-
<text
|
|
52
|
-
<text class="sm
|
|
53
|
-
<text class="sm
|
|
54
|
-
<text class="sm r2" x="490" y="138">goal-reviewer, goal-diff-reviewer…</text>
|
|
55
|
-
<text class="dim" x="490" y="156">↳ then settles to yellow while running</text>
|
|
49
|
+
<!-- Panel 1: running, first-display rainbow -->
|
|
50
|
+
<text class="hdr" x="490" y="52">GOAL SESSION · running · first-display rainbow</text>
|
|
51
|
+
<line class="div" x1="490" y1="60" x2="980" y2="60"/>
|
|
52
|
+
<text class="lbl r0" x="490" y="80">GOAL</text>
|
|
53
|
+
<text class="ln r1" x="490" y="98">Ship the OAuth refactor</text>
|
|
54
|
+
<text class="sm r2" x="490" y="116">0/5 gates · in progress</text>
|
|
55
|
+
<text class="sm r3" x="490" y="134">□ Pass Prompt Auditor</text>
|
|
56
56
|
|
|
57
|
-
<!--
|
|
58
|
-
<text class="
|
|
59
|
-
<line class="div" x1="490" y1="
|
|
60
|
-
<text
|
|
61
|
-
<text class="
|
|
62
|
-
<text class="sm
|
|
57
|
+
<!-- Panel 2: running, settled (distinct colours per line) -->
|
|
58
|
+
<text class="hdr" x="490" y="166">GOAL SESSION · running · settled colours</text>
|
|
59
|
+
<line class="div" x1="490" y1="174" x2="980" y2="174"/>
|
|
60
|
+
<text class="lbl label" x="490" y="194">GOAL</text>
|
|
61
|
+
<text class="ln gtitle" x="490" y="212">Ship the OAuth refactor</text>
|
|
62
|
+
<text class="sm meta" x="490" y="230">3/5 gates · in progress</text>
|
|
63
|
+
<text class="sm todo" x="490" y="248">□ Pass Security Reviewer</text>
|
|
63
64
|
|
|
64
|
-
<!--
|
|
65
|
-
<text class="
|
|
66
|
-
<line class="div" x1="490" y1="
|
|
67
|
-
<text class="
|
|
68
|
-
<text class="
|
|
65
|
+
<!-- Panel 3: done (red) -->
|
|
66
|
+
<text class="hdr" x="490" y="280">GOAL SESSION · done</text>
|
|
67
|
+
<line class="div" x1="490" y1="288" x2="980" y2="288"/>
|
|
68
|
+
<text class="lbl done" x="490" y="308">GOAL</text>
|
|
69
|
+
<text class="ln done" x="490" y="326">Fix the parser bug</text>
|
|
70
|
+
<text class="sm done" x="490" y="344">5/5 gates · completed · 2 review cycles</text>
|
|
71
|
+
<text class="sm check" x="490" y="362">✓ All acceptance criteria covered</text>
|
|
72
|
+
|
|
73
|
+
<!-- Panel 4: build / other mode → native todos, no Goal section -->
|
|
74
|
+
<text class="hdr" x="490" y="394">BUILD / OTHER MODE</text>
|
|
75
|
+
<line class="div" x1="490" y1="402" x2="980" y2="402"/>
|
|
76
|
+
<text class="todo" x="490" y="422" font-size="12">▢ OpenCode's native todo section</text>
|
|
77
|
+
<text class="dim" x="490" y="440">(no Goal section is rendered here)</text>
|
|
69
78
|
</svg>
|
package/package.json
CHANGED
|
@@ -178,6 +178,13 @@ export function createGuard(input = {}, options = {}, overrides = {}) {
|
|
|
178
178
|
async "tool.execute.after"(inp, out) {
|
|
179
179
|
try {
|
|
180
180
|
const state = store.stateFor(inp?.sessionID);
|
|
181
|
+
// Goal bookkeeping is GOAL-ONLY. A Build/Plan/custom session must never have
|
|
182
|
+
// its edits, mutations, verification, or verdicts recorded as goal state —
|
|
183
|
+
// otherwise the guard would treat a non-Goal message/task as if it were a
|
|
184
|
+
// goal. Only an active Goal session (or a goal-namespace subagent's own
|
|
185
|
+
// session, for verdict capture) is tracked. Destructive-command blocking is
|
|
186
|
+
// handled in tool.execute.before and still applies in every mode.
|
|
187
|
+
if (!state.active && !isGoalAgent(state.currentAgent)) return;
|
|
181
188
|
const tool = inp?.tool;
|
|
182
189
|
const isReviewing = isReviewAgent(state.currentAgent);
|
|
183
190
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { requiredGates, missingGates, gatePassedFresh } from "./gates.js";
|
|
7
|
+
import { prettyAgentName } from "./agents.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* A short, single-line label for the current goal.
|
|
@@ -35,33 +36,55 @@ function criterionEvidenceFresh(state, criterion) {
|
|
|
35
36
|
return entries.some((entry) => evidenceMatchesCriterion(entry, criterion) && evidenceFresh(entry, state));
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
function clip(text, max) {
|
|
40
|
+
const s = String(text || "").replace(/\s+/g, " ").trim();
|
|
41
|
+
return s.length <= max ? s : `${s.slice(0, max - 1).trimEnd()}…`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Structured, ordered todo rows for the sidebar. Each row is a concrete, checkable
|
|
46
|
+
* step toward Goal completion — acceptance criteria first (✓ when fresh evidence
|
|
47
|
+
* covers them), then a re-verify nudge if the tree changed, then ONE row per still-
|
|
48
|
+
* missing review gate (friendly name, e.g. "Pass Security Reviewer"). This is the
|
|
49
|
+
* Goal plugin's own todo system: it is derived from real guard state (contract,
|
|
50
|
+
* evidence freshness, dirty flag, required gates), not transcript memory, so it
|
|
51
|
+
* tracks completion truthfully and updates as gates clear.
|
|
52
|
+
*/
|
|
38
53
|
function sidebarTodos(state, required, missing) {
|
|
39
54
|
const criteria = Array.isArray(state?.contract?.acceptanceCriteria) ? state.contract.acceptanceCriteria : [];
|
|
40
55
|
const items = [];
|
|
41
|
-
for (const criterion of criteria.slice(0,
|
|
42
|
-
const text =
|
|
56
|
+
for (const criterion of criteria.slice(0, 4)) {
|
|
57
|
+
const text = clip(criterion, 52);
|
|
43
58
|
if (!text) continue;
|
|
44
|
-
items.push({
|
|
45
|
-
status: criterionEvidenceFresh(state, text) ? "done" : "todo",
|
|
46
|
-
text: text.length <= 58 ? text : `${text.slice(0, 57).trimEnd()}…`,
|
|
47
|
-
});
|
|
59
|
+
items.push({ status: criterionEvidenceFresh(state, text) ? "done" : "todo", text });
|
|
48
60
|
}
|
|
49
|
-
if (state?.dirty) items.push({ status: "todo", text: "
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
if (state?.dirty) items.push({ status: "todo", text: "Re-verify & re-review after recent edits" });
|
|
62
|
+
// One row per missing/stale review gate, by friendly name — more scannable than a
|
|
63
|
+
// comma-joined id list and it shows exactly which reviewer is still owed.
|
|
64
|
+
for (const gate of missing.slice(0, 4)) {
|
|
65
|
+
items.push({ status: "todo", text: `Pass ${prettyAgentName(gate)}` });
|
|
66
|
+
}
|
|
67
|
+
if (items.length === 0 && required.length > 0) {
|
|
68
|
+
items.push({ status: "todo", text: "Record the Goal Contract & acceptance criteria" });
|
|
69
|
+
}
|
|
70
|
+
return items.slice(0, 8);
|
|
53
71
|
}
|
|
54
72
|
|
|
55
73
|
/**
|
|
56
74
|
* Compact projection for the TUI sidebar todo section. ALWAYS returns an object with a
|
|
57
|
-
* three-way `state`, plus
|
|
58
|
-
*
|
|
59
|
-
* - `
|
|
60
|
-
* - `
|
|
61
|
-
*
|
|
75
|
+
* three-way `state`, plus lines that stack vertically in the sidebar — each rendered
|
|
76
|
+
* on its OWN line in a distinct colour so it never reads as one run of text:
|
|
77
|
+
* - `label` → line 1: the fixed "GOAL" header (yellow when running, red when done).
|
|
78
|
+
* - `goal` → line 2: the short AI goal title.
|
|
79
|
+
* - `gates` → line 3: gate count + lifecycle, e.g. "3/5 gates · in progress" or
|
|
80
|
+
* "5/5 gates · completed · 2 review cycles". No "changes pending"
|
|
81
|
+
* noise — pending work surfaces as a structured todo row instead.
|
|
82
|
+
* - `todos` → following lines: structured acceptance/verification/gate todos.
|
|
62
83
|
* State drives colour: "running" = rainbow first, then yellow; "done" = red;
|
|
63
|
-
* "none" = render nothing so non-Goal
|
|
84
|
+
* "none" = render nothing so non-Goal / no-goal sessions keep the native todo section.
|
|
64
85
|
*/
|
|
86
|
+
export const GOAL_LABEL = "GOAL";
|
|
87
|
+
|
|
65
88
|
export function sidebarView(state, config) {
|
|
66
89
|
if (!state || !state.active) return NO_GOAL;
|
|
67
90
|
const goal = shortGoalLabel(state);
|
|
@@ -76,10 +99,11 @@ export function sidebarView(state, config) {
|
|
|
76
99
|
if (done) {
|
|
77
100
|
return {
|
|
78
101
|
state: "done",
|
|
102
|
+
label: GOAL_LABEL,
|
|
79
103
|
goal,
|
|
80
104
|
gates,
|
|
81
105
|
status: `completed · ${cycles} review cycle${cycles === 1 ? "" : "s"}`,
|
|
82
|
-
todoTitle:
|
|
106
|
+
todoTitle: GOAL_LABEL,
|
|
83
107
|
todos: todos.length ? todos.map((item) => ({ ...item, status: "done" })) : [{ status: "done", text: "All Goal completion gates are clear" }],
|
|
84
108
|
passing,
|
|
85
109
|
required: required.length,
|
|
@@ -88,10 +112,11 @@ export function sidebarView(state, config) {
|
|
|
88
112
|
}
|
|
89
113
|
return {
|
|
90
114
|
state: "running",
|
|
115
|
+
label: GOAL_LABEL,
|
|
91
116
|
goal,
|
|
92
117
|
gates,
|
|
93
|
-
status:
|
|
94
|
-
todoTitle:
|
|
118
|
+
status: "in progress",
|
|
119
|
+
todoTitle: GOAL_LABEL,
|
|
95
120
|
todos,
|
|
96
121
|
passing,
|
|
97
122
|
required: required.length,
|
package/plugins/goal-sidebar.tsx
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Goal Mode — TUI sidebar todo section.
|
|
4
4
|
*
|
|
5
|
-
* In Goal agent sessions this
|
|
6
|
-
* the
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* In Goal agent sessions this renders a Goal-owned, evidence-aware todo section
|
|
6
|
+
* into the sidebar_content slot (GOAL label, goal title, gate/status line, and
|
|
7
|
+
* structured todo rows). OpenCode renders the native todo list as that slot's
|
|
8
|
+
* fallback, so in replace/single-winner slot mode this REPLACES the native todos
|
|
9
|
+
* while a goal is active; rendering nothing (non-Goal or no-goal sessions) brings
|
|
10
|
+
* the native todos back. The section is strictly per-session: it is keyed by
|
|
11
|
+
* props.session_id, so a Build session in the same worktree never inherits
|
|
12
|
+
* another session's goal.
|
|
10
13
|
*
|
|
11
14
|
* How OpenCode loads this: TUI plugins are listed in `~/.config/opencode/tui.json`
|
|
12
15
|
* (NOT the plugins/ dir) and resolved via the package's `exports["./tui"]`. The
|
|
@@ -24,9 +27,12 @@ import { createSignal, onCleanup, For, Show } from "solid-js";
|
|
|
24
27
|
import { sidebarView, NO_GOAL } from "./goal-guard/summary.js";
|
|
25
28
|
import { DEFAULT_CONFIG } from "./goal-guard/config.js";
|
|
26
29
|
|
|
27
|
-
const DEFAULT_COLOR = "#FFD700"; // running — yellow
|
|
30
|
+
const DEFAULT_COLOR = "#FFD700"; // running — GOAL label, yellow
|
|
28
31
|
const DEFAULT_DONE = "#FF5555"; // done — red
|
|
29
|
-
const DEFAULT_MUTED = "#808080"; //
|
|
32
|
+
const DEFAULT_MUTED = "#808080"; // pending todo rows — grey
|
|
33
|
+
const TITLE_COLOR = "#FFFFFF"; // goal title line (running) — bright, distinct from the yellow GOAL label
|
|
34
|
+
const META_COLOR = "#8BE9FD"; // gates · status line (running) — cyan accent
|
|
35
|
+
const TODO_DONE_COLOR = "#50FA7B"; // ✓ done todo rows — green
|
|
30
36
|
const POLL_MS = 1500;
|
|
31
37
|
const RAINBOW = ["#FF5555", "#FFAA00", "#FFFF55", "#55FF55", "#55FFFF", "#5599FF", "#FF55FF"];
|
|
32
38
|
|
|
@@ -96,7 +102,7 @@ const id = "goal-mode-sidebar";
|
|
|
96
102
|
/** @type {import("@opencode-ai/plugin/tui").TuiPlugin} */
|
|
97
103
|
const tui = async (api, options) => {
|
|
98
104
|
try {
|
|
99
|
-
const { enabled, color, doneColor, rainbowMs } = resolveOptions(options, typeof process !== "undefined" ? process.env : {});
|
|
105
|
+
const { enabled, color, doneColor, muted, rainbowMs } = resolveOptions(options, typeof process !== "undefined" ? process.env : {});
|
|
100
106
|
if (!enabled) return;
|
|
101
107
|
if (!api?.slots?.register) return; // runtime without the slot API → no-op.
|
|
102
108
|
|
|
@@ -122,19 +128,33 @@ const tui = async (api, options) => {
|
|
|
122
128
|
const rainbowTimer = setTimeout(() => setRainbow(false), Math.max(0, rainbowMs || 0));
|
|
123
129
|
onCleanup(() => clearInterval(timer));
|
|
124
130
|
onCleanup(() => clearTimeout(rainbowTimer));
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
//
|
|
131
|
+
const isRainbow = () => rainbow() && model().state === "running";
|
|
132
|
+
// Settled (post-rainbow) colour for each header line. When done, every
|
|
133
|
+
// line is red; while running each line gets its OWN highlight colour so
|
|
134
|
+
// the GOAL label, the goal title, and the status never read as one text.
|
|
135
|
+
const settled = (kind) => {
|
|
136
|
+
if (model().state === "done") return doneColor;
|
|
137
|
+
if (kind === "label") return color; // GOAL — yellow
|
|
138
|
+
if (kind === "title") return TITLE_COLOR; // goal title — bright white
|
|
139
|
+
return META_COLOR; // gates · status — cyan
|
|
140
|
+
};
|
|
141
|
+
const lineColor = (index, kind) => (isRainbow() ? RAINBOW[index % RAINBOW.length] : settled(kind));
|
|
142
|
+
const todoColor = (index, item) => {
|
|
143
|
+
if (isRainbow()) return RAINBOW[index % RAINBOW.length];
|
|
144
|
+
if (item.status === "done") return TODO_DONE_COLOR;
|
|
145
|
+
return model().state === "done" ? doneColor : muted;
|
|
146
|
+
};
|
|
147
|
+
// Goal sessions render a Goal-owned todo section (GOAL label, then the goal
|
|
148
|
+
// title, status, and structured todos — each on its own line). Non-Goal /
|
|
149
|
+
// no-goal sessions returned undefined above, so native todos remain.
|
|
128
150
|
return (
|
|
129
151
|
<Show when={model().state !== "none"}>
|
|
130
152
|
<box flexDirection="column" paddingTop={1}>
|
|
131
|
-
<text fg={lineColor(0)}>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
</text>
|
|
135
|
-
<text fg={lineColor(1)}>{`${model().gates} · ${model().status}`}</text>
|
|
153
|
+
<text fg={lineColor(0, "label")}><b>{model().label || "GOAL"}</b></text>
|
|
154
|
+
<text fg={lineColor(1, "title")}>{model().goal}</text>
|
|
155
|
+
<text fg={lineColor(2, "meta")}>{`${model().gates} · ${model().status}`}</text>
|
|
136
156
|
<For each={model().todos || []}>
|
|
137
|
-
{(item, index) => <text fg={
|
|
157
|
+
{(item, index) => <text fg={todoColor(index() + 3, item)}>{`${item.status === "done" ? "✓" : "□"} ${item.text}`}</text>}
|
|
138
158
|
</For>
|
|
139
159
|
</box>
|
|
140
160
|
</Show>
|
package/scripts/install.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { join, resolve, relative, dirname } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { createHash } from "node:crypto";
|
|
18
|
+
import { homedir } from "node:os";
|
|
18
19
|
import { parseArgs } from "node:util";
|
|
19
20
|
|
|
20
21
|
const { values } = parseArgs({
|
|
@@ -67,8 +68,18 @@ if (values.global && values.target) {
|
|
|
67
68
|
function resolveTarget() {
|
|
68
69
|
if (values.target) return resolve(String(values.target));
|
|
69
70
|
if (values.global) {
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
// OpenCode reads global config from ~/.config/opencode. Resolve home from $HOME,
|
|
72
|
+
// falling back to the OS home dir (homedir() works where $HOME is unset, e.g.
|
|
73
|
+
// some CI/container shells) so --global doesn't fail in those environments.
|
|
74
|
+
let home = process.env.HOME;
|
|
75
|
+
if (!home) {
|
|
76
|
+
try {
|
|
77
|
+
home = homedir();
|
|
78
|
+
} catch {
|
|
79
|
+
home = "";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!home) throw new Error("Cannot resolve a home directory for --global install. Pass --target <dir> instead.");
|
|
72
83
|
return join(home, ".config", "opencode");
|
|
73
84
|
}
|
|
74
85
|
return resolve(process.cwd(), ".opencode");
|