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/.opencode/agents/debugger.md +70 -0
- package/.opencode/agents/executor.md +108 -0
- package/.opencode/agents/plan-reviewer.md +91 -0
- package/.opencode/agents/planner.md +111 -0
- package/.opencode/agents/reviewer.md +92 -0
- package/.opencode/agents/test-runner.md +55 -0
- package/LICENSE +21 -0
- package/README.md +414 -2
- package/bin/cli.mjs +86 -0
- package/install.json +42 -0
- package/package.json +58 -4
- package/scripts/check-markers.mjs +86 -0
- package/scripts/install-agents.mjs +504 -0
- package/scripts/install.mjs +559 -0
- package/skills/turnstile-setup/SKILL.md +76 -0
- package/src/config.ts +272 -0
- package/src/gates.ts +295 -0
- package/src/journal.ts +157 -0
- package/src/markers.ts +113 -0
- package/src/setup.ts +512 -0
- package/src/state.ts +181 -0
- package/src/types.ts +176 -0
- package/templates/global-AGENTS.md +55 -0
- package/turnstile.ts +82 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared type definitions for the turnstile plugin: the
|
|
3
|
+
* OpenCode v2 host wire shape, hook payloads, and gate-state structures.
|
|
4
|
+
*
|
|
5
|
+
* The host boundary is intentionally permissive: payloads crossing the
|
|
6
|
+
* plugin boundary are typed as optional fields so a host that omits or
|
|
7
|
+
* reshapes a field fails open instead of failing hard.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Task dispatch input as delivered by the host's `task`/`subagent` tools. */
|
|
11
|
+
export interface TaskInput {
|
|
12
|
+
subagent_type?: unknown
|
|
13
|
+
agent?: unknown
|
|
14
|
+
subagent?: unknown
|
|
15
|
+
prompt?: unknown
|
|
16
|
+
description?: unknown
|
|
17
|
+
command?: unknown
|
|
18
|
+
cmd?: unknown
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Tool result as delivered by the host's `execute.after` hook. */
|
|
22
|
+
export interface ToolResult {
|
|
23
|
+
output?: unknown
|
|
24
|
+
title?: unknown
|
|
25
|
+
input?: unknown
|
|
26
|
+
metadata?: { exit?: unknown; exitCode?: unknown; code?: unknown }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Payload of the `tool.execute.before` and `tool.execute.after` hooks. */
|
|
30
|
+
export interface ToolHookEvent {
|
|
31
|
+
tool?: unknown
|
|
32
|
+
sessionID?: unknown
|
|
33
|
+
input?: TaskInput
|
|
34
|
+
result?: ToolResult
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Payload of the `session.prompt` hook. */
|
|
38
|
+
export interface PromptHookProperties {
|
|
39
|
+
sessionID?: unknown
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Generation draft passed to the `session.generate` hook. */
|
|
43
|
+
export interface GenerateDraft {
|
|
44
|
+
agent?: unknown
|
|
45
|
+
options?: { temperature?: number }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Compaction draft passed to the `session.compaction` hook. */
|
|
49
|
+
export interface CompactionDraft {
|
|
50
|
+
system?: Array<{ type: string; text?: string }>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Session record returned by the host's `session.get`. */
|
|
54
|
+
export interface SessionRecord {
|
|
55
|
+
parentID?: unknown
|
|
56
|
+
data?: { parentID?: unknown }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Properties bag carried by host stream events. */
|
|
60
|
+
export interface EventProperties {
|
|
61
|
+
type?: unknown
|
|
62
|
+
sessionID?: unknown
|
|
63
|
+
parentID?: unknown
|
|
64
|
+
directory?: unknown
|
|
65
|
+
location?: { directory?: unknown }
|
|
66
|
+
part?: { type?: unknown; text?: unknown; sessionID?: unknown }
|
|
67
|
+
text?: unknown
|
|
68
|
+
delta?: unknown
|
|
69
|
+
error?: unknown
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A single host stream event delivered by `event.subscribe`. */
|
|
73
|
+
export interface StreamEvent {
|
|
74
|
+
type: string
|
|
75
|
+
data?: EventProperties
|
|
76
|
+
properties?: EventProperties
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Gate state for one parent session. */
|
|
80
|
+
export interface GateState {
|
|
81
|
+
hasGatePass: boolean
|
|
82
|
+
/** True when this task's plan has a current plan-reviewer APPROVE. */
|
|
83
|
+
hasPlanVerdict: boolean
|
|
84
|
+
verdicts: string[]
|
|
85
|
+
reviewRounds: number
|
|
86
|
+
reads: number
|
|
87
|
+
seen: Set<string>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** One first-sight marker observation returned by the marker parser. */
|
|
91
|
+
export interface MarkerEvent {
|
|
92
|
+
kind: "gate" | "verdict"
|
|
93
|
+
marker: string
|
|
94
|
+
result?: "PASS" | "FAIL"
|
|
95
|
+
rounds?: number
|
|
96
|
+
/** Marker matches observed in the same text but not journaled
|
|
97
|
+
* (latest-wins policy); 0 when this was the only match. */
|
|
98
|
+
suppressed: number
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Agent names turnstile recognizes in dispatches and generation drafts. */
|
|
102
|
+
export interface TurnstileAgents {
|
|
103
|
+
reviewer: string
|
|
104
|
+
/** Plan reviewer: adversarial review of the implementation plan. */
|
|
105
|
+
planReviewer: string
|
|
106
|
+
executor: string
|
|
107
|
+
testRunner: string
|
|
108
|
+
debugger: string
|
|
109
|
+
/** Agents contracted to emit GATE/VERDICT markers (bare-output check). */
|
|
110
|
+
bareOutput: string[]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Layered plugin configuration. Defaults in `src/config.ts`; user overrides
|
|
115
|
+
* via `~/.config/opencode/turnstile.json`, then env vars, then injected
|
|
116
|
+
* options (tests). Read once at setup; restart sessions to apply.
|
|
117
|
+
*/
|
|
118
|
+
export interface TurnstileConfig {
|
|
119
|
+
/** Directory holding the JSONL journal and trace files. */
|
|
120
|
+
metricsDir: string
|
|
121
|
+
/** Rotate a journal file when it exceeds this many bytes. */
|
|
122
|
+
journalMaxBytes: number
|
|
123
|
+
/** Number of rotated journal generations to keep. */
|
|
124
|
+
journalMaxGenerations: number
|
|
125
|
+
/**
|
|
126
|
+
* Max reviewer dispatches per task; the cap counts dispatches, applies
|
|
127
|
+
* regardless of gate state, and is bypassed by an explicit waiver.
|
|
128
|
+
*/
|
|
129
|
+
maxReviewerRounds: number
|
|
130
|
+
/** Cap on tracked sessions (LRU); oldest state evicted beyond this. */
|
|
131
|
+
maxSessions: number
|
|
132
|
+
/** Whole-file read count at which the horizon warning is injected. */
|
|
133
|
+
readWarn: number
|
|
134
|
+
/** Whole-file read count at which further reads are blocked. */
|
|
135
|
+
readBlock: number
|
|
136
|
+
/** Literal marker that waives the reviewer gate when in the prompt. */
|
|
137
|
+
waiverMarker: string
|
|
138
|
+
/** Project-relative directory scanned for `## Decomposition` plans. */
|
|
139
|
+
plansDir: string
|
|
140
|
+
/** Whether the human-readable trace file is written (JSONL journal always). */
|
|
141
|
+
trace: boolean
|
|
142
|
+
/** Agent names turnstile recognizes. */
|
|
143
|
+
agents: TurnstileAgents
|
|
144
|
+
/** Gate marker regex source (compiled with `gm`). */
|
|
145
|
+
gateMarker: string
|
|
146
|
+
/** Verdict marker regex source (compiled with `gm`). */
|
|
147
|
+
verdictMarker: string
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Options accepted by the plugin factory; tests inject config and env here. */
|
|
151
|
+
export interface TurnstileOptions {
|
|
152
|
+
/** Overrides applied last, on top of file and env layers. */
|
|
153
|
+
config?: Partial<TurnstileConfig>
|
|
154
|
+
/** Environment for config resolution; defaults to `process.env`. */
|
|
155
|
+
env?: Record<string, string | undefined>
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The v2 promise-plugin host shape (verified against opencode v2.0.22).
|
|
160
|
+
*
|
|
161
|
+
* Hook callback payloads are typed `any` at this wire boundary only; the
|
|
162
|
+
* host does not expose stable payload types. Every function turnstile
|
|
163
|
+
* defines has concrete parameter and return types; the `any` ends here.
|
|
164
|
+
*/
|
|
165
|
+
export type Host = {
|
|
166
|
+
tool?: {
|
|
167
|
+
hook?: (name: string, cb: (event: any) => Promise<void> | void) => Promise<unknown>
|
|
168
|
+
}
|
|
169
|
+
session?: {
|
|
170
|
+
hook?: (name: string, cb: (input: any) => Promise<void> | void) => Promise<unknown>
|
|
171
|
+
get?: (input: { sessionID: string }, options?: any) => Promise<SessionRecord | undefined>
|
|
172
|
+
}
|
|
173
|
+
event?: {
|
|
174
|
+
subscribe?: (options?: { signal?: AbortSignal }) => AsyncIterable<StreamEvent>
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<!-- turnstile:start (managed block — edit templates/global-AGENTS.md in the
|
|
2
|
+
turnstile repo, then rerun the installer; do not edit in place) -->
|
|
3
|
+
|
|
4
|
+
## Turnstile pipeline gates
|
|
5
|
+
|
|
6
|
+
Order: planner → plan-reviewer → executor → test-runner → reviewer. This
|
|
7
|
+
file is the source of truth for policy; the `turnstile` plugin (installed
|
|
8
|
+
at `~/.config/opencode/plugins/turnstile.ts`) enforces it mechanically.
|
|
9
|
+
Enforcement mechanics (regexes, thresholds, journals) are canonical in
|
|
10
|
+
turnstile's README; if prose here diverges from enforcement, trust the
|
|
11
|
+
plugin and fix this file. Advisory only if the plugin is absent.
|
|
12
|
+
|
|
13
|
+
Plan gate: dispatch `executor` only after this task's plan-reviewer first
|
|
14
|
+
line is `VERDICT: APPROVE crit=<n> high=<n> med=<n> low=<n>`, or after an
|
|
15
|
+
explicit user waiver. Plan-file changes invalidate the approval — after a
|
|
16
|
+
re-plan, dispatch plan-reviewer again before resuming executor work.
|
|
17
|
+
|
|
18
|
+
Dispatch gate: dispatch `reviewer` only after this task's test-runner first
|
|
19
|
+
line is `GATE: PASS tests=<n> passed=<n> failed=<n>`, or after an explicit
|
|
20
|
+
user waiver. Max 2 reviewer dispatches per task; then escalate the open
|
|
21
|
+
findings to the user.
|
|
22
|
+
|
|
23
|
+
Markers (first line, machine-parseable):
|
|
24
|
+
|
|
25
|
+
- test-runner: `GATE: PASS|FAIL tests=<n> passed=<n> failed=<n>`
|
|
26
|
+
- reviewer: `VERDICT: APPROVE|BLOCK crit=<n> high=<n> med=<n> low=<n>`
|
|
27
|
+
- plan-reviewer: `VERDICT: APPROVE|BLOCK crit=<n> high=<n> med=<n> low=<n>`
|
|
28
|
+
(first line; then an AC traceability table with `Covered-by` per AC)
|
|
29
|
+
- reviewer acceptance table: add an `AC-ID` column referencing `AC-<n>`
|
|
30
|
+
- planner: number criteria `AC-1…AC-n`; if the change spans >5 files or >3
|
|
31
|
+
outcomes, add `## Decomposition` with ordered `chunk-N` entries (≤5 files)
|
|
32
|
+
- executor/debugger: 5-file budget; if 10 tool calls pass without a test
|
|
33
|
+
command, STOP, emit a state summary, re-anchor on the plan.
|
|
34
|
+
|
|
35
|
+
Reviewer ensemble: dispatch two `reviewer` subagents on the same diff; merge
|
|
36
|
+
one finding per `file+symbol` (max severity). A singleton CRITICAL/HIGH
|
|
37
|
+
blocks only after re-reading the cited location; LOW singletons become
|
|
38
|
+
`UNCONFIRMED` notes.
|
|
39
|
+
|
|
40
|
+
Worktree/commit: git worktrees will be used, but it does not matter how they
|
|
41
|
+
get created. Work inside the checkout and branch your session starts in; do
|
|
42
|
+
not create/add/remove worktrees, switch branches, merge, or open PRs unless
|
|
43
|
+
the user explicitly asks. On success, commit your change set with a
|
|
44
|
+
conventional message and stop. If a worktree is in use, assert a `WORKTREE:`
|
|
45
|
+
header for logging; otherwise skip it. Committing on a shared default branch
|
|
46
|
+
means your change lands there; treat main/branch commits as high
|
|
47
|
+
sensitivity; do not self-reset.
|
|
48
|
+
|
|
49
|
+
Trace (works with the plugin off): append every dispatch/verdict to
|
|
50
|
+
`~/.local/share/opencode/metrics/gate-events.jsonl` as
|
|
51
|
+
`YYYY-MM-DD HH:MM | agent | event | detail`. The plugin also writes
|
|
52
|
+
`~/.local/share/opencode/metrics/turnstile.jsonl`
|
|
53
|
+
(`{ts,sessionID,type,...}`). Read: grep/glob/rg first, whole-file reads last.
|
|
54
|
+
|
|
55
|
+
<!-- turnstile:end -->
|
package/turnstile.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turnstile: runtime enforcement of the AGENTS.md pipeline contracts.
|
|
3
|
+
*
|
|
4
|
+
* Written for OpenCode v2.x promise-plugin host (`{ id, setup }`):
|
|
5
|
+
* - `ctx.tool.hook("execute.before" | "execute.after", cb)`
|
|
6
|
+
* - `ctx.session.hook("prompt" | "generate" | "compaction", cb)`
|
|
7
|
+
* - `ctx.event.subscribe({ signal })`
|
|
8
|
+
*
|
|
9
|
+
* Enforced mechanically:
|
|
10
|
+
* - Plan-approval gate: `task` dispatches to `executor` require a current
|
|
11
|
+
* plan-reviewer `VERDICT: APPROVE` for the task whenever a plan file
|
|
12
|
+
* exists in `.opencode/plans/`, or an explicit user waiver (`USER
|
|
13
|
+
* WAIVER:` literal in the prompt). Plan-file changes invalidate the
|
|
14
|
+
* approval, forcing re-review after a re-plan.
|
|
15
|
+
* - Hard gate: `task` dispatches to `reviewer` require a recorded `GATE: PASS`
|
|
16
|
+
* from test-runner for the current task (a later `GATE: FAIL` un-sets
|
|
17
|
+
* it), or an explicit user waiver (the dispatch prompt must contain the
|
|
18
|
+
* literal marker `USER WAIVER:`).
|
|
19
|
+
* - Round cap: max 2 `task -> reviewer` dispatches per task.
|
|
20
|
+
* - Horizon: read-counter backstop (warn @8, block @15 whole-file reads).
|
|
21
|
+
* - Decomposition: `task -> executor` dispatches must cite `chunk-N` / `AC-n`
|
|
22
|
+
* when a plan file with a `## Decomposition` block exists.
|
|
23
|
+
* - Markers: `GATE:` / `VERDICT:` parsed from task results and child-session
|
|
24
|
+
* assistant text, deduped per task (streaming re-delivery is ignored);
|
|
25
|
+
* marker-less outputs from marker-contracted agents (test-runner/reviewer/
|
|
26
|
+
* plan-reviewer) are journaled as violations; a plan-reviewer result's
|
|
27
|
+
* last `VERDICT` moves the plan-approval gate (parent-keyed, journaled as
|
|
28
|
+
* type `plan`); gate journal entries are keyed by the parent session so
|
|
29
|
+
* replay restores where the gates look.
|
|
30
|
+
* - Trace: JSONL journal of dispatches, gate transitions, verdicts, blocked
|
|
31
|
+
* calls, bash commands, errors.
|
|
32
|
+
* - Decoding: executor/test-runner/debugger temperature clamped ≤ 0.2;
|
|
33
|
+
* reviewer temperature = 0.
|
|
34
|
+
* - Config: layered and read once at setup: code defaults ⊕
|
|
35
|
+
* `~/.config/opencode/turnstile.json` ⊕ env (`TURNSTILE_TRACE`,
|
|
36
|
+
* `TURNSTILE_METRICS_DIR`, `TURNSTILE_CONFIG`) ⊕ injected options (tests).
|
|
37
|
+
* Invalid keys are ignored and journaled (fail-open); restart to apply.
|
|
38
|
+
*
|
|
39
|
+
* Fail-open by design: every hook body is guarded; an internal error is
|
|
40
|
+
* journaled and the tool call proceeds. Removing this file leaves the
|
|
41
|
+
* pipeline functional (prompt contracts alone).
|
|
42
|
+
*
|
|
43
|
+
* Module layout: one boundary per file; this file is the thin entry point
|
|
44
|
+
* the host loads, holding only the module shape and setup composition:
|
|
45
|
+
* - `src/types.ts`: host wire shapes and gate-state structures.
|
|
46
|
+
* - `src/journal.ts`: telemetry persistence (JSONL journal + trace).
|
|
47
|
+
* - `src/state.ts`: per-session gate state, session mapping, journal replay.
|
|
48
|
+
* - `src/markers.ts`: GATE/VERDICT parsing and dedup.
|
|
49
|
+
* - `src/gates.ts`: enforcement decisions (plan-approval gate, reviewer
|
|
50
|
+
* gate, read horizon, decomposition).
|
|
51
|
+
* - `src/setup.ts`: hook registration and the stream-event loop.
|
|
52
|
+
*/
|
|
53
|
+
import { createTurnstile } from "./src/setup.ts"
|
|
54
|
+
import type { Host, TurnstileOptions } from "./src/types.ts"
|
|
55
|
+
|
|
56
|
+
/** The async setup function a plugin instance hands to the host. */
|
|
57
|
+
export type SetupFn = (h: Host) => Promise<(() => void) | void>
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates a turnstile plugin bound to the given host context.
|
|
61
|
+
* @param host The OpenCode host context, or null in degraded loads.
|
|
62
|
+
* @param opts Injected config/env overrides (tests); defaults to env.
|
|
63
|
+
* @returns The plugin's setup function; resolves to a cleanup disposer.
|
|
64
|
+
*/
|
|
65
|
+
export const turnstile = (host: Host | undefined | null, opts?: TurnstileOptions): SetupFn =>
|
|
66
|
+
createTurnstile(host, opts)
|
|
67
|
+
|
|
68
|
+
/** The module object the OpenCode host loads. */
|
|
69
|
+
const module: {
|
|
70
|
+
id: string
|
|
71
|
+
server: () => Promise<Record<string, never>>
|
|
72
|
+
setup: (ctx: Host, opts?: TurnstileOptions) => Promise<(() => void) | void>
|
|
73
|
+
} = {
|
|
74
|
+
id: "turnstile",
|
|
75
|
+
// Legacy v1 shape kept for loaders that require `server`; the active v2
|
|
76
|
+
// path is `setup` (same pattern as the working orca-opencode-status plugin).
|
|
77
|
+
server: async (): Promise<Record<string, never>> => ({}),
|
|
78
|
+
setup: async (ctx: Host, opts?: TurnstileOptions): Promise<(() => void) | void> =>
|
|
79
|
+
turnstile(ctx, opts)(ctx),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default module
|