opencode-turnstile 0.2.1 → 0.2.5
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/README.md +263 -99
- package/agents/debugger.md +3 -2
- package/agents/executor.md +7 -2
- package/bin/cli.mjs +34 -22
- package/package.json +4 -5
- package/scripts/branch-coverage.mjs +206 -0
- package/scripts/check-markers.mjs +23 -1
- package/scripts/install.mjs +113 -13
- package/scripts/status.mjs +333 -0
- package/skills/turnstile-setup/SKILL.md +22 -38
- package/src/config.ts +295 -6
- package/src/evidence.ts +162 -0
- package/src/gates.ts +338 -93
- package/src/journal.ts +10 -3
- package/src/markers.ts +117 -15
- package/src/policy.ts +33 -0
- package/src/setup.ts +1265 -96
- package/src/state.ts +316 -21
- package/src/tool-policy.ts +683 -0
- package/src/types.ts +300 -1
- package/turnstile.ts +150 -14
- package/scripts/install-agents.mjs +0 -504
- package/templates/global-AGENTS.md +0 -55
package/README.md
CHANGED
|
@@ -2,39 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
An [OpenCode](https://opencode.ai) v2 plugin that mechanically enforces an
|
|
4
4
|
agent pipeline: planner → plan-reviewer → executor → test-runner → reviewer.
|
|
5
|
-
The
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
The pipeline policy is injected at runtime by the plugin: when the plugin is
|
|
6
|
+
loaded, it prepends the orchestrator policy to the primary session's first
|
|
7
|
+
task result every task (see [Policy injection](#policy-injection)), so a fresh
|
|
8
|
+
install can never lose it and nothing is written to any instructions file at
|
|
9
|
+
install time. Per-agent instruction lines live in each `agents/<name>.md`. The
|
|
10
|
+
policy steers the agents, and turnstile blocks the dispatches that violate it.
|
|
10
11
|
|
|
11
12
|
The entry point is `turnstile.ts`; behavior lives in focused modules under
|
|
12
|
-
`src/` (one boundary per file: types,
|
|
13
|
-
setup). The plugin has zero runtime
|
|
13
|
+
`src/` (one boundary per file: types, config, policy, journal, state,
|
|
14
|
+
markers, evidence, gates, tool-policy, setup). The plugin has zero runtime
|
|
15
|
+
dependencies and fails open.
|
|
14
16
|
|
|
15
17
|
Verify with:
|
|
16
18
|
|
|
17
19
|
```sh
|
|
18
20
|
npm install
|
|
19
|
-
npm run ci # ci:host (lint, format:check, typecheck, check:markers, test)
|
|
21
|
+
npm run ci # ci:host (lint, format:check, typecheck, check:markers, test with a 100% coverage gate) — host chain only; green on a fresh clone
|
|
22
|
+
npm run ci:installed # install:check — run after `npm run init` on a machine with installed artifacts
|
|
20
23
|
```
|
|
21
24
|
|
|
22
25
|
## What it enforces
|
|
23
26
|
|
|
24
|
-
| Gate | Behavior
|
|
25
|
-
| ---------------------- |
|
|
26
|
-
| Plan-approval gate | `executor` subagent dispatches are blocked until
|
|
27
|
-
| Reviewer dispatch gate | `reviewer` subagent dispatches are blocked until a test-runner result in the session recorded `GATE: PASS
|
|
28
|
-
| Reviewer round cap | Max 2 `reviewer` dispatches per task
|
|
29
|
-
| Decomposition gate | `executor` dispatches must cite `chunk-N` / `AC-n` when any plan in `.opencode/plans/*.md` has a `## Decomposition` block
|
|
30
|
-
| Read horizon | Warns at 8 whole-file `read` calls per session, blocks at 15 (prefer grep/glob)
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
| Gate | Behavior |
|
|
28
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
29
|
+
| Plan-approval gate | `executor` subagent dispatches are blocked until this task's plan-reviewer recorded `VERDICT: APPROVE` (when any plan exists in `.opencode/plans/*.md`), or the task has a human approval — a prompt-channel `PLAN APPROVED:` in the user's prompt, a prompt-channel `USER WAIVER:` waiver, or a permission-prompt reply (`always`/`once`/`reject`); plan-content changes invalidate approvals issued against the previous plans-dir signature (replayed approvals are accepted once, then stamped). See [Waivers and approvals](#waivers-and-approvals) |
|
|
30
|
+
| Reviewer dispatch gate | `reviewer` subagent dispatches are blocked until a test-runner result in the session recorded a `GATE: PASS` _verified_ by the current task's bash/shell executions — a matching exit-0 test command under the default `gateEvidence:enforce`; `flag` flips on trust (still stamped), `off` never stamps — or the user's prompt carries a prompt-channel `USER WAIVER:` waiver (a `USER WAIVER:` in an agent-composed dispatch prompt unlocks nothing; see [Waivers and approvals](#waivers-and-approvals)) |
|
|
31
|
+
| Reviewer round cap | Max 2 `reviewer` dispatches per task, regardless of gate state; a prompt-channel `USER WAIVER:` waiver bypasses the cap and does not count toward it (a `USER WAIVER:` in a dispatch prompt does not; see [Waivers and approvals](#waivers-and-approvals)) |
|
|
32
|
+
| Decomposition gate | `executor` dispatches must cite `chunk-N` / `AC-n` when any plan in `.opencode/plans/*.md` has a `## Decomposition` block |
|
|
33
|
+
| Read horizon | Warns at 8 whole-file `read` calls per session, blocks at 15 (prefer grep/glob) |
|
|
34
|
+
| Tool-call policy gate | `bash`/`shell` commands and `write`/`edit` target paths are matched in `execute.before` against a configurable denylist and blocked with `BLOCKED by turnstile:` under `toolPolicy.mode: enforce`; `shadow` journals the would-block without throwing (adoption path), `off` is a silent no-op. Default `bashDeny` blocks the git-protective operations in [`src/policy.ts`](src/policy.ts) (worktree, branch `switch`/`checkout`, `merge`, `gh pr create or merge`); `pathDeny` defaults empty so ordinary editing is never blocked until configured. `session_move` is journaled as telemetry only, never blocked |
|
|
35
|
+
| Commit-lint | `bash`/`shell` `git commit -m '<subject>'` messages are validated as conventional commits when `gitGates.commitMessages` is on (default): a non-conventional subject throws `BLOCKED by turnstile:` (journaled `reason:"invalid-commit-message"`) under `gitGates.mode: enforce`; `shadow` journals only, `off` is a no-op. An uninspectable form (`-F`/`-t`/`--edit`/interactive; `--amend -m` is inspectable) is blocked under `enforce` (`reason:"commit-message-uninspectable"`) — pass the subject with `git commit -m <subject>` |
|
|
36
|
+
| Landing gate | `git push` without a reviewer `VERDICT: APPROVE` for the current task is `BLOCKED by turnstile:` (journaled `reason:"no-landing-approval"`) under `gitGates.mode: enforce`; `shadow` journals only, `off` is a no-op. Approval comes only from a reviewer delivery `VERDICT: APPROVE` (streamed text is inert); `gh pr create or merge` stays denied by the default `bashDeny` list, and every `git push` is journaled as landing telemetry; a repeat push under `enforce` escalates to a human ask — the repeat push is the decision point |
|
|
37
|
+
| Marker parsing | `GATE:` / `VERDICT:` markers are parsed from task results and child-session text, deduped per task; marker-less outputs from marker-contracted agents (test-runner/reviewer/plan-reviewer) are journaled |
|
|
38
|
+
| Trace | JSONL journal of dispatches, gate transitions, verdicts, blocked calls, bash commands, errors; `setup` heartbeat per session (plugin version, config digest, hook count), stream-death errors, unresolvable-agent dispatch errors |
|
|
39
|
+
| Decoding clamps | executor/test-runner/debugger temperature ≤ 0.2; reviewer and plan-reviewer temperature = 0 |
|
|
40
|
+
| Compaction | Injects a "preserve verbatim" instruction for markers and findings |
|
|
41
|
+
|
|
42
|
+
A new user prompt resets the session's gate state. Replayed gate state
|
|
43
|
+
survives a restart and the next user prompt (the resume prompt), then
|
|
44
|
+
reverts: every later prompt starts a clean gate cycle. Replay reads the
|
|
45
|
+
last 2000 lines of the journal, spanning the current file and its previous
|
|
46
|
+
rotated generation (`GATE: PASS` and plan `VERDICT: APPROVE`
|
|
47
|
+
entries).
|
|
38
48
|
|
|
39
49
|
## How it fits the ecosystem
|
|
40
50
|
|
|
@@ -75,7 +85,8 @@ are visible in this repo's tests:
|
|
|
75
85
|
- Verifiable state. `GATE:` and `VERDICT:` markers are parsed from
|
|
76
86
|
child session text (`src/markers.ts`), stored per parent session
|
|
77
87
|
(`src/state.ts`), and replayed from the JSONL journal on restart, so
|
|
78
|
-
gate state survives
|
|
88
|
+
replayed gate state survives a restart and the next user prompt, then
|
|
89
|
+
every later prompt starts a clean gate cycle rather than living in a
|
|
79
90
|
prompt.
|
|
80
91
|
- Failure posture. Hooks fail open: an internal error is journaled and
|
|
81
92
|
the call proceeds. The only refusals are deliberate
|
|
@@ -101,17 +112,20 @@ ways to use it; pick one, and do not combine the plugin-array path with
|
|
|
101
112
|
the CLI install.
|
|
102
113
|
|
|
103
114
|
1. **Zero-config enforcement (plugin only).** Add the package to your
|
|
104
|
-
OpenCode config's plugin array and restart. turnstile loads and
|
|
105
|
-
|
|
106
|
-
|
|
115
|
+
OpenCode config's plugin array and restart. turnstile loads and enforces
|
|
116
|
+
its gates, injecting the orchestrator policy into the primary session at
|
|
117
|
+
runtime (see [Policy injection](#policy-injection)) — nothing is written to
|
|
118
|
+
any instructions file, and the pipeline agent definitions are not installed
|
|
119
|
+
by this option alone:
|
|
107
120
|
|
|
108
121
|
```json
|
|
109
122
|
{ "plugins": ["opencode-turnstile"] }
|
|
110
123
|
```
|
|
111
124
|
|
|
112
|
-
2. **Full install via the CLI.** `npx opencode-turnstile init` installs
|
|
113
|
-
|
|
114
|
-
block
|
|
125
|
+
2. **Full install via the CLI.** `npx opencode-turnstile init` installs the
|
|
126
|
+
plugin and the pipeline agent definitions; the orchestrator policy is
|
|
127
|
+
injected at runtime by the plugin, so no contract block is written to
|
|
128
|
+
`~/.config/opencode/AGENTS.md` (see
|
|
115
129
|
[From a clone](#from-a-clone) below for the same commands run from
|
|
116
130
|
a clone).
|
|
117
131
|
|
|
@@ -124,9 +138,9 @@ the CLI install.
|
|
|
124
138
|
Turnstile has **no** npm lifecycle hook (`postinstall`, etc.). Installing
|
|
125
139
|
`opencode-turnstile` as a dependency never writes to
|
|
126
140
|
`~/.config/opencode`; it only makes the gates available through the
|
|
127
|
-
plugin array. Everything else (agent definitions
|
|
128
|
-
|
|
129
|
-
|
|
141
|
+
plugin array. Everything else (the agent definitions) happens only when you
|
|
142
|
+
run an explicit command. This is deliberate: writes to your config directory
|
|
143
|
+
are opt-in, not automatic.
|
|
130
144
|
|
|
131
145
|
### Modes
|
|
132
146
|
|
|
@@ -156,16 +170,21 @@ The plugin updates with your dependency manager (`npm upgrade
|
|
|
156
170
|
opencode-turnstile`, or your registry's equivalent). Re-run `check` to see
|
|
157
171
|
whether anything is outdated or drifted, then `init` to re-sync only what
|
|
158
172
|
changed: `npx opencode-turnstile init --copy` for package installs,
|
|
159
|
-
`npm run init -- --copy` from a clone. The
|
|
160
|
-
|
|
173
|
+
`npm run init -- --copy` from a clone. The injected orchestrator policy
|
|
174
|
+
travels with the plugin, so re-syncing the plugin re-supplies it — there is
|
|
175
|
+
no separate contract block to re-sync.
|
|
161
176
|
|
|
162
177
|
### Uninstall
|
|
163
178
|
|
|
164
179
|
`npx opencode-turnstile uninstall` reverses the install: it removes
|
|
165
|
-
ledger-owned copies and repo-owned symlinks and
|
|
166
|
-
|
|
180
|
+
ledger-owned copies and repo-owned symlinks and stops injecting the
|
|
181
|
+
orchestrator policy. It is idempotent and ownership-aware:
|
|
167
182
|
files it never installed (untracked files, symlinks pointing outside the
|
|
168
|
-
repo) are left alone, and a second run is a clean no-op.
|
|
183
|
+
repo) are left alone, and a second run is a clean no-op. Injected policy is
|
|
184
|
+
gone the moment the plugin is unloaded; a legacy contract block once written
|
|
185
|
+
by hand into `~/.config/opencode/AGENTS.md` is not owned by turnstile and is
|
|
186
|
+
removed separately — see
|
|
187
|
+
[Migrating a legacy AGENTS.md block](#migrating-a-legacy-agentsmd-block).
|
|
169
188
|
|
|
170
189
|
### From a clone
|
|
171
190
|
|
|
@@ -295,9 +314,10 @@ Paste the output into `~/.config/opencode/opencode.json` (see the
|
|
|
295
314
|
[Set the agents' models](#set-the-agents-models) section).
|
|
296
315
|
`opencode-turnstile init -- --print-model-config` routes the same flags
|
|
297
316
|
through `scripts/install.mjs` and adds nothing of its own: `install.mjs`
|
|
298
|
-
recognizes `--print-model-config` and prints before writing,
|
|
299
|
-
|
|
300
|
-
|
|
317
|
+
recognizes `--print-model-config` and prints before writing, and the router
|
|
318
|
+
skips the agents install step entirely in that case — so the whole command
|
|
319
|
+
writes nothing (no ledger, no links, no agents, no injected policy). Plain
|
|
320
|
+
`init` still installs everything as usual.
|
|
301
321
|
|
|
302
322
|
### Customizing the pipeline agents
|
|
303
323
|
|
|
@@ -353,69 +373,115 @@ Shared caveats:
|
|
|
353
373
|
- Symlinks point at the clone's absolute path. Do not move, rename, or
|
|
354
374
|
delete the checkout; if you relocate it, re-run `npm run init`.
|
|
355
375
|
- `npm run install:check` reports drift between the manifest and what is
|
|
356
|
-
actually installed
|
|
357
|
-
|
|
358
|
-
|
|
376
|
+
actually installed. There is no `install:agents:check`: the orchestrator
|
|
377
|
+
policy is injected from code (`src/policy.ts`), not a manifest-managed
|
|
378
|
+
template, so nothing to verify there — `npm run ci:installed` is now just
|
|
379
|
+
`install:check`. These install checks must run after `npm run init` on a
|
|
380
|
+
machine with artifacts installed — a fresh clone has nothing to check. The
|
|
381
|
+
host chain (`npm run ci` = `npm run ci:host`) covers lint, format:check,
|
|
382
|
+
typecheck, check:markers, and test without the install checks. GitHub CI
|
|
383
|
+
runs `npm run ci:host`, the host chain only, because a runner has no
|
|
384
|
+
installs to check. Verify a copy install with
|
|
359
385
|
`npm run install:check -- --copy`.
|
|
360
386
|
|
|
361
|
-
###
|
|
362
|
-
|
|
363
|
-
Turnstile
|
|
364
|
-
|
|
387
|
+
### Policy injection
|
|
388
|
+
|
|
389
|
+
Turnstile does not read orchestrator policy from a file at install time —
|
|
390
|
+
it makes that policy available by injecting it at runtime. When the plugin is
|
|
391
|
+
loaded, `src/setup.ts` prepends the orchestrator policy paragraph from
|
|
392
|
+
`src/policy.ts` to the primary session's first `task` (or `subagent`) result
|
|
393
|
+
delivery, once per task, via the `tool.hook("execute.after")` handler — the
|
|
394
|
+
same hook the plugin already uses to rewrite tool output reaching the model.
|
|
395
|
+
The source of truth is code shipped with the plugin, so a fresh install can
|
|
396
|
+
never lose it, and it follows the plugin through updates and
|
|
397
|
+
uninstall–reinstall cycles.
|
|
398
|
+
|
|
399
|
+
- **Fail-open.** The injection is fully guarded with an inner try/catch; on
|
|
400
|
+
failure it journals a `type:"policy"` entry with `injected:false` and
|
|
401
|
+
proceeds — a broken injection never blocks a tool call.
|
|
402
|
+
- **Per-task idempotency.** A `GateState.policyInjected` flag (cleared on
|
|
403
|
+
each new prompt, so the block re-injects per task) means the policy is
|
|
404
|
+
prepended exactly once per task, not once per turn.
|
|
405
|
+
- **Marker placeholders.** The injected paragraph quotes the gate/verdict
|
|
406
|
+
markers with `<n>` placeholders (e.g. `GATE: PASS tests=<n> passed=<n>
|
|
407
|
+
failed=<n>`, `VERDICT: APPROVE crit=<n> high=<n> med=<n> low=<n>`) rather
|
|
408
|
+
than concrete digits, and never the pipe-joined `PASS|FAIL` form. The
|
|
409
|
+
plugin parses markers from _any_ session text, so a fully concrete example
|
|
410
|
+
echoed back by the model could flip a gate; the placeholder form stays
|
|
411
|
+
unparseable while remaining human-readable.
|
|
412
|
+
- **Per-agent instructions** are not injected by the plugin: they live in
|
|
413
|
+
each `agents/<name>.md` installed alongside the plugin.
|
|
414
|
+
|
|
415
|
+
With the plugin **off**, none of this prose is present anywhere — there is no
|
|
416
|
+
AGENTS.md block to fall back on. That is the deliberate tradeoff: loss-proof
|
|
417
|
+
by construction while the plugin loads, and a known, documented gap when it
|
|
418
|
+
does not.
|
|
419
|
+
|
|
420
|
+
### Migrating a legacy AGENTS.md block
|
|
421
|
+
|
|
422
|
+
If a previous turnstile install once wrote a managed contract block into
|
|
423
|
+
`~/.config/opencode/AGENTS.md`, that block is now orphaned: the plugin no
|
|
424
|
+
longer reads or manages it, and it will coexist with the injected policy
|
|
425
|
+
(duplicate prose until removed). There is deliberately no installer to remove
|
|
426
|
+
it — turnstile no longer owns that file, so it never rewrites it. Delete the
|
|
427
|
+
sentinel-delimited block by hand, once:
|
|
365
428
|
|
|
366
429
|
```sh
|
|
367
|
-
|
|
430
|
+
# ~/.config/opencode/AGENTS.md — delete the section between the
|
|
431
|
+
# `<!-- turnstile:start -->` and `<!-- turnstile:end -->` sentinels, then save
|
|
368
432
|
```
|
|
369
433
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
`<!-- turnstile:end -->` sentinels; re-running updates only that block
|
|
373
|
-
and preserves all surrounding personal content.
|
|
374
|
-
- Legacy targets are adopted automatically. If the file already contains
|
|
375
|
-
pipeline-gates content _without_ sentinels (e.g. an install from before
|
|
376
|
-
the block was managed), the installer classifies the existing section:
|
|
377
|
-
equivalent to the template, it wraps the section in sentinels without
|
|
378
|
-
changing its content; differing from the template, it backs the file up
|
|
379
|
-
to `<name>.bak.<timestamp>` and replaces the section with the current
|
|
380
|
-
block. Pass `--no-adopt` to refuse both and print reconciliation
|
|
381
|
-
instructions instead.
|
|
382
|
-
- The installer refuses (exit 1, no write) only when a target is
|
|
383
|
-
ambiguous (more than one turnstile section) or corrupted (a start
|
|
384
|
-
sentinel without an end sentinel); reconcile those by hand.
|
|
385
|
-
- `--check` reports the outcome without writing: OK, or the adoption
|
|
386
|
-
decision (would-adopt-equivalent / would-replace / drift), exiting 0
|
|
387
|
-
only for OK. Useful in CI or dotfiles setup.
|
|
388
|
-
- Agent definitions and the plugin itself are installed separately via
|
|
389
|
-
`npm run init` (symlinks, for development) or
|
|
390
|
-
`npm run init -- --copy` (see the Install section above).
|
|
391
|
-
|
|
392
|
-
Alternatively, in an OpenCode session, invoke the bundled
|
|
393
|
-
`turnstile-setup` skill, which runs the installer and verifies marker
|
|
394
|
-
sync. The skill ships in the package and installs to
|
|
395
|
-
`~/.config/opencode/skills/turnstile-setup/SKILL.md`; the template
|
|
396
|
-
source lives at `templates/global-AGENTS.md`.
|
|
434
|
+
After that, the injected policy is the single source of orchestrator policy
|
|
435
|
+
for the session.
|
|
397
436
|
|
|
398
437
|
## Configuration
|
|
399
438
|
|
|
400
439
|
Defaults live in code (`src/config.ts`); override via
|
|
401
440
|
`~/.config/opencode/turnstile.json`, environment variables, or both.
|
|
441
|
+
|
|
442
|
+
**Quick start: copy the example config** — [`examples/turnstile.example.json`](examples/turnstile.example.json)
|
|
443
|
+
to `~/.config/opencode/turnstile.json` and edit from there. Every key in it
|
|
444
|
+
is optional and matches the code defaults; delete any key to fall back to the
|
|
445
|
+
default. One key is deliberately omitted from the example:
|
|
446
|
+
|
|
447
|
+
- `metricsDir`: computed at load time (`$XDG_DATA_HOME/opencode/metrics`,
|
|
448
|
+
fallback `~/.local/share/opencode/metrics`); if you set it yourself it must
|
|
449
|
+
be an absolute path (`~` is not expanded)
|
|
450
|
+
|
|
451
|
+
The example also carries `gateMarker` / `verdictMarker` verbatim so you can
|
|
452
|
+
see the format; leave them alone unless you are intentionally changing
|
|
453
|
+
the marker syntax (the shipped subagent definitions emit the canonical
|
|
454
|
+
markers).
|
|
455
|
+
|
|
402
456
|
Precedence: defaults < config file < env. The plugin reads config once, at
|
|
403
457
|
setup; restart sessions to apply. Invalid keys are ignored and journaled
|
|
404
458
|
(fail-open); a malformed config file never breaks the host.
|
|
405
459
|
|
|
406
|
-
| Key | Default
|
|
407
|
-
| ------------------------------------------------------------------------------------------------------- |
|
|
408
|
-
| `metricsDir` | `$XDG_DATA_HOME/opencode/metrics`, fallback `~/.local/share/opencode/metrics`
|
|
409
|
-
| `journalMaxBytes` | 5242880
|
|
410
|
-
| `journalMaxGenerations` | 5
|
|
411
|
-
| `maxReviewerRounds` | 2
|
|
412
|
-
| `
|
|
413
|
-
| `
|
|
414
|
-
| `
|
|
415
|
-
| `
|
|
416
|
-
| `
|
|
417
|
-
| `
|
|
418
|
-
| `
|
|
460
|
+
| Key | Default | Meaning |
|
|
461
|
+
| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
462
|
+
| `metricsDir` | `$XDG_DATA_HOME/opencode/metrics`, fallback `~/.local/share/opencode/metrics` | directory for both telemetry files |
|
|
463
|
+
| `journalMaxBytes` | 5242880 | rotate a journal file past this size |
|
|
464
|
+
| `journalMaxGenerations` | 5 | rotated generations kept per journal |
|
|
465
|
+
| `maxReviewerRounds` | 2 | reviewer dispatches per task, regardless of gate state (waived dispatches exempt) |
|
|
466
|
+
| `maxSessions` | 64 | LRU cap on tracked sessions (gate state, child→parent edges, verdicts); registered live parents are never evicted |
|
|
467
|
+
| `readWarn` / `readBlock` | 8 / 15 | whole-file read horizon |
|
|
468
|
+
| `waiverMarker` | `USER WAIVER:` | literal waiver marker |
|
|
469
|
+
| `approvalMarker` | `PLAN APPROVED:` | literal prompt-channel plan-approval marker |
|
|
470
|
+
| `plansDir` | `.opencode/plans` | executor decomposition scan directory |
|
|
471
|
+
| `trace` | `true` | write the human-readable trace file (JSONL journal always writes) |
|
|
472
|
+
| `agents.reviewer` / `agents.planReviewer` / `agents.executor` / `agents.testRunner` / `agents.debugger` | `reviewer` / `plan-reviewer` / `executor` / `test-runner` / `debugger` | agent names recognized in dispatches |
|
|
473
|
+
| `agents.bareOutput` | `["test-runner", "reviewer", "plan-reviewer"]` | agents whose marker-less output is a violation |
|
|
474
|
+
| `gateMarker` / `verdictMarker` | canonical regex sources | marker patterns, compiled with `gm` |
|
|
475
|
+
| `gateEvidence` | `enforce` | `enforce` requires a correlating exit-0 test command to flip the gate; `flag` flips on trust (stamps `verified`); `off` journals without it |
|
|
476
|
+
| `evidencePattern` | a npm/pnpm/yarn/bun/deno/node `--test`/pytest/`make test`/go/cargo pattern | regex matched (case-insensitively) against bash/shell commands; user-overridable for non-JS toolchains (e.g. Ruby/`rspec`) |
|
|
477
|
+
| `toolPolicy.mode` | `enforce` | `enforce` blocks matching commands/paths; `shadow` journals would-blocks without throwing (adoption path); `off` is a silent no-op |
|
|
478
|
+
| `toolPolicy.bashDeny` | the git-protective defaults (worktree, branch switch/checkout, merge, `gh pr`) | case-insensitive regex sources matched against `bash`/`shell` commands; an invalid source is dropped and journaled |
|
|
479
|
+
| `toolPolicy.pathDeny` | `[]` (empty) | case-insensitive regex sources matched against `write`/`edit` target paths; a dropped source is journaled |
|
|
480
|
+
| `toolPolicy.observeSessionMove` | `true` | journal `session_move` dispatches as telemetry (never blocks) |
|
|
481
|
+
| `gitGates.mode` | `shadow` | `shadow` would-block without throwing; `enforce` blocks invalid commits and unapproved pushes (opt-in), `off` is a silent no-op |
|
|
482
|
+
| `gitGates.commitMessages` | `true` | validate `git commit -m` subjects as conventional commits; `shadow` journals, `enforce` blocks, `off` is a no-op |
|
|
483
|
+
| `gitGates.commitFormat` | a conventional-commits regex source | conventional-commits regex source (compiled `^...$`) matched against each subject; a bad source is dropped and journaled |
|
|
484
|
+
| `gitGates.landingGate` | `true` | gate `git push` on this task reviewer `VERDICT: APPROVE`; `shadow` journals, `enforce` blocks then escalates, `off` is a no-op |
|
|
419
485
|
|
|
420
486
|
Environment variables (override the file):
|
|
421
487
|
|
|
@@ -426,7 +492,7 @@ Environment variables (override the file):
|
|
|
426
492
|
## Markers
|
|
427
493
|
|
|
428
494
|
Subagents must emit these as the first line of their output. These examples
|
|
429
|
-
are the canonical syntax; CI asserts the parser in `
|
|
495
|
+
are the canonical syntax; CI asserts the parser in `src/markers.ts` accepts
|
|
430
496
|
exactly them.
|
|
431
497
|
|
|
432
498
|
```
|
|
@@ -437,44 +503,142 @@ VERDICT: APPROVE crit=0 high=0 med=2 low=1
|
|
|
437
503
|
VERDICT: BLOCK crit=1 high=0 med=0 low=2
|
|
438
504
|
```
|
|
439
505
|
|
|
440
|
-
Waiver
|
|
506
|
+
Waiver and approval markers (must appear in the user's prompt, verbatim — a dispatch prompt that merely contains the marker unlocks nothing; see [Waivers and approvals](#waivers-and-approvals)):
|
|
441
507
|
|
|
442
508
|
```
|
|
443
509
|
USER WAIVER: <the user's words>
|
|
510
|
+
PLAN APPROVED: <the user's words>
|
|
444
511
|
```
|
|
445
512
|
|
|
513
|
+
## Waivers and approvals
|
|
514
|
+
|
|
515
|
+
Turnstile reads a waiver or a human plan approval only from the user's OWN
|
|
516
|
+
prompt — the plugin observes it at prompt admission, never from an agent's
|
|
517
|
+
dispatch prompt. This is the core of issue #34: a marker inside an
|
|
518
|
+
agent-composed prompt must never unlock a gate.
|
|
519
|
+
|
|
520
|
+
- **Prompt-channel waiver.** Put the literal marker `USER WAIVER:` in the text
|
|
521
|
+
of your prompt to waive the plan gate and the reviewer gate for the task.
|
|
522
|
+
The waiver is provenance-checked: it must come from the user prompt, and a
|
|
523
|
+
dispatch prompt that merely contains `USER WAIVER:` unlocks nothing.
|
|
524
|
+
- **Prompt-channel approval.** Put `PLAN APPROVED:` in your prompt to approve
|
|
525
|
+
the plan the same way a plan-reviewer `VERDICT: APPROVE` would; it sets the
|
|
526
|
+
plan verdict and records human provenance, and replays across restart
|
|
527
|
+
exactly like a reviewer verdict.
|
|
528
|
+
- **Permission-prompt escalation.** When no approval holds, the first
|
|
529
|
+
executor dispatch escalates the plan gate to the human through OpenCode's
|
|
530
|
+
permission surface: `permission.evaluate` forces an `ask` that names the
|
|
531
|
+
plan file and its head. The human replies — `always` approves for the rest
|
|
532
|
+
of the task, `once` grants a single dispatch, and `reject` keeps the gate
|
|
533
|
+
red (that single reply is consumed).
|
|
534
|
+
|
|
535
|
+
Do not create, paste, or relay a `USER WAIVER:` / `PLAN APPROVED:` marker to
|
|
536
|
+
open a gate — only the user's own prompt carries a real waiver or approval.
|
|
537
|
+
|
|
446
538
|
## What turnstile is not
|
|
447
539
|
|
|
448
540
|
Turnstile is a speed bump and an audit trail, not a security boundary.
|
|
449
541
|
|
|
450
|
-
- Any text matching the gate marker
|
|
451
|
-
hallucinated or copied from documentation
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
-
|
|
455
|
-
|
|
456
|
-
|
|
542
|
+
- Any text matching the gate marker — including a marker an agent
|
|
543
|
+
hallucinated or copied from documentation — is journaled `verified:false`
|
|
544
|
+
under the default `gateEvidence:enforce` mode and does not flip
|
|
545
|
+
`hasGatePass` unless the current task's bash/shell executions contain a
|
|
546
|
+
matching exit-0 test command (plausibility checked against
|
|
547
|
+
`evidencePattern`). An agent that wants a green gate can no longer emit
|
|
548
|
+
`GATE: PASS tests=1 passed=1 failed=0` without running anything — but
|
|
549
|
+
`gateEvidence:flag` reverts to the historical flip-on-trust semantics
|
|
550
|
+
(still stamped `verified:false`), and `gateEvidence:off` journals without
|
|
551
|
+
the field at all.
|
|
552
|
+
- A `GATE: PASS` is only as trustworthy as the journal: an attacker who can
|
|
553
|
+
write or replay the metrics directory can craft a `verified:true` entry,
|
|
554
|
+
and `flag`/`off` make a fabricated marker trust-lossless. Tests that run
|
|
555
|
+
outside the observable bash/shell surface (external CI, a differently
|
|
556
|
+
named execution tool) leave enforce mode with no evidence to verify
|
|
557
|
+
against — operators in that situation use `flag`/`off` at the cost of the
|
|
558
|
+
guarantee. The journal records what was _claimed_, never proof.
|
|
559
|
+
- The dispatch-prompt waiver-forgery vector is closed: a `USER WAIVER:` (or
|
|
560
|
+
`PLAN APPROVED:`) substring inside an agent-composed dispatch prompt
|
|
561
|
+
unlocks nothing — turnstile reads waivers and approvals only from the
|
|
562
|
+
user's prompt at admission. The residual trust stance is unchanged: the
|
|
563
|
+
journal is still the record of what was _claimed_ (an attacker who can
|
|
564
|
+
write or replay the metrics directory can still craft entries), so the
|
|
565
|
+
journal is treated as sensitive data (see SECURITY.md).
|
|
457
566
|
- `scripts/check-markers.mjs` guards against accidental drift between the
|
|
458
567
|
parser and documented markers; it cannot guard against forgery, because
|
|
459
568
|
the markers are plain prose by design.
|
|
569
|
+
- The `bash`/`shell`/`write`/`edit` denylist is a deterrent, not a sandbox: it
|
|
570
|
+
matches strings, so `bash -c "$X"`, script files, `eval` indirection, and
|
|
571
|
+
values sourced from the environment still bypass it. A block names the matched
|
|
572
|
+
_rule_ (never the raw command, per the redaction contract); a dropped or
|
|
573
|
+
invalid regex _source_ means the operation it guarded is now **allowed**
|
|
574
|
+
(documented so a config typo weakens the policy visibly in the journal rather
|
|
575
|
+
than silently). The git rules are global-option tolerant, so `git -C dir
|
|
576
|
+
checkout main` is caught as well as `git checkout main`, and the checkout guard
|
|
577
|
+
allows the file-restore form `git [global options] checkout [<ref>] -- <path>`
|
|
578
|
+
while blocking `git [global options] checkout <branch>` and `-b`/`-C`; but two
|
|
579
|
+
gaps remain by design. `git` **aliases** (e.g. `git co main`, `git cm -m x`)
|
|
580
|
+
are not matched, because a rule names `git` plus a literal subcommand only; and
|
|
581
|
+
the wider base can _over-match_ — e.g. `git commit -m "… merge …"` trips the
|
|
582
|
+
`merge` rule. `session_move` never blocks.
|
|
583
|
+
- MCP tools and other tool names not named in `execute.before` (`task`,
|
|
584
|
+
`subagent`, `read`, `bash`, `shell`, `write`, `edit`, `session_move`) remain
|
|
585
|
+
unguarded. Governing them is a follow-on, not part of this policy layer.
|
|
460
586
|
|
|
461
587
|
Use OpenCode's permission and sandboxing mechanisms against hostile agents.
|
|
462
588
|
Treat turnstile's journal as the record of what was _claimed_, and treat the
|
|
463
589
|
journal itself as sensitive data (see SECURITY.md).
|
|
464
590
|
|
|
591
|
+
### Adopting the tool-call policy (shadow first)
|
|
592
|
+
|
|
593
|
+
The default `toolPolicy.mode: enforce` blocks the git-protective operations the
|
|
594
|
+
policy already demands, so it is safe to adopt immediately. To tune it with zero
|
|
595
|
+
risk of breaking work, boot one session in `toolPolicy.mode: shadow` first:
|
|
596
|
+
would-block commands and paths are journaled with `"reason":"tool-policy"` and
|
|
597
|
+
`mode:"shadow"` but never throw. Grep the journal for that reason, review the
|
|
598
|
+
rules that fired, adjust `toolPolicy.bashDeny`/`pathDeny` (each rule is
|
|
599
|
+
case-insensitive), then flip to `enforce`. Restart sessions to apply config —
|
|
600
|
+
it is read once at setup.
|
|
601
|
+
The same shadow-first adoption applies to `gitGates` (default `mode: shadow`):
|
|
602
|
+
under `shadow` an invalid commit message or an unapproved `git push` is journaled
|
|
603
|
+
(`"reason":"invalid-commit-message"` / `"reason":"no-landing-approval"`) without
|
|
604
|
+
throwing, so boot one session in `shadow`, review the journal, then flip to
|
|
605
|
+
`enforce` when you are ready to start blocking.
|
|
606
|
+
|
|
465
607
|
## Files and journals
|
|
466
608
|
|
|
467
609
|
- Journals live outside the repo in `~/.local/share/opencode/metrics/`:
|
|
468
610
|
- `turnstile.jsonl`: full JSONL trace; rotates at 5 MB, keeps 5
|
|
469
611
|
generations
|
|
470
612
|
- `gate-events.jsonl`: human-readable
|
|
471
|
-
`YYYY-MM-DD HH:MM | agent | event | detail` lines
|
|
472
|
-
|
|
613
|
+
`YYYY-MM-DD HH:MM | agent | event | detail` lines, with an optional
|
|
614
|
+
trailing ` | <sessionID>` segment when the trace line is wired to a
|
|
615
|
+
session (the first four segments are unchanged when it is absent);
|
|
616
|
+
suppressed entirely by `trace: false`
|
|
473
617
|
- Retention is bounded: 2 files × (1 current + 5 generations) × 5 MB ≈
|
|
474
618
|
~30 MB per journal, worst case ~60 MB total.
|
|
475
619
|
- `.opencode/plans/*.md` containing `## Decomposition` are load-bearing:
|
|
476
620
|
their presence activates the executor chunk-citation gate.
|
|
477
621
|
|
|
622
|
+
## Status
|
|
623
|
+
|
|
624
|
+
`opencode-turnstile status` reports turnstile gate and plan state per parent
|
|
625
|
+
session, read from the journal, as a human-readable table by default or JSON
|
|
626
|
+
with `--json`:
|
|
627
|
+
|
|
628
|
+
```
|
|
629
|
+
opencode-turnstile status
|
|
630
|
+
opencode-turnstile status --json
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
It is read-only: it never writes to the journal or config. Per session it
|
|
634
|
+
replays the same gate/plan last-wins rules the runtime uses (a trailing
|
|
635
|
+
`GATE: FAIL` keeps the gate red; an enforce-mode `verified:false` PASS never
|
|
636
|
+
flips), plus last markers, waiver events (with channel), and dispatch/block/
|
|
637
|
+
permission counts. The journal reflects the last prompt-cycle; in-memory state
|
|
638
|
+
can be fresher (a session that has prompted since is newer than the journal
|
|
639
|
+
records), so treat status as the durable state of the previous cycle rather
|
|
640
|
+
than a live snapshot.
|
|
641
|
+
|
|
478
642
|
## Behavior contract
|
|
479
643
|
|
|
480
644
|
- Fail-open by design. Every hook body is guarded; internal errors are
|
package/agents/debugger.md
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
description: Investigates failures, identifies root causes, implements targeted fixes, and verifies the result.
|
|
3
3
|
mode: subagent
|
|
4
4
|
# Runaway-loop backstop for the Horizon rule below: near the cap OpenCode
|
|
5
|
-
# drops tools and asks for a summary.
|
|
6
|
-
|
|
5
|
+
# drops tools and asks for a summary. Tuned 2026-10-14: 150 capped once on the
|
|
6
|
+
# #34/#35 fix rounds; 200 fits a diagnose-fix-verify loop with full CI.
|
|
7
|
+
steps: 200
|
|
7
8
|
---
|
|
8
9
|
|
|
9
10
|
You are a debugging specialist.
|
package/agents/executor.md
CHANGED
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
description: Implements an approved plan, validates changes, and fixes test failures
|
|
3
3
|
mode: subagent
|
|
4
4
|
# Runaway-loop backstop for the Horizon rule below: near the cap OpenCode
|
|
5
|
-
# drops tools and asks for a summary.
|
|
6
|
-
|
|
5
|
+
# drops tools and asks for a summary. Tuned 2026-10-12: 50 repeatedly cut off
|
|
6
|
+
# executor-sized chunks mid-implementation (issue #51 needed 3 continuations);
|
|
7
|
+
# 150 keeps the backstop while fitting a real chunk (tests + docs + CI + commit).
|
|
8
|
+
# Tuned 2026-10-14: 150 still capped mid-chunk three times on the #34/#35 work
|
|
9
|
+
# (coverage hunts + full CI loops are the cost driver); 200 fits a chunk
|
|
10
|
+
# including one coverage-debug iteration.
|
|
11
|
+
steps: 200
|
|
7
12
|
---
|
|
8
13
|
|
|
9
14
|
You are the implementation agent.
|