opencode-turnstile 0.0.0-stage → 0.1.0

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 CHANGED
@@ -1,3 +1,413 @@
1
- # Temporary Holding Version
1
+ # turnstile
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ An [OpenCode](https://opencode.ai) v2 plugin that mechanically enforces an
4
+ agent pipeline: planner → plan-reviewer → executor → test-runner → reviewer.
5
+ The workflow
6
+ itself lives in a policy instruction file (e.g. `AGENTS.md`) that tells the
7
+ agents what the pipeline is; turnstile complements that file rather than
8
+ replacing it. The policy steers the agents, and turnstile blocks the
9
+ dispatches that violate it.
10
+
11
+ The entry point is `turnstile.ts`; behavior lives in focused modules under
12
+ `src/` (one boundary per file: types, journal, state, markers, gates,
13
+ setup). The plugin has zero runtime dependencies and fails open.
14
+
15
+ Verify with:
16
+
17
+ ```sh
18
+ npm install
19
+ npm run ci # ci:host (lint, format:check, typecheck, check:markers, test) + install:check
20
+ ```
21
+
22
+ ## What it enforces
23
+
24
+ | Gate | Behavior |
25
+ | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
26
+ | Plan-approval gate | `executor` subagent dispatches are blocked until a plan-reviewer result in the session recorded `VERDICT: APPROVE` (when any plan exists in `.opencode/plans/*.md`), or the dispatch prompt contains the literal marker `USER WAIVER:`; plan-file changes invalidate the approval |
27
+ | Reviewer dispatch gate | `reviewer` subagent dispatches are blocked until a test-runner result in the session recorded `GATE: PASS`, or the dispatch prompt contains the literal marker `USER WAIVER:` |
28
+ | Reviewer round cap | Max 2 `reviewer` dispatches per task without a `GATE: PASS` |
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
+ | 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 |
32
+ | Trace | JSONL journal of dispatches, gate transitions, verdicts, blocked calls, bash commands, errors |
33
+ | Decoding clamps | executor/test-runner/debugger temperature ≤ 0.2; reviewer and plan-reviewer temperature = 0 |
34
+ | Compaction | Injects a "preserve verbatim" instruction for markers and findings |
35
+
36
+ A new user prompt resets the session's gate state. Gate state survives
37
+ restarts via journal replay (last 2000 lines of `GATE: PASS` entries).
38
+
39
+ ## How it fits the ecosystem
40
+
41
+ Claude Code's agent workflows are backed by host-side governance:
42
+ permission prompts and tool hooks that can stop a call before it
43
+ happens. I built turnstile to bring that shape to OpenCode: gates the
44
+ runtime enforces, not requests the model honors. It borrows the
45
+ enforcement model without the implementation, and stays
46
+ OpenCode-native: zero dependencies, TypeScript, loaded from your own
47
+ checkout.
48
+
49
+ Most other agent-structure projects on OpenCode are orchestration
50
+ suites: an orchestrator plans work, dispatches specialists, and
51
+ reconciles results. Some add pipeline discipline on top, but the
52
+ discipline is carried by prompts: a phase header the model prints at
53
+ the start of a response, a "review gate" step inside a skill's
54
+ instructions, a warning when a write lands without a prior test run.
55
+ Those asks work when the model complies. Turnstile's gates aim a step
56
+ further: they are decided in tool and task-dispatch hooks, outside the
57
+ model loop, so a dispatch that violates a gate fails before a subagent
58
+ session is created.
59
+
60
+ Turnstile is a layer under orchestration suites, not a competitor: it
61
+ reads task dispatches to attribute markers and enforce gates; it never
62
+ plans work, routes models, or spawns specialists. An orchestration
63
+ suite that dispatches task calls with agent names will pass through
64
+ turnstile unchanged unless a gate applies. Other adjacent approaches
65
+ gate on written artifacts, like spec workflows, or on a single
66
+ human-approval point; turnstile gates the machine steps around those.
67
+ The bundled planner/executor/test-runner/reviewer agents in
68
+ `.opencode/agents/` are the reference pipeline, not a requirement; the
69
+ gates key off agent names and markers, and other suites' agents can
70
+ carry their own markers.
71
+
72
+ Three properties matter for comparing enforcement layers, and all three
73
+ are visible in this repo's tests:
74
+
75
+ - Verifiable state. `GATE:` and `VERDICT:` markers are parsed from
76
+ child session text (`src/markers.ts`), stored per parent session
77
+ (`src/state.ts`), and replayed from the JSONL journal on restart, so
78
+ gate state survives sessions and reloads rather than living in a
79
+ prompt.
80
+ - Failure posture. Hooks fail open: an internal error is journaled and
81
+ the call proceeds. The only refusals are deliberate
82
+ `BLOCKED by turnstile:` throws at a declared gate. A broken
83
+ enforcement plugin should degrade to prompt contracts, not break
84
+ every session.
85
+ - Auditability. Every dispatch, gate transition, verdict, and blocked
86
+ call is appended to `turnstile.jsonl` (bounded rotation, see
87
+ SECURITY.md). The journal is the record of what was claimed, which is
88
+ what makes the gates reviewable after the fact.
89
+
90
+ The tradeoff is scope: turnstile is host-coupled to the OpenCode v2
91
+ plugin API, verified against v2.0.22, and enforces exactly the
92
+ pipeline above. Multi-harness suites reach further; turnstile goes
93
+ deeper on one host and one pipeline.
94
+
95
+ ## Install
96
+
97
+ Turnstile ships as the npm package `opencode-turnstile`. There are three
98
+ ways to use it; pick one, and do not combine the plugin-array path with
99
+ the CLI install.
100
+
101
+ 1. **Zero-config enforcement (plugin only).** Add the package to your
102
+ OpenCode config's plugin array and restart. turnstile loads and
103
+ enforces its gates, but neither the pipeline agent definitions nor the
104
+ AGENTS.md contract block are installed:
105
+
106
+ ```json
107
+ { "plugins": ["opencode-turnstile"] }
108
+ ```
109
+
110
+ 2. **Full install via the CLI.** `npx opencode-turnstile init` installs
111
+ the plugin, the pipeline agent definitions, and the managed contract
112
+ block in `~/.config/opencode/AGENTS.md` (see
113
+ [From a clone](#from-a-clone) below for the same commands run from
114
+ a clone).
115
+
116
+ 3. **Develop from a clone.** Clone this repo and run `npm run init`
117
+ (symlink mode) so installed artifacts stay live against your edits.
118
+ See [From a clone](#from-a-clone) below.
119
+
120
+ ### No postinstall
121
+
122
+ Turnstile has **no** npm lifecycle hook (`postinstall`, etc.). Installing
123
+ `opencode-turnstile` as a dependency never writes to
124
+ `~/.config/opencode`; it only makes the gates available through the
125
+ plugin array. Everything else (agent definitions, the AGENTS.md contract
126
+ block) happens only when you run an explicit command. This is deliberate:
127
+ writes to your config directory are opt-in, not automatic.
128
+
129
+ ### Modes
130
+
131
+ `init` auto-detects an install mode: a git checkout installs by
132
+ linking (the working tree stays the source of truth, so edits are
133
+ live); an `npx` package install copies files (the installed files are
134
+ the source of truth). Override with `--link` or `--copy`:
135
+
136
+ ```sh
137
+ npx opencode-turnstile init --copy
138
+ ```
139
+
140
+ The chosen mode is recorded in the ledger, so `check` stays consistent
141
+ across reruns.
142
+
143
+ ### Check
144
+
145
+ `npx opencode-turnstile check` verifies the installed state against the
146
+ manifest and exits 0 (clean) or 1 (drift). A copy install whose recorded
147
+ version is older than the published package is reported as a warning
148
+ (`check: outdated <dest> (installed X, package Y)`) and does not fail the
149
+ check.
150
+
151
+ ### Upgrade
152
+
153
+ The plugin updates with your dependency manager (`npm upgrade
154
+ opencode-turnstile`, or your registry's equivalent). Re-run `check` to see
155
+ whether anything is outdated or drifted, then `init` to re-sync only what
156
+ changed: `npx opencode-turnstile init --copy` for package installs,
157
+ `npm run init -- --copy` from a clone. The AGENTS.md contract block is
158
+ re-synced the same way, keeping only the managed block in step.
159
+
160
+ ### Uninstall
161
+
162
+ `npx opencode-turnstile uninstall` reverses the install: it removes
163
+ ledger-owned copies and repo-owned symlinks and unmerges the managed
164
+ contract block from AGENTS.md. It is idempotent and ownership-aware:
165
+ files it never installed (untracked files, symlinks pointing outside the
166
+ repo) are left alone, and a second run is a clean no-op.
167
+
168
+ ### From a clone
169
+
170
+ This repo is the source of truth for what lands in `~/.config/opencode`:
171
+ the plugin and the pipeline agent definitions are installed per the
172
+ manifest in `install.json`; unowned files in the target directories are
173
+ never touched. Two install modes are available:
174
+
175
+ | | Symlink (default) | Copy (`--copy`) |
176
+ | ------------------------- | --------------------------------------------------- | ------------------------------------------------------- |
177
+ | Best for | developing turnstile | general use, dotfiles repos that reject foreign links |
178
+ | How targets point at repo | symlinks into the checkout | real copies |
179
+ | After `git pull` | nothing to do; edits are live on next session start | re-run `npm run init -- --copy` to re-sync |
180
+ | Editing installed files | edit the repo; links share it | edit the repo; user-edited copies conflict on next sync |
181
+
182
+ ```sh
183
+ git clone <repo-url> && cd turnstile
184
+ npm install # typescript + @types/node, for typechecking
185
+ npm run init # symlink mode
186
+ # or: npm run install:copy
187
+ npm run install:check # verify installed state; exit 1 on drift (gates local CI)
188
+ ```
189
+
190
+ A target that differs from the repo is a **conflict**: `install` fails with
191
+ a per-path summary. Re-run with `--force` to back the file up
192
+ (`<name>.bak.<timestamp>`) and replace it. Conflicts mean:
193
+
194
+ - symlink mode: a real file (or a link pointing outside the repo) sits at a
195
+ manifest target and differs from the source, typically pre-existing
196
+ files or hand edits.
197
+ - copy mode: an installed copy was edited after install (the ledger can
198
+ tell user edits from repo updates), or an untracked real file sits at a
199
+ target.
200
+
201
+ Copy mode records everything it installed in
202
+ `~/.config/opencode/.turnstile-install.json` (the ledger: destination
203
+ mapped to source, content hash, package version, and install mode). The
204
+ ledger makes re-syncs safe: repo
205
+ updates re-copy cleanly and user edits fail loudly. It also lets the
206
+ installer remove copies whose manifest entry was deleted. Deleting the
207
+ ledger turns every copy into an untracked file the installer will refuse
208
+ to touch.
209
+
210
+ ### Set the agents' models
211
+
212
+ The six agent definitions in `.opencode/agents/` ship with `model:`
213
+ frontmatter pointing at the author's locally served models (`oMLX/...`).
214
+ You almost certainly do not have those models, and dispatches to the
215
+ pipeline agents will fail or fall back until you set your own.
216
+
217
+ Since installs share the repo's files (symlink mode) or conflict on
218
+ divergence (copy mode), set your models **in the clone**, not in
219
+ `~/.config/opencode/agents/`: edit the `model:` line of each agent file
220
+ (`planner`, `plan-reviewer`, `executor`, `test-runner`, `reviewer`,
221
+ `debugger`) to a model
222
+ you have, in `providerID/modelID` form, then install or re-sync per the
223
+ mode table above. Model overrides in installed copies are lost on the next
224
+ sync; the repo is the source of truth.
225
+
226
+ ### Customizing the pipeline agents
227
+
228
+ The agent definitions are opinionated on purpose: turnstile recognizes
229
+ and clamps them by name, and the marker contract in the agent prompts is
230
+ what the plugin enforces against. Editing the installed files is therefore
231
+ not a supported customization path: symlink installs share the repo file,
232
+ and copy installs conflict on the next sync.
233
+
234
+ Two sanctioned ways to change the pipeline:
235
+
236
+ - **Bring your own agents.** Add your own agent definitions (any names) to
237
+ `~/.config/opencode/agents/`; the installer never touches files it does
238
+ not own. Then point turnstile at them via
239
+ `~/.config/opencode/turnstile.json`: `agents.planReviewer`,
240
+ `agents.reviewer`, `agents.executor`, `agents.testRunner`, and
241
+ `agents.debugger` take the names turnstile should recognize, clamp, and
242
+ hold to the marker contract. Your agents get the same gates the shipped
243
+ ones do.
244
+ - **Fork the repo.** To change the pipeline content itself, fork and edit
245
+ `.opencode/agents/*.md` there; your installer serves your fork.
246
+
247
+ ### Updating
248
+
249
+ Symlink mode: because everything is linked into the checkout, updating is
250
+ just:
251
+
252
+ ```sh
253
+ git pull # in this repo
254
+ npm install # only if dependencies changed
255
+ ```
256
+
257
+ then restart OpenCode sessions (or `opencode service restart`) so the
258
+ running server reloads the plugin. There is no separate install step for
259
+ code or agent changes; the links are already live. Re-run
260
+ `npm run init` only when the **set** of artifacts changes (a new agent
261
+ added to `install.json`, an artifact removed or moved): it creates, fixes,
262
+ or removes repo-owned links to match the manifest.
263
+
264
+ Copy mode adds one extra step, because copies do not follow the repo:
265
+
266
+ ```sh
267
+ git pull # in this repo
268
+ npm install # only if dependencies changed
269
+ npm run init -- --copy # re-sync: re-copies changed files, skips clean ones
270
+ ```
271
+
272
+ then restart OpenCode sessions. Unchanged files are skipped; changed ones
273
+ are re-copied; user-edited copies conflict and wait for `--force`.
274
+
275
+ Shared caveats:
276
+
277
+ - Symlinks point at the clone's absolute path. Do not move, rename, or
278
+ delete the checkout; if you relocate it, re-run `npm run init`.
279
+ - `npm run install:check` reports drift between the manifest and what is
280
+ actually installed; local `npm run ci` gates on it. GitHub CI runs
281
+ `npm run ci:host`, the same chain without the install check, because a
282
+ runner has no installs to check. Verify a copy install with
283
+ `npm run install:check -- --copy`.
284
+
285
+ ### Install the AGENTS.md contracts
286
+
287
+ Turnstile enforces what your instructions file declares. Install the
288
+ matching contract block into your global AGENTS.md:
289
+
290
+ ```sh
291
+ node scripts/install-agents.mjs # or: npm run install:agents
292
+ ```
293
+
294
+ - Default target: `~/.config/opencode/AGENTS.md` (override with `--file`).
295
+ - The block is delimited by `<!-- turnstile:start -->` …
296
+ `<!-- turnstile:end -->` sentinels; re-running updates only that block
297
+ and preserves all surrounding personal content.
298
+ - Legacy targets are adopted automatically. If the file already contains
299
+ pipeline-gates content _without_ sentinels (e.g. an install from before
300
+ the block was managed), the installer classifies the existing section:
301
+ equivalent to the template, it wraps the section in sentinels without
302
+ changing its content; differing from the template, it backs the file up
303
+ to `<name>.bak.<timestamp>` and replaces the section with the current
304
+ block. Pass `--no-adopt` to refuse both and print reconciliation
305
+ instructions instead.
306
+ - The installer refuses (exit 1, no write) only when a target is
307
+ ambiguous (more than one turnstile section) or corrupted (a start
308
+ sentinel without an end sentinel); reconcile those by hand.
309
+ - `--check` reports the outcome without writing: OK, or the adoption
310
+ decision (would-adopt-equivalent / would-replace / drift), exiting 0
311
+ only for OK. Useful in CI or dotfiles setup.
312
+ - Agent definitions and the plugin itself are installed separately via
313
+ `npm run init` (symlinks, for development) or
314
+ `npm run init -- --copy` (see the Install section above).
315
+
316
+ Alternatively, in an OpenCode session, invoke the bundled
317
+ `turnstile-setup` skill, which runs the installer and verifies marker
318
+ sync. The skill ships in the package and installs to
319
+ `~/.config/opencode/skills/turnstile-setup/SKILL.md`; the template
320
+ source lives at `templates/global-AGENTS.md`.
321
+
322
+ ## Configuration
323
+
324
+ Defaults live in code (`src/config.ts`); override via
325
+ `~/.config/opencode/turnstile.json`, environment variables, or both.
326
+ Precedence: defaults < config file < env. The plugin reads config once, at
327
+ setup; restart sessions to apply. Invalid keys are ignored and journaled
328
+ (fail-open); a malformed config file never breaks the host.
329
+
330
+ | Key | Default | Meaning |
331
+ | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------- |
332
+ | `metricsDir` | `$XDG_DATA_HOME/opencode/metrics`, fallback `~/.local/share/opencode/metrics` | directory for both telemetry files |
333
+ | `journalMaxBytes` | 5242880 | rotate a journal file past this size |
334
+ | `journalMaxGenerations` | 5 | rotated generations kept per journal |
335
+ | `maxReviewerRounds` | 2 | reviewer dispatches per task without `GATE: PASS` |
336
+ | `readWarn` / `readBlock` | 8 / 15 | whole-file read horizon |
337
+ | `waiverMarker` | `USER WAIVER:` | literal waiver marker |
338
+ | `plansDir` | `.opencode/plans` | executor decomposition scan directory |
339
+ | `trace` | `true` | write the human-readable trace file (JSONL journal always writes) |
340
+ | `agents.reviewer` / `agents.planReviewer` / `agents.executor` / `agents.testRunner` / `agents.debugger` | `reviewer` / `plan-reviewer` / `executor` / `test-runner` / `debugger` | agent names recognized in dispatches |
341
+ | `agents.bareOutput` | `["test-runner", "reviewer", "plan-reviewer"]` | agents whose marker-less output is a violation |
342
+ | `gateMarker` / `verdictMarker` | canonical regex sources | marker patterns, compiled with `gm` |
343
+
344
+ Environment variables (override the file):
345
+
346
+ - `TURNSTILE_TRACE=0|1`: disable/enable the trace file
347
+ - `TURNSTILE_METRICS_DIR=<dir>`: relocate the telemetry directory
348
+ - `TURNSTILE_CONFIG=<path>`: use a different config file
349
+
350
+ ## Markers
351
+
352
+ Subagents must emit these as the first line of their output. These examples
353
+ are the canonical syntax; CI asserts the parser in `turnstile.ts` accepts
354
+ exactly them.
355
+
356
+ ```
357
+ GATE: PASS tests=12 passed=11 failed=1
358
+ GATE: FAIL tests=12 passed=10 failed=2
359
+
360
+ VERDICT: APPROVE crit=0 high=0 med=2 low=1
361
+ VERDICT: BLOCK crit=1 high=0 med=0 low=2
362
+ ```
363
+
364
+ Waiver marker (must appear in the reviewer dispatch prompt, verbatim):
365
+
366
+ ```
367
+ USER WAIVER: <the user's words>
368
+ ```
369
+
370
+ ## What turnstile is not
371
+
372
+ Turnstile is a speed bump and an audit trail, not a security boundary.
373
+
374
+ - Any text matching the gate marker, including a marker an agent
375
+ hallucinated or copied from documentation, flips `hasGatePass`. An agent
376
+ that wants a green gate can emit `GATE: PASS tests=1 passed=1 failed=0`
377
+ without running anything.
378
+ - `USER WAIVER:` as a substring anywhere in a dispatch prompt unlocks the
379
+ reviewer gate. A prompt-injected waiver is indistinguishable from a real
380
+ one.
381
+ - `scripts/check-markers.mjs` guards against accidental drift between the
382
+ parser and documented markers; it cannot guard against forgery, because
383
+ the markers are plain prose by design.
384
+
385
+ Use OpenCode's permission and sandboxing mechanisms against hostile agents.
386
+ Treat turnstile's journal as the record of what was _claimed_, and treat the
387
+ journal itself as sensitive data (see SECURITY.md).
388
+
389
+ ## Files and journals
390
+
391
+ - Journals live outside the repo in `~/.local/share/opencode/metrics/`:
392
+ - `turnstile.jsonl`: full JSONL trace; rotates at 5 MB, keeps 5
393
+ generations
394
+ - `gate-events.jsonl`: human-readable
395
+ `YYYY-MM-DD HH:MM | agent | event | detail` lines (suppressed entirely
396
+ by `trace: false`)
397
+ - Retention is bounded: 2 files × (1 current + 5 generations) × 5 MB ≈
398
+ ~30 MB per journal, worst case ~60 MB total.
399
+ - `.opencode/plans/*.md` containing `## Decomposition` are load-bearing:
400
+ their presence activates the executor chunk-citation gate.
401
+
402
+ ## Behavior contract
403
+
404
+ - Fail-open by design. Every hook body is guarded; internal errors are
405
+ journaled and the tool call proceeds. Removing the plugin leaves the
406
+ pipeline functional (prompt contracts alone). The only thrown errors are
407
+ the deliberate `BLOCKED by turnstile:` ones.
408
+ - The doc comment at the top of `turnstile.ts` is the authoritative summary
409
+ of enforced contracts; this README mirrors it and CI keeps the marker
410
+ examples in sync with the parser regexes.
411
+ - Host API is the OpenCode v2 promise-plugin shape
412
+ (`{ tool.hook, session.hook, event.subscribe }`), verified against
413
+ opencode v2.0.22.
package/bin/cli.mjs ADDED
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ // Thin argv router for the opencode-turnstile package. Maps the four
3
+ // subcommands onto the installer scripts (spawned, not imported, so each
4
+ // keeps its own process.exit and stdio) or prints the package version. Paths
5
+ // are resolved against this file, not cwd, so `npx opencode-turnstile` works
6
+ // from any directory.
7
+ //
8
+ // opencode-turnstile init Install/sync artifacts into ~/.config/opencode
9
+ // opencode-turnstile check Verify artifacts against the manifest
10
+ // opencode-turnstile uninstall Remove installed artifacts (agents + block)
11
+ // opencode-turnstile version Print the installed package version
12
+ //
13
+ // Unknown subcommand or no args prints usage and exits 2.
14
+
15
+ import { spawn } from "node:child_process"
16
+ import { readFileSync } from "node:fs"
17
+ import { dirname, join, resolve } from "node:path"
18
+ import { fileURLToPath } from "node:url"
19
+
20
+ const here = dirname(fileURLToPath(import.meta.url))
21
+ const repoRoot = resolve(here, "..")
22
+ const installScript = join(repoRoot, "scripts/install.mjs")
23
+ const agentsScript = join(repoRoot, "scripts/install-agents.mjs")
24
+
25
+ const USAGE = `Usage: opencode-turnstile <command>
26
+
27
+ Commands:
28
+ init Install or sync artifacts into ~/.config/opencode
29
+ check Verify installed artifacts against the manifest
30
+ uninstall Remove installed artifacts (agents + block)
31
+ version Print the installed package version
32
+
33
+ A git checkout installs by linking (live edits); a package install copies.`
34
+
35
+ /**
36
+ * Spawns an installer script with inherited stdio and resolves with its exit
37
+ * code so this router returns the child's status verbatim.
38
+ * @param {string} script Absolute path to the script to run.
39
+ * @param {string[]} passthrough Arguments forwarded to the installer.
40
+ * @returns {Promise<number>} The child's exit code.
41
+ */
42
+ function spawnInstaller(script, passthrough) {
43
+ return new Promise((resolveChild) => {
44
+ const child = spawn(process.execPath, [script, ...passthrough], { stdio: "inherit" })
45
+ child.on("error", () => resolveChild(1))
46
+ child.on("exit", (code, signal) => {
47
+ if (signal) process.kill(process.pid, signal)
48
+ resolveChild(code ?? 0)
49
+ })
50
+ })
51
+ }
52
+
53
+ async function main() {
54
+ const [command, ...passthrough] = process.argv.slice(2)
55
+
56
+ switch (command) {
57
+ case "init": {
58
+ // Full install: manifest artifacts (plugin + agents), then the
59
+ // managed contract block in ~/.config/opencode/AGENTS.md, mirroring
60
+ // uninstall's two-step shape in reverse. Passthrough flags go to the
61
+ // manifest installer; the agents installer needs none of them.
62
+ const byInstall = await spawnInstaller(installScript, passthrough)
63
+ const byAgents = await spawnInstaller(agentsScript, [])
64
+ return process.exit(byInstall || byAgents ? 1 : 0)
65
+ }
66
+ case "check":
67
+ return spawnInstaller(installScript, ["--check", ...passthrough])
68
+ case "uninstall": {
69
+ // Reverse the copy/symlink install, then unmerge the managed block.
70
+ // Inherited stdio surfaces both scripts' reports verbatim; exit non-zero
71
+ // only if either step failed. No restart note is printed here; each
72
+ // child prints its own, gated on whether it changed anything.
73
+ const byInstall = await spawnInstaller(installScript, ["--uninstall"])
74
+ const byAgents = await spawnInstaller(agentsScript, ["--remove"])
75
+ return process.exit(byInstall || byAgents ? 1 : 0)
76
+ }
77
+ case "version":
78
+ console.log(JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")).version)
79
+ return
80
+ default:
81
+ console.log(USAGE)
82
+ process.exit(2)
83
+ }
84
+ }
85
+
86
+ await main().then((code) => process.exit(code ?? 0))
package/install.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "ledger": "~/.config/opencode/.turnstile-install.json",
3
+ "links": [
4
+ {
5
+ "src": "turnstile.ts",
6
+ "dest": "~/.config/opencode/plugins/turnstile.ts"
7
+ },
8
+ {
9
+ "src": ".opencode/agents/plan-reviewer.md",
10
+ "dest": "~/.config/opencode/agents/plan-reviewer.md"
11
+ },
12
+ {
13
+ "src": ".opencode/agents/planner.md",
14
+ "dest": "~/.config/opencode/agents/planner.md"
15
+ },
16
+ {
17
+ "src": ".opencode/agents/executor.md",
18
+ "dest": "~/.config/opencode/agents/executor.md"
19
+ },
20
+ {
21
+ "src": ".opencode/agents/test-runner.md",
22
+ "dest": "~/.config/opencode/agents/test-runner.md"
23
+ },
24
+ {
25
+ "src": ".opencode/agents/reviewer.md",
26
+ "dest": "~/.config/opencode/agents/reviewer.md"
27
+ },
28
+ {
29
+ "src": ".opencode/agents/debugger.md",
30
+ "dest": "~/.config/opencode/agents/debugger.md"
31
+ },
32
+ {
33
+ "src": "skills/turnstile-setup/SKILL.md",
34
+ "dest": "~/.config/opencode/skills/turnstile-setup/SKILL.md"
35
+ }
36
+ ],
37
+ "retired": [
38
+ {
39
+ "dest": "~/.config/opencode/plugin/turnstile.ts"
40
+ }
41
+ ]
42
+ }
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
2
  "name": "opencode-turnstile",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "OpenCode plugin that mechanically enforces agent pipeline gates (planner → executor → test-runner → reviewer)",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "opencode-turnstile": "bin/cli.mjs"
9
+ },
10
+ "files": [
11
+ "turnstile.ts",
12
+ "src",
13
+ "scripts",
14
+ "bin",
15
+ "install.json",
16
+ "templates",
17
+ "skills",
18
+ ".opencode/agents",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "scripts": {
23
+ "typecheck": "tsc --noEmit",
24
+ "lint": "eslint .",
25
+ "lint:fix": "eslint . --fix",
26
+ "format": "prettier --write .",
27
+ "format:check": "prettier --check .",
28
+ "check:markers": "node scripts/check-markers.mjs",
29
+ "test": "node --test --experimental-strip-types tests/*.test.ts",
30
+ "init": "node scripts/install.mjs",
31
+ "init:copy": "node scripts/install.mjs --copy",
32
+ "init:check": "node scripts/install.mjs --check",
33
+ "install:copy": "node scripts/install.mjs --copy",
34
+ "install:check": "node scripts/install.mjs --check",
35
+ "install:agents": "node scripts/install-agents.mjs",
36
+ "pack:dry": "npm pack --dry-run",
37
+ "ci:host": "npm run lint && npm run format:check && npm run typecheck && npm run check:markers && npm test",
38
+ "ci": "npm run ci:host && npm run install:check"
39
+ },
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/edseabolt/turnstile.git"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/edseabolt/turnstile/issues"
46
+ },
47
+ "homepage": "https://github.com/edseabolt/turnstile#readme",
48
+ "engines": {
49
+ "node": ">=22.6"
50
+ },
51
+ "devDependencies": {
52
+ "@eslint/js": "^10.0.1",
53
+ "@types/node": "^22.10.0",
54
+ "eslint": "^10.12.0",
55
+ "globals": "^17.13.0",
56
+ "prettier": "^3.9.9",
57
+ "typescript": "^5.7.0",
58
+ "typescript-eslint": "^8.71.1"
59
+ }
60
+ }