Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,46 @@ changed, why it matters, and which user-visible guarantee got stronger. Use it a
the fastest factual tour of Goal Mode's evolution from prompt discipline into a
guard-enforced workflow.

## v0.7.0

### Feature: YOLO mode + a fully customizable guard

Every gate is now tunable from `opencode.json` options or `GOAL_GUARD_*` env vars,
with a one-switch escape hatch:

- **`yolo`** relaxes the soft gates (network-exec blocking, completion
enforcement, the Goal-only subagent lock, block toasts) while keeping
destructive-command guarding on.
- **`allowDestructive`** turns destructive guarding off too — with `yolo`, that's
*full YOLO*: the guard blocks nothing. It also works standalone.
- **`allowCommands`** (regex allow-list) waves specific commands through whatever
the analyzer thinks; **`extraDestructive`** (regex deny-list) adds your own
destructive rules. Invalid patterns are ignored, never fatal.
- YOLO only relaxes keys you did **not** set explicitly, so any per-key option
still wins.

### Feature: a customization tool, skill, and command

- **`goal-config`** (`scripts/goal-config.mjs`, also `opencode-goal-mode-config`):
`list` every key with its default/env/summary, `explain <key>` with the exact
snippet to set it, `recipes`/`recipe <name>` for paste-ready presets
(`safe-yolo`, `full-yolo`, `allow-commands`, …), and `effective '<json>' --diff`
to preview the resolved config before you commit.
- A **goal-mode-customization skill** and a **`/goal-mode-customize` command** that
teach the agent the discover → choose → apply → verify loop. Both ship and
install with the package (`skills/` is a component dir) and refresh on update.
- `CONFIG_DOCS` is the single doc source for every key; a test keeps it in lockstep
with `DEFAULT_CONFIG` so docs can't drift.

### Hardening since v0.6.12 (≈190 issues)

A large triage-and-fix sweep landed: deeper shell-analyzer coverage and bypass
fixes (container runtimes, interpreter/decoder sinks, fail-*closed* on parser
error), completion-bypass and state-leak fixes in the guard core, Goal Lab
server/web hardening, sidebar and agent-definition cleanups, an atomic installer,
Biome linting, and a CI overhaul (dependency-review fixed, CodeQL
security-extended, release gating + post-publish smoke test, Dependabot).

## v0.6.12

### Fix: a failing review could be recorded as PASS (safety-critical)
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,15 @@ GOAL_GUARD_YOLO=1 GOAL_GUARD_ALLOW_DESTRUCTIVE=1 opencode
- add `allowDestructive: true` → that last guard drops too: full YOLO.
- Prefer surgical control? Leave YOLO off and use `allowCommands` (whitelist exactly the commands you want to wave through) and/or `extraDestructive` (block extra ones), e.g. `{ "allowCommands": ["^docker compose ", "^rm -rf \\./tmp/"] }`.

The customization skill (`/goal-mode-customize`, installed alongside the plugin) walks the agent through tuning every one of these.
**Don't guess — use the tool.** `goal-config` (installed as `opencode-goal-mode-config`, or `node scripts/goal-config.mjs` in the repo) lists every key, explains how to set one, ships paste-ready recipes, and previews the resolved config:

```bash
opencode-goal-mode-config list # every key: default, env var, what it does
opencode-goal-mode-config recipe full-yolo # paste-ready opencode.json snippet
opencode-goal-mode-config effective '{"yolo":true}' --diff # confirm what it resolves to
```

The customization skill and the `/goal-mode-customize` command (both installed alongside the plugin) walk the agent through the discover → apply → verify loop on top of this tool.

**Slash commands:** `/goal`, `/goal-contract`, `/goal-review`, `/goal-evidence-map`,
`/goal-status`, `/goal-repair`, `/goal-final`.
Expand Down
72 changes: 34 additions & 38 deletions commands/goal-mode-customize.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,43 @@
---
description: Customize the OpenCode Goal Mode pluginguard config, YOLO mode, allow/deny rules, sidebar, then reinstall.
description: Customize the OpenCode Goal Mode guard — YOLO, allow/deny commands, reviews, sidebar — then reinstall + verify.
agent: goal
---

## What this command does

`/goal-mode-customize` teaches you to reconfigure the Goal Mode guard for this user/project and apply the change correctly. The whole guard is data-driven: every behavior is a key in `DEFAULT_CONFIG` (`plugins/goal-guard/config.js`), settable three ways (later wins): built-in default → `GOAL_GUARD_*` env var → the plugin `options` object in `opencode.json`.

### 1. Find the knob

Read `plugins/goal-guard/config.js` (`DEFAULT_CONFIG`) — it is the single source of truth and each key is documented inline. Common asks:

