opencode-turnstile 0.2.0 → 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/src/types.ts CHANGED
@@ -16,6 +16,14 @@ export interface TaskInput {
16
16
  description?: unknown
17
17
  command?: unknown
18
18
  cmd?: unknown
19
+ /**
20
+ * Target path as delivered by the host's `write`/`edit` tools. The shape is
21
+ * unconfirmed on v2.0.27, hence `unknown`; `src/tool-policy.ts` reads it as a
22
+ * documented candidate (`filePath ?? path`) and fails open when absent.
23
+ */
24
+ filePath?: unknown
25
+ /** Alternate candidate key for the same write/edit target path. */
26
+ path?: unknown
19
27
  }
20
28
 
21
29
  /** Tool result as delivered by the host's `execute.after` hook. */
@@ -29,6 +37,14 @@ export interface ToolResult {
29
37
  /** Payload of the `tool.execute.before` and `tool.execute.after` hooks. */
30
38
  export interface ToolHookEvent {
31
39
  tool?: unknown
40
+ /**
41
+ * Dispatched subagent/agent name, observed on v2.0.27
42
+ * execute.before/after payloads. The host contract does not type this
43
+ * field, hence `unknown`; `src/setup.ts` reads it (against the configured
44
+ * agent-name set) to skip injecting orchestrator policy into child
45
+ * sessions.
46
+ */
47
+ agent?: unknown
32
48
  sessionID?: unknown
33
49
  input?: TaskInput
34
50
  result?: ToolResult
@@ -37,6 +53,15 @@ export interface ToolHookEvent {
37
53
  /** Payload of the `session.prompt` hook. */
38
54
  export interface PromptHookProperties {
39
55
  sessionID?: unknown
56
+ /**
57
+ * Candidate key for the raw user prompt text on v2.0.27. The host does not
58
+ * type this field, hence `unknown`; `src/setup.ts` reads it (against the
59
+ * candidate `text ?? prompt`) to capture user-authored waiver/approval markers
60
+ * at prompt admission. Fails open when absent.
61
+ */
62
+ text?: unknown
63
+ /** Alternate candidate key for the raw user prompt text. */
64
+ prompt?: unknown
40
65
  }
41
66
 
42
67
  /** Generation draft passed to the `session.generate` hook. */
@@ -67,6 +92,23 @@ export interface EventProperties {
67
92
  text?: unknown
68
93
  delta?: unknown
69
94
  error?: unknown
95
+ /**
96
+ * Host request id on `permission.asked`; the matched id on `permission.replied`.
97
+ * Present only on the permission stream events (opencode v2.0.27+).
98
+ */
99
+ id?: unknown
100
+ requestID?: unknown
101
+ /**
102
+ * A human reply to a permission ask: `"once"`, `"always"`, or `"reject"`.
103
+ * Present only on `permission.replied`.
104
+ */
105
+ reply?: unknown
106
+ /**
107
+ * The ask message echoed by the host on `permission.asked`. Read to
108
+ * recognize plan-gate asks (the evaluate handler stamps a known prefix);
109
+ * unknown-typed, hence `unknown`.
110
+ */
111
+ message?: unknown
70
112
  }
71
113
 
72
114
  /** A single host stream event delivered by `event.subscribe`. */
@@ -76,15 +118,169 @@ export interface StreamEvent {
76
118
  properties?: EventProperties
77
119
  }
78
120
 
121
+ /**
122
+ * Evidence-correlation mode for the reviewer gate. `enforce` (default) refuses
123
+ * to flip `hasGatePass` on a `GATE: PASS` with no correlated exit-0
124
+ * bash/shell test execution; `flag` flips but still stamps `verified`;
125
+ * `off` behaves exactly as before (no correlation).
126
+ */
127
+ export type GateEvidenceMode = "enforce" | "flag" | "off"
128
+
129
+ /**
130
+ * Enforcement mode for the tool-call denylist policy in `src/tool-policy.ts`.
131
+ * `enforce` (default) blocks matching bash/shell commands and write/edit paths;
132
+ * `shadow` journals the would-block without throwing (adoption path); `off` is
133
+ * a silent no-op escape hatch. Mirrors `GateEvidenceMode`.
134
+ */
135
+ export type ToolPolicyMode = "enforce" | "shadow" | "off"
136
+
137
+ /**
138
+ * Denylist policy governing the tools that flow through `execute.before`:
139
+ * bash/shell commands and write/edit target paths. Defaults encode the
140
+ * git-protective prose of `src/policy.ts` verbatim in `bashDeny` and leave
141
+ * `pathDeny` empty (ordinary editing is covered — a decision is made and, on
142
+ * block, journaled — with no default blocks, so it never breaks until a user
143
+ * configures `pathDeny`). Regex sources are case-insensitive, compiled once at
144
+ * setup, and matched with `RegExp.test`.
145
+ */
146
+ export interface TurnstileToolPolicy {
147
+ /** Enforcement mode: `enforce` (default), `shadow`, or `off`. */
148
+ mode: ToolPolicyMode
149
+ /** Case-insensitive regex sources matched against bash/shell commands. */
150
+ bashDeny: string[]
151
+ /** Case-insensitive regex sources matched against write/edit target paths. */
152
+ pathDeny: string[]
153
+ /** Journal `session_move` dispatches (never blocks). */
154
+ observeSessionMove: boolean
155
+ }
156
+
157
+ /**
158
+ * Git-and-commit gate configuration backing the mechanical gates in
159
+ * `src/tool-policy.ts` (chunk-7/8). The default `mode` is `"shadow"`: the
160
+ * gates are active but non-blocking (audit-only) until a user flips a key to
161
+ * `"enforce"`, mirroring the tool-policy "adopt shadow first" adoption path.
162
+ *
163
+ * `commitFormat` is a conventional-commits regex SOURCE string (no leading
164
+ * `^`/trailing `$`): callers compile it with `new RegExp("^" + source + "$",
165
+ * "s")`. It accepts one of the default types `{feat,fix,docs,style,refactor,
166
+ * perf,test,build,ci,chore,revert}`, an optional `(scope)`, a required `!` or
167
+ * `:` separator (only one is needed — no `BREAKING CHANGE` trailer), a subject
168
+ * of up to 72 characters on the first line, and one optional following body
169
+ * line.
170
+ */
171
+ export interface TurnstileGitGates {
172
+ /** Enforcement mode: `enforce` (blocks), `shadow` (audit-only, default), or `off` (silent no-op). */
173
+ mode: ToolPolicyMode
174
+ /** Whether `git commit` messages are linted as conventional commits. */
175
+ commitMessages: boolean
176
+ /** Conventional-commit regex SOURCE string (compiled by callers with `"^…$"` + `"s"`). */
177
+ commitFormat: string
178
+ /** Whether pushing/PR-landing is gated on a reviewer `VERDICT: APPROVE`. */
179
+ landingGate: boolean
180
+ }
181
+
79
182
  /** Gate state for one parent session. */
80
183
  export interface GateState {
81
184
  hasGatePass: boolean
185
+ /**
186
+ * Last `GATE: PASS` rejected for lack of correlated test execution (an
187
+ * unverified claim under `enforce`); "" when none. Cleared on a green flip,
188
+ * on `GATE: FAIL`, and on a task reset. Task-scoped: never carried through
189
+ * `mergeAfterReplay`.
190
+ */
191
+ unverifiedPass: string
82
192
  /** True when this task's plan has a current plan-reviewer APPROVE. */
83
193
  hasPlanVerdict: boolean
84
- verdicts: string[]
194
+ /**
195
+ * True once this task's reviewer `execute.after` delivery has carried a
196
+ * `VERDICT: APPROVE` for the parent — the landing gate's release signal.
197
+ * Restart-sticky like `hasPlanVerdict`: restored by journal replay across a
198
+ * restart (chunk-7) and carried one prompt via `mergeAfterReplay`; fresh
199
+ * `false` on an ordinary prompt reset.
200
+ */
201
+ hasReviewApprove: boolean
202
+ /**
203
+ * Latest user-authored prompt text containing the waiver marker; "" when
204
+ * none. Non-empty ⇒ the current task's reviewer/dispatch gates are waived.
205
+ * Task-scoped: cleared on every prompt reset and never carried through
206
+ * `mergeAfterReplay`.
207
+ */
208
+ userWaiver: string
209
+ /**
210
+ * Provenance flag: true once a human (prompt marker or permission reply)
211
+ * rather than the plan-reviewer subagent released the plan gate. Provenance
212
+ * only — `execute.before` never consults it; it is read by the status CLI and
213
+ * trace lines. Fresh `false` on each prompt reset, but restart-restored values
214
+ * survive the first post-restart prompt via `mergeAfterReplay`, exactly like
215
+ * `hasPlanVerdict`.
216
+ */
217
+ humanApproval: boolean
218
+ /**
219
+ * A single-dispatch grant — the matched host `requestID` for a once-granted
220
+ * human approval, keyed per ask-reason so a plan-gate `once` grant (releasing
221
+ * exactly one executor dispatch) can never be consumed by a landing-gate
222
+ * `once` grant (releasing exactly one push) and vice versa: `{ plan }` for the
223
+ * plan gate (consumed by the executor dispatch short-circuit), `{ landing }`
224
+ * for the landing gate (consumed by the bash short-circuit). "" per reason
225
+ * when none. Task-scoped: cleared immediately after the single dispatch
226
+ * consumes it (a stale grant cannot outlive one dispatch) and never carried
227
+ * through `mergeAfterReplay`.
228
+ */
229
+ pendingOnceGrant: {
230
+ /** Plan-gate once grant: the matched requestID, or "" when none. */
231
+ plan: string
232
+ /** Landing-gate once grant: the matched requestID, or "" when none. */
233
+ landing: string
234
+ }
235
+ /**
236
+ * True once this task's first parent task/subagent result delivery has
237
+ * carried the injected orchestrator policy forward into model context.
238
+ * Cleared per task by `ledger.resetState()` on every user prompt, so the
239
+ * policy is re-injected on each task (the intended per-task re-supply,
240
+ * which also re-applies it after compaction).
241
+ */
242
+ policyInjected: boolean
243
+ /**
244
+ * Dedup key → raw session ID that first observed the marker; written
245
+ * alongside `seen.add` by `observeMarker`. Tracks first-sight provenance so
246
+ * a dedup hit from a different raw session can be accounted as cross-child
247
+ * suppression (a different raw session than the stored provenance).
248
+ *
249
+ * Holds gate-kind provenance only: verdict-kind streaming dedup lives in the
250
+ * per-child `childVerdicts` store, not here.
251
+ */
252
+ seenFrom: Map<string, string>
253
+ /**
254
+ * Cross-child suppressions pending attribution on the next emitted
255
+ * `MarkerEvent` (a `GATE:` dedup hit whose raw session differs from the
256
+ * provenance stored in `seenFrom`). Read and zeroed when an event emits; 0 on
257
+ * a fresh bucket. Verdict-kind deliveries use the per-child store instead, so
258
+ * this counter is now gated on gate-kind hits only.
259
+ */
260
+ suppressedPending: number
85
261
  reviewRounds: number
262
+ /**
263
+ * Count of `git push`/PR-landing blocks this task under the landing gate —
264
+ * telemetry that arms the permission-ask escalation on a repeat attempt
265
+ * (chunk-8). Task-scoped: fresh `0` on each prompt reset and never carried
266
+ * through `mergeAfterReplay`.
267
+ */
268
+ landingBlocks: number
86
269
  reads: number
87
270
  seen: Set<string>
271
+ /**
272
+ * Plans-dir signature the approval was issued against; "" means
273
+ * unknown (journal-replayed or pre-stamp) and is accepted once, then
274
+ * stamped, on the next enforcing dispatch.
275
+ */
276
+ plansSignature: string
277
+ /**
278
+ * True while a journal-replayed bucket's unconsumed one-prompt grace is
279
+ * outstanding: the next reset carries hasGatePass/hasPlanVerdict forward
280
+ * instead of wiping them. Cleared by the first reset; re-granted only by
281
+ * journal replay at ledger creation (process restart or plugin reload).
282
+ */
283
+ replayGrace: boolean
88
284
  }
89
285
 
90
286
  /** One first-sight marker observation returned by the marker parser. */
@@ -93,13 +289,50 @@ export interface MarkerEvent {
93
289
  marker: string
94
290
  result?: "PASS" | "FAIL"
95
291
  rounds?: number
292
+ /**
293
+ * Evidence verdict of a `PASS` gate event: true (correlated exit-0 test
294
+ * execution exists for this session) or false (flagged, unverified). Present
295
+ * only on `PASS` gate events when the evidence mode is not `"off"`.
296
+ */
297
+ verified?: boolean
298
+ /** Evidence mode at the time this event was stamped, when `verified` is. */
299
+ mode?: GateEvidenceMode
96
300
  /** Marker matches observed in the same text but not journaled
97
301
  * (latest-wins policy); 0 when this was the only match. */
98
302
  suppressed: number
99
303
  }
100
304
 
305
+ /**
306
+ * The latest `VERDICT:` marker observation for a single child session and the
307
+ * provenance label of the pipeline that carried it. The store is
308
+ * verdict-kind only: only `VERDICT:` markers are stored (never `GATE:`), so a
309
+ * GATE-only child can never suppress a plan-reviewer bare-output violation.
310
+ *
311
+ * It is also the streamed-verdict observation ledger: `recordChildVerdict`
312
+ * returns whether the observation changed (first-record, marker flip, or
313
+ * source change), so a caller can distinguish a flip from an identical
314
+ * re-record without re-reading the store. Gate-kind (`GATE:`) dedup still
315
+ * lives in `GateState.seen`; this store is verdict-kind only.
316
+ */
317
+ export interface ChildVerdict {
318
+ /** Last `VERDICT:` marker observed for the child, or "" when none. */
319
+ marker: string
320
+ /** Pipeline provenance label, e.g. "child-text" or "child-message". */
321
+ source: string
322
+ /**
323
+ * `Date.now()` when this verdict was last recorded; `0` on the empty default.
324
+ *
325
+ * Used by the plausibility join: an unresolved-child verdict is
326
+ * plausible for a parent only when it was recorded at-or-after that parent's
327
+ * most recent plan-reviewer dispatch.
328
+ */
329
+ recordedAt: number
330
+ }
331
+
101
332
  /** Agent names turnstile recognizes in dispatches and generation drafts. */
102
333
  export interface TurnstileAgents {
334
+ /** Planner: first in the pipeline; the primary session's plan gate gates dispatch. */
335
+ planner: string
103
336
  reviewer: string
104
337
  /** Plan reviewer: adversarial review of the implementation plan. */
105
338
  planReviewer: string
@@ -135,6 +368,13 @@ export interface TurnstileConfig {
135
368
  readBlock: number
136
369
  /** Literal marker that waives the reviewer gate when in the prompt. */
137
370
  waiverMarker: string
371
+ /**
372
+ * Literal marker that releases the plan gate via an explicit human
373
+ * approval (chunk-3). Present at config resolution so the key is valid and
374
+ * the digest is stable; not yet consumed by the chunk-2 prompt-channel
375
+ * waiver. Defaults to `PLAN APPROVED:`.
376
+ */
377
+ approvalMarker: string
138
378
  /** Project-relative directory scanned for `## Decomposition` plans. */
139
379
  plansDir: string
140
380
  /** Whether the human-readable trace file is written (JSONL journal always). */
@@ -145,6 +385,36 @@ export interface TurnstileConfig {
145
385
  gateMarker: string
146
386
  /** Verdict marker regex source (compiled with `gm`). */
147
387
  verdictMarker: string
388
+ /**
389
+ * Evidence-correlation mode for the reviewer gate: `enforce` (default)
390
+ * requires a correlated exit-0 bash/shell test execution before a
391
+ * `GATE: PASS` flips the gate; `flag` flips yet stamps `verified`; `off`
392
+ * keeps the pre-evidence behavior. See `src/evidence.ts`.
393
+ */
394
+ gateEvidence: GateEvidenceMode
395
+ /**
396
+ * Case-insensitive regex source a successful (exit-0) bash/shell command
397
+ * must match to count as test execution. Overridable for non-JS/other
398
+ * toolchains; the default covers npm/pnpm/yarn/bun/deno/node --test/
399
+ * pytest/make test/go test/cargo test.
400
+ */
401
+ evidencePattern: string
402
+ /**
403
+ * Denylist policy for bash/shell and write/edit tools in `execute.before`.
404
+ * Default `mode` is `enforce`; the default `bashDeny` encodes the
405
+ * git-protective prose of `src/policy.ts`, `pathDeny` defaults empty, and
406
+ * `observeSessionMove` journals `session_move` dispatches without blocking.
407
+ * See `src/tool-policy.ts`.
408
+ */
409
+ toolPolicy: TurnstileToolPolicy
410
+ /**
411
+ * Git-and-commit gate configuration. Default `mode` is `shadow` (audit-only
412
+ * until a user opts into `enforce`), `commitMessages` lints `git commit`
413
+ * messages against the conventional-commits `commitFormat`, and `landingGate`
414
+ * gates push/PR-landing on a reviewer `VERDICT: APPROVE`. See
415
+ * `src/tool-policy.ts`.
416
+ */
417
+ gitGates: TurnstileGitGates
148
418
  }
149
419
 
150
420
  /** Options accepted by the plugin factory; tests inject config and env here. */
@@ -173,4 +443,33 @@ export type Host = {
173
443
  event?: {
174
444
  subscribe?: (options?: { signal?: AbortSignal }) => AsyncIterable<StreamEvent>
175
445
  }
446
+ permission?: {
447
+ hook?: (
448
+ name: string,
449
+ callback: (event: unknown) => unknown | Promise<unknown>,
450
+ ) => Promise<unknown>
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Properties carried by a host `permission.evaluate` request at the wire
456
+ * boundary. Every field is optional and typed `unknown`: the host does not
457
+ * expose a stable shape for this payload, so a reshaped or partial payload
458
+ * fails open rather than failing hard. The plugin matches on `action` (the
459
+ * host's task/subagent dispatch name) and `source` rather than the tool input
460
+ * (which this payload does not carry), so escalation surfaces the same decision
461
+ * to a human without leaking the dispatch target to the subagent.
462
+ */
463
+ export interface PermissionEvaluateEvent {
464
+ sessionID?: unknown
465
+ action?: unknown
466
+ resources?: unknown
467
+ metadata?: unknown
468
+ source?: {
469
+ type?: unknown
470
+ messageID?: unknown
471
+ id?: unknown
472
+ }
473
+ effect?: unknown
474
+ message?: unknown
176
475
  }
package/turnstile.ts CHANGED
@@ -1,5 +1,13 @@
1
1
  /**
2
- * turnstile: runtime enforcement of the AGENTS.md pipeline contracts.
2
+ * turnstile: runtime enforcement of the orchestrator pipeline contracts.
3
+ *
4
+ * - Orchestrator policy: injected into the primary (parent) session per task
5
+ * via the `execute.after` hook — the `ORCHESTRATOR_POLICY` block from
6
+ * `src/policy.ts` is prepended to the first `task`/`subagent` result delivery
7
+ * per task, with per-task idempotency via `GateState.policyInjected`, fully
8
+ * guarded and fail-open (a broken injection journals `type:"policy"` and
9
+ * proceeds). Nothing is written to an instructions file at install time — the
10
+ * AGENTS.md contract block is a historical install step, removed.
3
11
  *
4
12
  * Written for OpenCode v2.x promise-plugin host (`{ id, setup }`):
5
13
  * - `ctx.tool.hook("execute.before" | "execute.after", cb)`
@@ -8,27 +16,153 @@
8
16
  *
9
17
  * Enforced mechanically:
10
18
  * - 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.
19
+ * plan approval whenever a plan file exists in `.opencode/plans/`: a
20
+ * plan-reviewer `VERDICT: APPROVE`, a prompt-channel human approval, or a
21
+ * permission-prompt human approval. A prompt-channel approval is a
22
+ * user-authored prompt (a `PLAN APPROVED:` literal in the user's prompt sets
23
+ * the plan verdict and records human provenance) and replays identically to
24
+ * a plan-reviewer verdict. When none holds, the first executor dispatch
25
+ * escalates to the human through the host permission surface:
26
+ * `permission.evaluate` forced `ask` with the plan file + head; a reply
27
+ * `always` sets the verdict, `once` grants exactly one executor dispatch
28
+ * (any other agent's dispatch leaves the grant pending and still runs its
29
+ * own guards), and `reject` keeps the gate red (that single reply is
30
+ * consumed). Replies match only asks escalated as plan-gate asks (recorded
31
+ * per request id, bounded, per session) — a reply to an unrelated permission
32
+ * ask can never release the gate: an ask is recorded only when its message
33
+ * carries the plan-ask prefix, or when the parent's escalation latch is
34
+ * still live (it expires after a few seconds and is cleared by a prompt
35
+ * reset) and the ask carries no message of its own. The explicit
36
+ * user waiver is prompt-channel too (see the hard-gate bullet): a
37
+ * `USER WAIVER:` literal in the user's prompt waives the gate; a
38
+ * `USER WAIVER:` literal in an agent-composed dispatch prompt is inert —
39
+ * issue #34's injection hole, now closed. Plan-content changes invalidate
40
+ * approvals issued against the previous plans-dir signature (replayed
41
+ * approvals are accepted once, then stamped). A journal-replayed approval
42
+ * survives the first prompt after a restart (the resume prompt) and then
43
+ * every later prompt starts a clean cycle.
15
44
  * - Hard gate: `task` dispatches to `reviewer` require a recorded `GATE: PASS`
16
45
  * 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).
46
+ * it), or an explicit user waiver (`USER WAIVER:` literal in the user's
47
+ * prompt). Under the default `gateEvidence:enforce`
48
+ * mode a `GATE: PASS` satisfies the gate only when the current task's
49
+ * bash/shell executions include a matching exit-0 test command (plausibility
50
+ * via `evidencePattern`); an unverified PASS is journaled `verified:false`
51
+ * and does not flip the gate. `flag` degrades to the historical
52
+ * flip-on-trust semantics while still stamping `verified`, and `off`
53
+ * journals without the field — both documented in the README. A
54
+ * journal-replayed `GATE: PASS` survives the first prompt after a restart
55
+ * (the resume prompt) and then every later prompt starts a clean cycle.
56
+ * - Round cap: max 2 `task -> reviewer` dispatches per task, regardless of gate
57
+ * state; a prompt-channel user waiver bypasses the cap without counting
58
+ * toward it (a dispatch prompt with the `USER WAIVER:` marker is inert); a
59
+ * new user prompt resets it.
60
+ * - Parent-bucket eviction: live-parent gate buckets (hasGatePass,
61
+ * hasPlanVerdict, read counter, policyInjected) are registered per
62
+ * prompt/dispatch/delivery/edge via `markParent`, and the `states` LRU never
63
+ * evicts a registered parent; unresolved child buckets remain evictable and
64
+ * can no longer push a live parent out past `maxSessions`.
65
+ * - Horizon: read-counter backstop (warn @8, block @15 whole-file reads),
66
+ * keyed off the parent session: the child→parent edge is awaited
67
+ * (host `session.get` when `session.created` has not yet landed) before the
68
+ * guard fires, so an in-flight child draws down the parent's budget; a
69
+ * still-unresolved child self-keys and journals a bounded signal.
21
70
  * - Decomposition: `task -> executor` dispatches must cite `chunk-N` / `AC-n`
22
71
  * when a plan file with a `## Decomposition` block exists.
72
+ * - Status: `turnstile status` (the `opencode-turnstile status` CLI) reads
73
+ * the journal read-only — it never writes — and reports gate + plan state
74
+ * per parent session: gate flags restored like replay, prompt-channel plan
75
+ * approvals and waiver events (with channel), last markers, and
76
+ * dispatch/block/permission counts (a human table or `--json`).
77
+ * - Tool-call policy: `bash`/`shell` commands and `write`/`edit`
78
+ * target paths are matched in `execute.before` against a configurable
79
+ * denylist (`src/tool-policy.ts`). Under `toolPolicy.mode: enforce` a match
80
+ * throws `BLOCKED by turnstile:`; `shadow` journals the would-block without
81
+ * throwing and `off` is a no-op. Default `bashDeny` blocks the git-protective
82
+ * operations in `src/policy.ts` (worktree, branch `switch`/`checkout`, `merge`,
83
+ * `gh pr create|merge`); `pathDeny` defaults empty. `session_move` is journaled
84
+ * as telemetry only, never blocked. A misconfigured/absent host payload fails
85
+ * open with a journal entry rather than blocking.
86
+ * - Commit-lint: `bash`/`shell` `git commit -m '<subject>'` messages are
87
+ * validated as conventional commits when `gitGates.commitMessages` is on
88
+ * (default) — a `type(scope)!: subject` shape (`type` ∈
89
+ * `{feat,fix,docs,style,refactor,perf,test,build,ci,chore,revert}`, `!`
90
+ * optional, subject ≤ 72 chars). A non-conventional message is
91
+ * `BLOCKED by turnstile: commit message '<subject>' is not a conventional
92
+ * commit — expected type(scope)!: subject` under `gitGates.mode: enforce`
93
+ * (journaled `reason:"invalid-commit-message"`); `shadow` journals only,
94
+ * `off` is a no-op. An uninspectable form (`-F`/`-t`/`--edit`/interactive;
95
+ * `--amend -m` is inspectable) is treated as non-conventional and blocked
96
+ * under `enforce` (`reason:"commit-message-uninspectable"`): pass the subject
97
+ * with `git commit -m <subject>`, not an editor or file.
98
+ * - Landing gate: `git push` with no delivery-attributed reviewer
99
+ * `VERDICT: APPROVE` for the current task is `BLOCKED by turnstile:
100
+ * push/PR requires a reviewer VERDICT: APPROVE before landing — wait for the
101
+ * reviewer or escalate to the user` under `gitGates.mode: enforce` (journaled
102
+ * `reason:"no-landing-approval"`); `shadow` journals only, `off` is a no-op.
103
+ * Approval is set only when the reviewer's task-result delivery carries
104
+ * `VERDICT: APPROVE` (a streamed-only verdict in child text does not
105
+ * satisfy the gate); `gh pr create|merge` stays denied by the default
106
+ * `bashDeny` list (disjointly journaled), and every `git push` is journaled
107
+ * as landing telemetry in every mode.
108
+ * - Landing escalation: on a repeat `git push` with no approval and
109
+ * `gitGates.mode: enforce`, turnstile journals a permission ask
110
+ * (`reason:"landing"`) and lets `execute.before` pass so the
111
+ * `permission.evaluate` handler forces a human ask
112
+ * (`turnstile: push/PR requires a reviewer VERDICT: APPROVE before
113
+ * landing`). The reply `always` stands a reviewer approval for the task,
114
+ * `once` grants exactly one push (consumed by the next push), and `reject`
115
+ * re-blocks and re-arms — the repeat push is the human decision point.
23
116
  * - Markers: `GATE:` / `VERDICT:` parsed from task results and child-session
24
117
  * assistant text, deduped per task (streaming re-delivery is ignored);
25
118
  * 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.
119
+ * plan-reviewer) are journaled as violations; the summary/divergence
120
+ * suppression (a child's streamed verdict hiding a marker-less summarized
121
+ * delivery) is scoped to the plan-reviewer only — reviewer/test-runner
122
+ * violate on a marker-less delivery even when the child's streamed text
123
+ * quoted a VERDICT; the last `VERDICT` for a dispatch moves the plan-approval
124
+ * gate, taken from the task result or, when the host summarized the result,
125
+ * from that child session's assistant text; the journaled `type:"plan"`
126
+ * `source` names the winning pipeline; a delivery with no marker on any
127
+ * source keeps the gate red (parent-keyed, journaled as type `plan`); gate
128
+ * journal entries are keyed by the parent session so replay restores where
129
+ * the gates look.
130
+ * - Verdict attribution: every journaled `type:"plan"` entry is
131
+ * keyed `raw` = an actual child session id, never the parent/session id; a
132
+ * marker-less parent-keyed re-fire can never journal a `{raw: p1}` phantom
133
+ * that clobbers a live verification. Attribution is gated on the delivery's
134
+ * agent being `plan-reviewer`: the child→parent edge is awaited (host
135
+ * `session.get` when `session.created` has not yet landed), then, for any
136
+ * self-keyed delivery (its own id is not a registered child, marker-less or
137
+ * not), candidate discovery attributes it to the unique evidenced registered
138
+ * child (via the child-text verdict store); an own VERDICT: APPROVE with zero
139
+ * evidenced candidates is dropped to the error journal (fail-safe), and zero
140
+ * or more-than-one candidate journals an unattributable `type:"error"` and
141
+ * returns (fail-open, gate unchanged, nothing mutated). Child-text verdict
142
+ * observations dedup per (child, marker, source): identical re-delivery and a
143
+ * cross-source same-marker replay are both suppressed; only a marker flip
144
+ * journals, and a prompt reset drops the previous
145
+ * task's children's evidence (scoped to the reset parent, other parents intact).
146
+ * - Verdict attribution, unresolved edge: a self-keyed delivery whose
147
+ * child→parent edge is unresolved now also claims the unique child evidenced
148
+ * only in the streaming store — plausibility = a recorded plan-reviewer
149
+ * dispatch for the parent predates (at-or-before) the observation's recordedAt,
150
+ * plausibly unique across parents (a verdict with two candidate parents is
151
+ * ambiguous and excluded). Timestamps are last-wins and cleared on prompt reset,
152
+ * so a verdict is plausible only within the parent's current task scope. Every
153
+ * self-keyed delivery journals one `type:"attribution"` entry before any mutation
154
+ * (`{decision, deliveryIsParent, sessionApi, candidates:{edge,unresolved,
155
+ * plausible} counts, unresolvedIDs, lookup:{session:{attempts,reason}}}`) so the
156
+ * field can tell the three root-cause hypotheses apart; the attribution is
157
+ * unanimous-pool driven. A plausible streamed `VERDICT: APPROVE` makes a
158
+ * self-keyed `BLOCK` unreachable (structural fail-open over BLOCK). The
159
+ * edge-resolved branch and the never-BLOCK-on-zero-candidates guarantee are
160
+ * unchanged, so an unattributable parent-keyed delivery stays a single
161
+ * `/unattributable/` error on fail-open paths.
30
162
  * - Trace: JSONL journal of dispatches, gate transitions, verdicts, blocked
31
- * calls, bash commands, errors.
163
+ * calls, bash commands, errors, and the load heartbeat
164
+ * (a "setup" entry: plugin version, config digest, hook count). Errors also
165
+ * cover the stream-end and unresolvable-agent signals; all telemetry only.
32
166
  * - Decoding: executor/test-runner/debugger temperature clamped ≤ 0.2;
33
167
  * reviewer temperature = 0.
34
168
  * - Config: layered and read once at setup: code defaults ⊕
@@ -48,6 +182,8 @@
48
182
  * - `src/markers.ts`: GATE/VERDICT parsing and dedup.
49
183
  * - `src/gates.ts`: enforcement decisions (plan-approval gate, reviewer
50
184
  * gate, read horizon, decomposition).
185
+ * - `src/tool-policy.ts`: stateless denylist matching for bash/shell and
186
+ * write/edit tools in `execute.before`.
51
187
  * - `src/setup.ts`: hook registration and the stream-event loop.
52
188
  */
53
189
  import { createTurnstile } from "./src/setup.ts"