opencode-turnstile 0.0.0-stage → 0.1.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/README.md CHANGED
@@ -1,3 +1,415 @@
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
98
+ [`opencode-turnstile`](https://www.npmjs.com/package/opencode-turnstile).
99
+ There are three
100
+ ways to use it; pick one, and do not combine the plugin-array path with
101
+ the CLI install.
102
+
103
+ 1. **Zero-config enforcement (plugin only).** Add the package to your
104
+ OpenCode config's plugin array and restart. turnstile loads and
105
+ enforces its gates, but neither the pipeline agent definitions nor the
106
+ AGENTS.md contract block are installed:
107
+
108
+ ```json
109
+ { "plugins": ["opencode-turnstile"] }
110
+ ```
111
+
112
+ 2. **Full install via the CLI.** `npx opencode-turnstile init` installs
113
+ the plugin, the pipeline agent definitions, and the managed contract
114
+ block in `~/.config/opencode/AGENTS.md` (see
115
+ [From a clone](#from-a-clone) below for the same commands run from
116
+ a clone).
117
+
118
+ 3. **Develop from a clone.** Clone this repo and run `npm run init`
119
+ (symlink mode) so installed artifacts stay live against your edits.
120
+ See [From a clone](#from-a-clone) below.
121
+
122
+ ### No postinstall
123
+
124
+ Turnstile has **no** npm lifecycle hook (`postinstall`, etc.). Installing
125
+ `opencode-turnstile` as a dependency never writes to
126
+ `~/.config/opencode`; it only makes the gates available through the
127
+ plugin array. Everything else (agent definitions, the AGENTS.md contract
128
+ block) happens only when you run an explicit command. This is deliberate:
129
+ writes to your config directory are opt-in, not automatic.
130
+
131
+ ### Modes
132
+
133
+ `init` auto-detects an install mode: a git checkout installs by
134
+ linking (the working tree stays the source of truth, so edits are
135
+ live); an `npx` package install copies files (the installed files are
136
+ the source of truth). Override with `--link` or `--copy`:
137
+
138
+ ```sh
139
+ npx opencode-turnstile init --copy
140
+ ```
141
+
142
+ The chosen mode is recorded in the ledger, so `check` stays consistent
143
+ across reruns.
144
+
145
+ ### Check
146
+
147
+ `npx opencode-turnstile check` verifies the installed state against the
148
+ manifest and exits 0 (clean) or 1 (drift). A copy install whose recorded
149
+ version is older than the published package is reported as a warning
150
+ (`check: outdated <dest> (installed X, package Y)`) and does not fail the
151
+ check.
152
+
153
+ ### Upgrade
154
+
155
+ The plugin updates with your dependency manager (`npm upgrade
156
+ opencode-turnstile`, or your registry's equivalent). Re-run `check` to see
157
+ whether anything is outdated or drifted, then `init` to re-sync only what
158
+ changed: `npx opencode-turnstile init --copy` for package installs,
159
+ `npm run init -- --copy` from a clone. The AGENTS.md contract block is
160
+ re-synced the same way, keeping only the managed block in step.
161
+
162
+ ### Uninstall
163
+
164
+ `npx opencode-turnstile uninstall` reverses the install: it removes
165
+ ledger-owned copies and repo-owned symlinks and unmerges the managed
166
+ contract block from AGENTS.md. It is idempotent and ownership-aware:
167
+ files it never installed (untracked files, symlinks pointing outside the
168
+ repo) are left alone, and a second run is a clean no-op.
169
+
170
+ ### From a clone
171
+
172
+ This repo is the source of truth for what lands in `~/.config/opencode`:
173
+ the plugin and the pipeline agent definitions are installed per the
174
+ manifest in `install.json`; unowned files in the target directories are
175
+ never touched. Two install modes are available:
176
+
177
+ | | Symlink (default) | Copy (`--copy`) |
178
+ | ------------------------- | --------------------------------------------------- | ------------------------------------------------------- |
179
+ | Best for | developing turnstile | general use, dotfiles repos that reject foreign links |
180
+ | How targets point at repo | symlinks into the checkout | real copies |
181
+ | After `git pull` | nothing to do; edits are live on next session start | re-run `npm run init -- --copy` to re-sync |
182
+ | Editing installed files | edit the repo; links share it | edit the repo; user-edited copies conflict on next sync |
183
+
184
+ ```sh
185
+ git clone <repo-url> && cd turnstile
186
+ npm install # typescript + @types/node, for typechecking
187
+ npm run init # symlink mode
188
+ # or: npm run install:copy
189
+ npm run install:check # verify installed state; exit 1 on drift (gates local CI)
190
+ ```
191
+
192
+ A target that differs from the repo is a **conflict**: `install` fails with
193
+ a per-path summary. Re-run with `--force` to back the file up
194
+ (`<name>.bak.<timestamp>`) and replace it. Conflicts mean:
195
+
196
+ - symlink mode: a real file (or a link pointing outside the repo) sits at a
197
+ manifest target and differs from the source, typically pre-existing
198
+ files or hand edits.
199
+ - copy mode: an installed copy was edited after install (the ledger can
200
+ tell user edits from repo updates), or an untracked real file sits at a
201
+ target.
202
+
203
+ Copy mode records everything it installed in
204
+ `~/.config/opencode/.turnstile-install.json` (the ledger: destination
205
+ mapped to source, content hash, package version, and install mode). The
206
+ ledger makes re-syncs safe: repo
207
+ updates re-copy cleanly and user edits fail loudly. It also lets the
208
+ installer remove copies whose manifest entry was deleted. Deleting the
209
+ ledger turns every copy into an untracked file the installer will refuse
210
+ to touch.
211
+
212
+ ### Set the agents' models
213
+
214
+ The six agent definitions in `.opencode/agents/` ship with `model:`
215
+ frontmatter pointing at the author's locally served models (`oMLX/...`).
216
+ You almost certainly do not have those models, and dispatches to the
217
+ pipeline agents will fail or fall back until you set your own.
218
+
219
+ Since installs share the repo's files (symlink mode) or conflict on
220
+ divergence (copy mode), set your models **in the clone**, not in
221
+ `~/.config/opencode/agents/`: edit the `model:` line of each agent file
222
+ (`planner`, `plan-reviewer`, `executor`, `test-runner`, `reviewer`,
223
+ `debugger`) to a model
224
+ you have, in `providerID/modelID` form, then install or re-sync per the
225
+ mode table above. Model overrides in installed copies are lost on the next
226
+ sync; the repo is the source of truth.
227
+
228
+ ### Customizing the pipeline agents
229
+
230
+ The agent definitions are opinionated on purpose: turnstile recognizes
231
+ and clamps them by name, and the marker contract in the agent prompts is
232
+ what the plugin enforces against. Editing the installed files is therefore
233
+ not a supported customization path: symlink installs share the repo file,
234
+ and copy installs conflict on the next sync.
235
+
236
+ Two sanctioned ways to change the pipeline:
237
+
238
+ - **Bring your own agents.** Add your own agent definitions (any names) to
239
+ `~/.config/opencode/agents/`; the installer never touches files it does
240
+ not own. Then point turnstile at them via
241
+ `~/.config/opencode/turnstile.json`: `agents.planReviewer`,
242
+ `agents.reviewer`, `agents.executor`, `agents.testRunner`, and
243
+ `agents.debugger` take the names turnstile should recognize, clamp, and
244
+ hold to the marker contract. Your agents get the same gates the shipped
245
+ ones do.
246
+ - **Fork the repo.** To change the pipeline content itself, fork and edit
247
+ `.opencode/agents/*.md` there; your installer serves your fork.
248
+
249
+ ### Updating
250
+
251
+ Symlink mode: because everything is linked into the checkout, updating is
252
+ just:
253
+
254
+ ```sh
255
+ git pull # in this repo
256
+ npm install # only if dependencies changed
257
+ ```
258
+
259
+ then restart OpenCode sessions (or `opencode service restart`) so the
260
+ running server reloads the plugin. There is no separate install step for
261
+ code or agent changes; the links are already live. Re-run
262
+ `npm run init` only when the **set** of artifacts changes (a new agent
263
+ added to `install.json`, an artifact removed or moved): it creates, fixes,
264
+ or removes repo-owned links to match the manifest.
265
+
266
+ Copy mode adds one extra step, because copies do not follow the repo:
267
+
268
+ ```sh
269
+ git pull # in this repo
270
+ npm install # only if dependencies changed
271
+ npm run init -- --copy # re-sync: re-copies changed files, skips clean ones
272
+ ```
273
+
274
+ then restart OpenCode sessions. Unchanged files are skipped; changed ones
275
+ are re-copied; user-edited copies conflict and wait for `--force`.
276
+
277
+ Shared caveats:
278
+
279
+ - Symlinks point at the clone's absolute path. Do not move, rename, or
280
+ delete the checkout; if you relocate it, re-run `npm run init`.
281
+ - `npm run install:check` reports drift between the manifest and what is
282
+ actually installed; local `npm run ci` gates on it. GitHub CI runs
283
+ `npm run ci:host`, the same chain without the install check, because a
284
+ runner has no installs to check. Verify a copy install with
285
+ `npm run install:check -- --copy`.
286
+
287
+ ### Install the AGENTS.md contracts
288
+
289
+ Turnstile enforces what your instructions file declares. Install the
290
+ matching contract block into your global AGENTS.md:
291
+
292
+ ```sh
293
+ node scripts/install-agents.mjs # or: npm run install:agents
294
+ ```
295
+
296
+ - Default target: `~/.config/opencode/AGENTS.md` (override with `--file`).
297
+ - The block is delimited by `<!-- turnstile:start -->` …
298
+ `<!-- turnstile:end -->` sentinels; re-running updates only that block
299
+ and preserves all surrounding personal content.
300
+ - Legacy targets are adopted automatically. If the file already contains
301
+ pipeline-gates content _without_ sentinels (e.g. an install from before
302
+ the block was managed), the installer classifies the existing section:
303
+ equivalent to the template, it wraps the section in sentinels without
304
+ changing its content; differing from the template, it backs the file up
305
+ to `<name>.bak.<timestamp>` and replaces the section with the current
306
+ block. Pass `--no-adopt` to refuse both and print reconciliation
307
+ instructions instead.
308
+ - The installer refuses (exit 1, no write) only when a target is
309
+ ambiguous (more than one turnstile section) or corrupted (a start
310
+ sentinel without an end sentinel); reconcile those by hand.
311
+ - `--check` reports the outcome without writing: OK, or the adoption
312
+ decision (would-adopt-equivalent / would-replace / drift), exiting 0
313
+ only for OK. Useful in CI or dotfiles setup.
314
+ - Agent definitions and the plugin itself are installed separately via
315
+ `npm run init` (symlinks, for development) or
316
+ `npm run init -- --copy` (see the Install section above).
317
+
318
+ Alternatively, in an OpenCode session, invoke the bundled
319
+ `turnstile-setup` skill, which runs the installer and verifies marker
320
+ sync. The skill ships in the package and installs to
321
+ `~/.config/opencode/skills/turnstile-setup/SKILL.md`; the template
322
+ source lives at `templates/global-AGENTS.md`.
323
+
324
+ ## Configuration
325
+
326
+ Defaults live in code (`src/config.ts`); override via
327
+ `~/.config/opencode/turnstile.json`, environment variables, or both.
328
+ Precedence: defaults < config file < env. The plugin reads config once, at
329
+ setup; restart sessions to apply. Invalid keys are ignored and journaled
330
+ (fail-open); a malformed config file never breaks the host.
331
+
332
+ | Key | Default | Meaning |
333
+ | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------- |
334
+ | `metricsDir` | `$XDG_DATA_HOME/opencode/metrics`, fallback `~/.local/share/opencode/metrics` | directory for both telemetry files |
335
+ | `journalMaxBytes` | 5242880 | rotate a journal file past this size |
336
+ | `journalMaxGenerations` | 5 | rotated generations kept per journal |
337
+ | `maxReviewerRounds` | 2 | reviewer dispatches per task without `GATE: PASS` |
338
+ | `readWarn` / `readBlock` | 8 / 15 | whole-file read horizon |
339
+ | `waiverMarker` | `USER WAIVER:` | literal waiver marker |
340
+ | `plansDir` | `.opencode/plans` | executor decomposition scan directory |
341
+ | `trace` | `true` | write the human-readable trace file (JSONL journal always writes) |
342
+ | `agents.reviewer` / `agents.planReviewer` / `agents.executor` / `agents.testRunner` / `agents.debugger` | `reviewer` / `plan-reviewer` / `executor` / `test-runner` / `debugger` | agent names recognized in dispatches |
343
+ | `agents.bareOutput` | `["test-runner", "reviewer", "plan-reviewer"]` | agents whose marker-less output is a violation |
344
+ | `gateMarker` / `verdictMarker` | canonical regex sources | marker patterns, compiled with `gm` |
345
+
346
+ Environment variables (override the file):
347
+
348
+ - `TURNSTILE_TRACE=0|1`: disable/enable the trace file
349
+ - `TURNSTILE_METRICS_DIR=<dir>`: relocate the telemetry directory
350
+ - `TURNSTILE_CONFIG=<path>`: use a different config file
351
+
352
+ ## Markers
353
+
354
+ Subagents must emit these as the first line of their output. These examples
355
+ are the canonical syntax; CI asserts the parser in `turnstile.ts` accepts
356
+ exactly them.
357
+
358
+ ```
359
+ GATE: PASS tests=12 passed=11 failed=1
360
+ GATE: FAIL tests=12 passed=10 failed=2
361
+
362
+ VERDICT: APPROVE crit=0 high=0 med=2 low=1
363
+ VERDICT: BLOCK crit=1 high=0 med=0 low=2
364
+ ```
365
+
366
+ Waiver marker (must appear in the reviewer dispatch prompt, verbatim):
367
+
368
+ ```
369
+ USER WAIVER: <the user's words>
370
+ ```
371
+
372
+ ## What turnstile is not
373
+
374
+ Turnstile is a speed bump and an audit trail, not a security boundary.
375
+
376
+ - Any text matching the gate marker, including a marker an agent
377
+ hallucinated or copied from documentation, flips `hasGatePass`. An agent
378
+ that wants a green gate can emit `GATE: PASS tests=1 passed=1 failed=0`
379
+ without running anything.
380
+ - `USER WAIVER:` as a substring anywhere in a dispatch prompt unlocks the
381
+ reviewer gate. A prompt-injected waiver is indistinguishable from a real
382
+ one.
383
+ - `scripts/check-markers.mjs` guards against accidental drift between the
384
+ parser and documented markers; it cannot guard against forgery, because
385
+ the markers are plain prose by design.
386
+
387
+ Use OpenCode's permission and sandboxing mechanisms against hostile agents.
388
+ Treat turnstile's journal as the record of what was _claimed_, and treat the
389
+ journal itself as sensitive data (see SECURITY.md).
390
+
391
+ ## Files and journals
392
+
393
+ - Journals live outside the repo in `~/.local/share/opencode/metrics/`:
394
+ - `turnstile.jsonl`: full JSONL trace; rotates at 5 MB, keeps 5
395
+ generations
396
+ - `gate-events.jsonl`: human-readable
397
+ `YYYY-MM-DD HH:MM | agent | event | detail` lines (suppressed entirely
398
+ by `trace: false`)
399
+ - Retention is bounded: 2 files × (1 current + 5 generations) × 5 MB ≈
400
+ ~30 MB per journal, worst case ~60 MB total.
401
+ - `.opencode/plans/*.md` containing `## Decomposition` are load-bearing:
402
+ their presence activates the executor chunk-citation gate.
403
+
404
+ ## Behavior contract
405
+
406
+ - Fail-open by design. Every hook body is guarded; internal errors are
407
+ journaled and the tool call proceeds. Removing the plugin leaves the
408
+ pipeline functional (prompt contracts alone). The only thrown errors are
409
+ the deliberate `BLOCKED by turnstile:` ones.
410
+ - The doc comment at the top of `turnstile.ts` is the authoritative summary
411
+ of enforced contracts; this README mirrors it and CI keeps the marker
412
+ examples in sync with the parser regexes.
413
+ - Host API is the OpenCode v2 promise-plugin shape
414
+ (`{ tool.hook, session.hook, event.subscribe }`), verified against
415
+ 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.1",
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
+ }