- **"Stop nagging / just let it run" →** `yolo: true`. Relaxes the soft gates (network-exec block, completion enforcement, the Goal-only subagent lock, block toasts) but KEEPS destructive guarding.
- **"Let it do literally anything (incl. `rm -rf`)" →** `yolo: true, allowDestructive: true` (full YOLO). `allowDestructive` also works on its own.
- **"Allow these specific commands only" →** `allowCommands: ["<js-regex>", …]` — a matching bash command is never blocked, whatever the analyzer thinks.
- **"Also treat these as destructive" →** `extraDestructive: ["<js-regex>", …]`.
- **Sidebar colours / markers / review timing / session caps →** the corresponding `sidebar*`, `completionMarker`/`blockedMarker`, `review*`, `maxSessions`/`sessionTtlMs` keys.

YOLO only relaxes keys the user did NOT set explicitly, so a per-key option always wins over YOLO.

### 2. Apply it

Edit `opencode.json` so the plugin entry carries the options, e.g.:

```jsonc
["opencode-goal-mode", { "yolo": true, "allowCommands": ["^docker compose ", "^rm -rf \\./tmp/"] }]
```

…or export the env equivalent (`GOAL_GUARD_YOLO=1`, `GOAL_GUARD_ALLOW_COMMANDS="^docker compose ,^rm -rf \./tmp/"`). Lists accept an array or a comma/newline-separated string. An invalid regex is ignored, never fatal.

### 3. Reinstall + restart (so the change actually loads)

After editing files in the installed copy or upgrading the package, re-run the installer and clear the TUI cache so the new version loads:

```bash
opencode-goal-mode --global # idempotent reinstall/update; --force to replace files you edited
```

Then fully restart OpenCode. (Global `npm install -g opencode-goal-mode@latest` re-runs this installer automatically via `postinstall`.)

### 4. Verify

Confirm the effective config behaves as intended — e.g. a destructive command is allowed under full YOLO, or your `allowCommands` entry passes while others are still blocked. The guard's block error names the offending command and reason.
`/goal-mode-customize` reconfigures the Goal Mode guard for this user/project. The
guard is fully data-driven: every behavior is a key in `DEFAULT_CONFIG`
(`plugins/goal-guard/config.js`), settable two ways (later wins): a `GOAL_GUARD_*`
env var, or the plugin `options` object in `opencode.json`. A bundled tool,
`scripts/goal-config.mjs`, does discovery, preview, and verification — use it.

### Always follow this loop

1. **Discover the knob**
```bash
node scripts/goal-config.mjs list # all keys: type, default, env var, summary
node scripts/goal-config.mjs explain <key> # one key + the exact opencode.json/env snippet
node scripts/goal-config.mjs recipes # presets; `recipe <name>` prints a paste-ready snippet
```
Map the request: "stop asking / just run" → `yolo: true`; "let it do anything incl. `rm -rf`" → `yolo: true, allowDestructive: true`; "allow this command" → `allowCommands: ["<js-regex>"]`; "also block these" → `extraDestructive: [...]`; reviews/sidebar/markers/timing → the matching `review*`/`sidebar*`/`completionMarker`/etc. keys. YOLO only relaxes keys the user didn't set explicitly — a per-key option always wins.

2. **Apply** — edit the user's `opencode.json` so the plugin entry carries the options:
```jsonc
"plugin": [["opencode-goal-mode", { "yolo": true, "allowCommands": ["^ruff( |$)"] }]]
```
(or export the `GOAL_GUARD_*` env equivalents). Lists accept an array or a comma/newline string; an invalid regex is ignored, never fatal.

3. **Reinstall + restart** so the change loads:
```bash
opencode-goal-mode --global # idempotent; --force replaces files you edited
```
Then fully restart OpenCode. (A global `npm install -g opencode-goal-mode@latest` reinstalls automatically via `postinstall`.)

4. **Verify** before claiming done:
```bash
node scripts/goal-config.mjs effective '{"yolo":true,"allowCommands":["^ruff( |$)"]}' --diff
```
Confirm the resolved config matches the intent (e.g. a destructive command passes under full YOLO; your `allowCommands` entry passes while others stay blocked). The guard's block error names the offending command and reason.

Additional context / the specific customization the user wants:

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "opencode-goal-mode",
"version": "0.6.12",
"version": "0.7.0",
"description": "Strict Goal Mode agents, commands, and guard plugin for OpenCode.",
"type": "module",
"exports": {
Expand All @@ -13,7 +13,8 @@
"packageManager": "npm@11.13.0",
"bin": {
"opencode-goal-mode": "scripts/install.mjs",
"opencode-goal-mode-install": "scripts/install.mjs"
"opencode-goal-mode-install": "scripts/install.mjs",
"opencode-goal-mode-config": "scripts/goal-config.mjs"
},
"files": [
"agents/",
Expand All @@ -25,6 +26,7 @@
"skills/",
"scripts/install.mjs",
"scripts/postinstall.mjs",
"scripts/goal-config.mjs",
"ARCHITECTURE.md",
"CHANGELOG.md",
"LICENSE",
Expand All @@ -46,6 +48,7 @@
"bench:corpus": "node benchmarks/build-external-corpus.mjs",
"bench:truthfulness": "node benchmarks/truthfulness.mjs",
"bench:compare": "node benchmarks/comparison.mjs",
"config": "node scripts/goal-config.mjs",
"pack:check": "npm pack --dry-run",
"lint": "biome lint plugins/ scripts/ benchmarks/ tests/ tools/",
"lint:fix": "biome lint --write plugins/ scripts/ benchmarks/ tests/ tools/",
Expand Down
53 changes: 53 additions & 0 deletions plugins/goal-guard/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,59 @@ export const DEFAULT_CONFIG = Object.freeze({
extraDestructive: [],
});

/**
* Human-facing metadata for every config key — `{ group, summary }`. The default
* and type come from DEFAULT_CONFIG; the env var name is derived (UPPER_SNAKE,
* `GOAL_GUARD_` prefix). This is the single source the customization tool/skill,
* README, and `/goal-mode-customize` all read, so docs never drift from the keys.
* A test asserts it covers exactly the DEFAULT_CONFIG keys.
*/
export const CONFIG_DOCS = Object.freeze({
// Command guard
blockDestructive: { group: "Guard", summary: "Block destructive bash (rm -rf, mkfs, disk writes, …) before it runs." },
blockNetworkExec: { group: "Guard", summary: "Block remote-code-exec pipelines like `curl … | sh`." },
yolo: { group: "Guard", summary: "YOLO: relax the soft gates (network-exec, completion, subagent lock, toasts); destructive stays on. Only relaxes keys you didn't set explicitly." },
allowDestructive: { group: "Guard", summary: "Turn OFF destructive guarding. With `yolo` = full YOLO (nothing blocked). Works standalone too." },
allowCommands: { group: "Guard", summary: "Regex allow-list: a matching bash command is never blocked, whatever the analyzer thinks." },
extraDestructive: { group: "Guard", summary: "Regex deny-list: a matching bash command is treated as destructive (extends the analyzer)." },
// Completion enforcement
enforceCompletion: { group: "Completion", summary: "Rewrite a premature `Goal Completed` claim until the required review gates pass." },
completionMarker: { group: "Completion", summary: "The phrase that, at the start of a message, claims completion." },
blockedMarker: { group: "Completion", summary: "The replacement written when a completion claim is blocked." },
// Auto-continue
autoContinue: { group: "Auto-continue", summary: "Auto-push an idle-but-unfinished goal forward instead of stopping." },
maxAutoContinue: { group: "Auto-continue", summary: "Hard cap on automatic continuations per goal session." },
abortGraceMs: { group: "Auto-continue", summary: "Grace (ms) before an idle goal auto-continues, so a user cancel is honored first." },
// Programmatic reviews
programmaticReview: { group: "Reviews", summary: "Have the guard LAUNCH the required reviewers itself on idle (reviews always run, no manual trigger)." },
reviewTimeoutMs: { group: "Reviews", summary: "Per-reviewer wall-clock cap (ms) for a programmatic review run." },
reviewPollMs: { group: "Reviews", summary: "Poll cadence (ms) while waiting for a reviewer's verdict." },
reviewIdleDeferMs: { group: "Reviews", summary: "Delay (ms) after idle before launching reviewers (lets the host settle)." },
reviewIdleRetryMs: { group: "Reviews", summary: "Backoff (ms) between idle-review retries when the host is still busy." },
maxReviewIdleRetries: { group: "Reviews", summary: "Max automatic idle-review retries before pausing for manual review." },
maxReviewCycles: { group: "Reviews", summary: "Hard cap on programmatic review cycles per goal before pausing for you." },
// Gates & scope
contextualGates: { group: "Gates", summary: "Require specialist reviewer gates derived from goal text / changed files." },
restrictSubagents: { group: "Gates", summary: "Lock the goal-* subagents to the Goal agent (other agents can't call them)." },
// State & lifecycle
injectSystemState: { group: "State", summary: "Inject a live Goal Guard state block into the system prompt." },
persist: { group: "State", summary: "Persist guard state to disk so it survives OpenCode restarts." },
maxSessions: { group: "State", summary: "Max tracked sessions before LRU eviction." },
sessionTtlMs: { group: "State", summary: "Idle TTL (ms) after which a session's state may be dropped (0 disables)." },
// Sidebar / toasts
toastOnBlock: { group: "Notifications", summary: "Emit a TUI toast when something is blocked." },
toastOnReview: { group: "Notifications", summary: "Emit a TUI toast on each review verdict and when completion unlocks." },
sidebarBanner: { group: "Sidebar", summary: "Show the live Goal todo section in the TUI sidebar." },
sidebarColor: { group: "Sidebar", summary: "Foreground colour (hex) of the GOAL label for a running goal." },
sidebarDoneColor: { group: "Sidebar", summary: "Foreground colour (hex) of a completed goal in the sidebar." },
sidebarMutedColor: { group: "Sidebar", summary: "Foreground colour (hex) for pending (□) Goal todo rows." },
});

/** The `GOAL_GUARD_*` environment variable name for a config key. */
export function envVarFor(key) {
return `GOAL_GUARD_${key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase()}`;
}

function coerceBool(value, fallback) {
if (value === undefined || value === null) return fallback;
if (typeof value === "boolean") return value;
Expand Down
Loading
Loading