sibyl-system 1.5.1 → 1.6.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
@@ -2,7 +2,22 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/sibyl-system.svg)](https://www.npmjs.com/package/sibyl-system) ![license MIT](https://img.shields.io/badge/license-MIT-green.svg)
4
4
 
5
- A standalone [opencode](https://opencode.ai) plugin:
5
+ A standalone [opencode](https://opencode.ai) plugin that adds independent,
6
+ fail-closed review to your agent workflow: nothing it produces can be approved
7
+ by the party that drafted it, and a missing or broken reviewer can never count
8
+ as consent.
9
+
10
+ ## Why three voters
11
+
12
+ An AI that reviews its own plan is grading its own homework: the same blind
13
+ spots that produced the artifact produced the review. Sibyl's answer is
14
+ structural, not temperamental. Three reviewer personas audit every artifact
15
+ independently and in parallel, and approval needs 2 of 3 votes with zero
16
+ broken seats. Silence, crashes, and unreadable replies count *against*
17
+ approval, never for it. One drafting agent can wear down three independent
18
+ checks; it cannot become all three.
19
+
20
+ What it gives you:
6
21
 
7
22
  - **`sibyl_consult`** — a three-voter review council (MELCHIOR / BALTHASAR / CASPER)
8
23
  that audits an artifact against a goal and returns a **fail-closed verdict**
@@ -10,21 +25,24 @@ A standalone [opencode](https://opencode.ai) plugin:
10
25
  - **`sibyl_swarm`** — a lightweight workflow swarm: an ARCHITECT persona plans a
11
26
  dependency-ordered task graph, deterministic workers execute it in waves, and
12
27
  the result is aggregated into a verdict.
13
- - **`sibyl_review` / `sibyl-chamber`** (v1.1) — a general review chamber with
14
- isolated role sessions and one conclusion spoken in one voice (see below).
15
- - **`sibyl_audit`** — the outside lane as a product organ: completion-state ballots convene themselves from a fresh root session (see Tools).
16
- - **`sibyl_status` + audit primitives** (`sibyl_anchor_check`, `sibyl_time_probe`,
17
- `sibyl_attribute`) — read-only verification tools: run-store view, human-message
18
- anchor re-verification, clock integrity, ref-move attribution.
28
+ - **`sibyl_review` / `sibyl-chamber`** — a general review chamber with isolated
29
+ role sessions and one conclusion spoken in one voice (see below).
30
+ - **`sibyl_audit`** — the outside lane as a product organ: completion reviews
31
+ convene themselves from a fresh root session, so the reviewer is
32
+ structurally unrelated to whoever drafted the artifact (see Tools).
33
+ - **`sibyl_status` + verification primitives** (`sibyl_anchor_check`,
34
+ `sibyl_time_probe`, `sibyl_attribute`) — read-only checks: run-store view,
35
+ re-verification of claimed human orders against the engine's own records,
36
+ clock integrity, commit attribution.
19
37
 
20
38
  Zero coupling to team-mode or fleet orchestration: no `team_*` tools, no
21
39
  cross-agent message bus. Everything runs through plain opencode child sessions.
22
40
 
23
- ## Session hygiene (L1)
41
+ ## Session hygiene
24
42
 
25
- Council voters, swarm workers and the swarm judge run as **child sessions of the
26
- calling session** (`parentID` from the tool context): the TUI session picker
27
- lists root sessions only, so no sibyl ballot ever clutters your top-level
43
+ Council voters, swarm workers and the swarm judge run as **child sessions of
44
+ the calling session** (`parentID` from the tool context): the TUI session
45
+ picker lists root sessions only, so no review vote ever clutters your top-level
28
46
  session list. Transcripts stay in the DB, reachable from the parent session and
29
47
  mirrored into the run's sealed reply files. A seatless (CLI/headless) caller
30
48
  falls back to top-level creation, and its voter rows are **garbage-collected
@@ -32,23 +50,25 @@ on success** (the reply files are the durable record; failed votes keep their
32
50
  DB rows for salvage). `sibyl_review` roles run fully isolated under /tmp with
33
51
  their own HOME/DB (`src/lane/isolated.ts`), never touching yours.
34
52
 
35
- ## v1.1 — the review chamber (民主集中制)
36
-
37
- `sibyl_review` + `sibyl-chamber` is a GENERAL review apparatus: any artifact
38
- (doc, plan, code, agent-behavior exam pack) goes through broad evidence
39
- gathering -> blind PRO/CON clash with cross-critique -> an independently-drawn
40
- judge (E2 pool+seed committed before launch) -> exactly ONE conclusion spoken
41
- in ONE voice (APPROVE / REJECT / NEEDS_HUMAN). Internal dissent is preserved
42
- by path+hash in the sealed run dir, never dumped outward. All role sessions
43
- run isolated under /tmp (zero sessions in your main list); runs are
44
- spotcheckable (`sha256sum -c CHECKSUMS.txt`) and append-only-ledgered.
53
+ ## The review chamber (民主集中制)
54
+
55
+ The name is the design: broad internal deliberation, one unified external
56
+ voice. `sibyl_review` + `sibyl-chamber` is a GENERAL review apparatus: any
57
+ artifact (doc, plan, code, agent-behavior exam pack) goes through broad
58
+ evidence gathering -> blind PRO/CON clash with cross-critique -> an
59
+ independently-drawn judge (the judge pool and draw seed are recorded before
60
+ launch) -> exactly ONE conclusion spoken in ONE voice (APPROVE / REJECT /
61
+ NEEDS_HUMAN). Internal dissent is preserved by path+hash in the sealed run
62
+ dir, never dumped outward. All role sessions run isolated under /tmp (zero
63
+ sessions in your main list); runs are spotcheckable
64
+ (`sha256sum -c CHECKSUMS.txt`) and append-only-ledgered.
45
65
  Laws: docs/isolation-laws.md. Mechanism: docs/democratic-centralism.md.
46
66
 
47
67
  ```
48
68
  node dist/cli.js run --target doc.md --goal "Is this sound?" --model your-provider/your-model
49
69
  node dist/cli.js status --tail 5
50
70
  node dist/cli.js spotcheck <runId>
51
- node dist/cli.js run --target - --profile exam --scenario scenarios/<id>.json ... # behavioral exam venue (bell preflight mandatory)
71
+ node dist/cli.js run --target - --profile exam --scenario scenarios/<id>.json ... # behavioral exam venue (a venue self-check must pass before any exam run)
52
72
  ```
53
73
 
54
74
  ## Install
@@ -96,13 +116,17 @@ stderr, registers nothing, and returns empty hooks.
96
116
 
97
117
  ## Tools
98
118
 
119
+ Throughout: a *receipt* here means a machine-verifiable result record —
120
+ identifiers and hashes you can re-check against the engine's own store, not a
121
+ transcription.
122
+
99
123
  | Tool | Arguments | Behavior |
100
124
  |------|-----------|----------|
101
125
  | `sibyl_consult` | `{ artifact, goal }` | `artifact` is a file path or inline multi-line text (≤ 256 KiB). The three councilors audit it in parallel; each reply is parsed into a verdict (one in-session JSON-only repair shot per voter). Returns the tally, merged reasons/must-fix, run id, and per-voter reply file paths. |
102
- | `sibyl_audit` | `{ artifact, goal }` | Independent audit lane: the council is convened by a FRESH engine-provisioned root session (structurally non-kin to any drafting chain), so the ballot enters the effective path without the caller's identity touching it. Receipt: auditor session id (retained, DB-reconcilable), artifact sha256 bound at start and end (mid-flight drift => VOID), verdict face; anything but INDEPENDENT on the fresh convener => REFUSED, no receipt. |
103
- | `sibyl_swarm` | `{ artifact, goal, judge? }` | ARCHITECT decomposes goal + artifact into a strict-JSON workflow schema; workers are minted deterministically and dispatched in dependency waves; drafts land in the run's space dir. Verdict: `APPROVE` / `REJECT` / `EXHAUSTED`, forced to `CANNOT_ANSWER` when a declared worker left no terminal row (W2) or zero drafts were produced. With `judge: true`, one extra judge pass may replace the derived verdict — an unrecognized or failed judge reply keeps the derived one. |
126
+ | `sibyl_audit` | `{ artifact, goal }` | Independent audit lane: the council is convened by a FRESH engine-provisioned root session (structurally unrelated to any drafting chain), so the review enters the effective path without the caller's identity touching it. Receipt: auditor session id (retained, DB-reconcilable), artifact sha256 bound at start and end (mid-flight drift => VOID), and the verdict; anything but INDEPENDENT on the fresh convener => REFUSED, no receipt. |
127
+ | `sibyl_swarm` | `{ artifact, goal, judge? }` | ARCHITECT decomposes goal + artifact into a strict-JSON workflow schema; workers are minted deterministically and dispatched in dependency waves; drafts land in the run's space dir. Verdict: `APPROVE` / `REJECT` / `EXHAUSTED`, forced to `CANNOT_ANSWER` when a declared worker left no terminal row (rule W2 below) or zero drafts were produced. With `judge: true`, one extra judge pass may replace the derived verdict — an unrecognized or failed judge reply keeps the derived one. |
104
128
  | `sibyl_status` | `{ runId? }` | Read-only. Lists all recorded runs (newest last) + the last chamber-ledger rows, or shows one run's full record and space dir. |
105
- | `sibyl_review` | `{ target, goal, seed?, maxRounds? }` | v1.1 general democratic-centralism chamber: launches the isolated evidence→clash→judge pipeline detached and returns a receipt (explicitly NOT a verdict); the single voice lands in the run record at terminal state. CLI twin: `sibyl-chamber`. |
129
+ | `sibyl_review` | `{ target, goal, seed?, maxRounds? }` | General democratic-centralism chamber: launches the isolated evidence→clash→judge pipeline detached and returns a receipt (explicitly NOT a verdict); the single voice lands in the run record at terminal state. CLI twin: `sibyl-chamber`. |
106
130
  | `sibyl_anchor_check` | `{ sessionId, index?, body?, expectSha256?, label? }` | Read-only verifier for "human anchor" claims (an order/approval message the owner says they sent): re-checks the claim against the engine's own session store instead of trusting a transcribed string. Closed 4-state verdict `MATCH` / `MISMATCH` / `ABSENT` / `ERROR`; the receipt carries a hash of the canonical view read, so later store mutation shows up as a view-hash difference. No `expectSha256` claim → non-verdict. |
107
131
  | `sibyl_time_probe` | `{ endpoints?, toleranceMs? }` | Clock-integrity check against independent HTTP `Date` headers (default pool: cloudflare / google / mozilla; sources with the same owner dept collapse to ONE source). Verdict: `SYNCED` / `DRIFT` / `INSUFFICIENT-SOURCES` (< 2 distinct owners is never consent). Default tolerance 300000 ms. `endpoints` JSON is shape-validated and every URL passes the egress gate before any fetch (reserved/internal shapes refused by rule name; blocked = failed receipt, never consent). HEAD only, zero payload, redirects not followed. |
108
132
  | `sibyl_attribute` | `{ moves, commits, roster }` | Pure comparator (zero network/git of its own): attributes git ref-moves to committer identities against a roster. Per-move verdict: `ATTRIBUTED` / `UNATTRIBUTED` (the silent-egress case) / `AMBIGUOUS` (byte-equal roster pair = registry defect, blocks, never picks). Identity matching is byte-exact by design — no normalization. `moves`/`roster` JSON is shape-validated; malformed input is refused by name (entry-indexed), never guessed at. |
@@ -125,9 +149,9 @@ defaults.
125
149
  | `timeoutMs` | `240000` | Per-child-session timeout (create + prompt share the budget). Integer ≥ 1. |
126
150
  | `concurrencyK` | `4` | Cap on parallel workers per wave (also capped by the schema's own `concurrency`). Integer 1–8. |
127
151
  | `staggerMs` | `2000` | Delay between worker launches within a wave, to avoid rate-limit bursts. Integer ≥ 0. |
128
- | `lane` | see below | v1.1 isolated-lane mechanics for `sibyl_review` / `sibyl-chamber` (`src/lane/isolated.ts`). Sub-keys: `runRoot` (string, default `"/tmp"`) — parent dir for run dirs; `opencodeBin` (non-empty string; discovery order: `$OPENCODE_BIN` → `~/.local/bin/opencode` → `/usr/local/bin/opencode` → `/usr/bin/opencode` → bare PATH name `"opencode"`; never empty, a wrong guess fails loud at spawn naming the bin) — the binary launched for each isolated role; `configSource` (default `~/.config/opencode/opencode.jsonc`) — **read-copied** into each role home, never written back; `roleTimeoutMs` (integer ≥ 5000, default `600000`) — per-role wall-clock budget; `examMaskDirs` (array of absolute paths, default `[]`) — dirs tmpfs-masked from exam candidates (grader files, other runs); empty = an unsandboxed venue that says so plainly on the voice face. |
129
- | `modelPolicy` | `{ allowedPrefixes: ["local-"] }` | v1.1 E4/F1 seating allow-list: only pool models whose `providerID/modelID` starts with one of these prefixes may take a seat; everything else is denied at launch. Array of non-empty strings, **at least 1 entry** (an empty list is rejected as a deny-everything typo). Set this to your own machine's providers — the shipped default is a prefix pattern, not a specific model. |
130
- | `chamber` | see below | v1.1 review-chamber seats and rounds. Sub-keys: `maxRounds` (integer 1–8, default `3`) — clash/cross-critique round cap; `roles` (`{ evidence, pro, con, judge }` → pool slot names, each default `"default"`); `judgePool` (non-empty array of slot names, default `["default"]`) — the slots the independent judge may be drawn from (E2 pool+seed committed before launch). |
152
+ | `lane` | see below | Isolated-lane mechanics for `sibyl_review` / `sibyl-chamber` (`src/lane/isolated.ts`). Sub-keys: `runRoot` (string, default `"/tmp"`) — parent dir for run dirs; `opencodeBin` (non-empty string; discovery order: `$OPENCODE_BIN` → `~/.local/bin/opencode` → `/usr/local/bin/opencode` → `/usr/bin/opencode` → bare PATH name `"opencode"`; never empty, a wrong guess fails loud at spawn naming the bin) — the binary launched for each isolated role; `configSource` (default `~/.config/opencode/opencode.jsonc`) — **read-copied** into each role home, never written back; `roleTimeoutMs` (integer ≥ 5000, default `600000`) — per-role wall-clock budget; `examMaskDirs` (array of absolute paths, default `[]`) — dirs tmpfs-masked from exam candidates (grader files, other runs); empty = an unsandboxed venue that says so plainly in its final report. |
153
+ | `modelPolicy` | `{ allowedPrefixes: ["local-"] }` | Model seating allow-list: only pool models whose `providerID/modelID` starts with one of these prefixes may take a seat; everything else is denied at launch. Array of non-empty strings, **at least 1 entry** (an empty list is rejected as a deny-everything typo). Set this to your own machine's providers — the shipped default is a prefix pattern, not a specific model. |
154
+ | `chamber` | see below | Review-chamber seats and rounds. Sub-keys: `maxRounds` (integer 1–8, default `3`) — clash/cross-critique round cap; `roles` (`{ evidence, pro, con, judge }` → pool slot names, each default `"default"`); `judgePool` (non-empty array of slot names, default `["default"]`) — the slots the independent judge may be drawn from (the pool and draw seed are recorded before launch). |
131
155
 
132
156
  ### Portability (release principle)
133
157
 
@@ -146,65 +170,92 @@ fails the release if any vendor model id reappears on a shipped surface.
146
170
 
147
171
  ## State layout
148
172
 
149
- - **Chamber ledger**: `~/.sibyl/chamber-ledger.jsonl` — append-only, one JSON
150
- record per line, serial-stamped at append (A5); `sibyl_status` reads its tail.
173
+ All persistent data lives under one directory, `~/.sibyl` (created on first
174
+ use; on Windows `%USERPROFILE%\.sibyl`). The only place outside it the plugin
175
+ writes is the isolated run root for chamber/exam lanes (`lane.runRoot`,
176
+ default `/tmp`, defined under Options) — sealed per-run directories there.
177
+
178
+ - **Chamber ledger**: `~/.sibyl/chamber-ledger.jsonl` — append-only history, one JSON
179
+ record per line, each run stamped with a per-run sequence number that
180
+ increments with every appended run; `sibyl_status` reads its tail.
151
181
  - **Runs file**: `~/.sibyl/runs.json` (Windows: `%USERPROFILE%\.sibyl\runs.json`) —
152
- one record per run (id, kind, status, verdict tally, operator notes), co-located
182
+ one record per run (id, kind, artifact, goal, status, verdict, timestamps,
183
+ plus notes the tool records at run milestones, shown by `sibyl-chamber status`),
184
+ co-located
153
185
  with `spaces/` under the one `~/.sibyl` state root, so npm upgrades (which ship a
154
186
  fresh, versioned package directory) never wipe your run history. Written
155
187
  atomically (tmp + rename; the parent directory is created on demand).
156
- *Note: 1.0.x-era runs recorded under the old package-local `<repo>/.state` path
157
- are not migrated (pre-adoption by design) — the store starts empty.*
158
188
  - **Per-run space**: `~/.sibyl/spaces/<runId>/` (Windows: `%USERPROFILE%\.sibyl\spaces`) — full voter replies
159
189
  (`MELCHIOR.md`, …) for consults, worker drafts (`<workerId>.draft.md`) for swarms.
160
- - **Test seam**: `SIBYL_STATE_FILE` env var overrides the runs-file path.
161
- - `load()` never throws: a missing or corrupt file recovers to an empty list
162
- (with a stderr warning); malformed individual entries are dropped, not fatal.
190
+ - **Test/CI override**: the `SIBYL_STATE_FILE` env var points **the runs file**
191
+ at a custom path (its scope is exactly that file — the chamber ledger and
192
+ `spaces/` keep their real paths, since the ledger is the audit trail and is
193
+ deliberately not silently relocatable; CI runners get a fresh home dir
194
+ anyway).
195
+ - `load()` never throws: a missing or corrupt runs file recovers to an empty
196
+ list (with a stderr warning) and the next write replaces the corrupt file —
197
+ so if run history matters, back up `~/.sibyl`; the chamber ledger is a
198
+ separate append-only file and is untouched by that recovery. Malformed
199
+ individual entries are dropped, not fatal.
163
200
 
164
201
  ## Fail-closed policy
165
202
 
166
- Plainly stated:
167
-
168
- - Approval needs **≥ 2 of 3 approvals and zero error/missing votes**.
169
- - When the council genuinely spoke (all three ballots substantive, or ≥ 2
170
- substantive rejects) and approval was not reached, the verdict is **REJECT**.
171
- - When error/missing seats leave no decision (2A + 1 error, 2A + 1 missing,
172
- a tie with an error seat), the verdict is **CANNOT_ANSWER** — a wounded
173
- instrument, NOT a substantive reject, and it can never approve. Repair the
174
- infra and re-run (since v1.2; pinned by `test/council.test.ts`).
175
- - Error votes, missing votes, and malformed verdicts all count against approval.
176
- - A reply that still doesn't parse after its single repair shot becomes a
177
- 0-confidence REJECT ballot with reason `verdict-unparseable: …`.
178
- - There is no lenient mode, by design.
179
-
180
- ## Ballot-integrity laws (W1–W4, drafted 2026-10-04/05 after the campaign postmortems; shipped in v1.3.0)
203
+ Plainly stated — every combination lands in exactly one state:
204
+
205
+ - **APPROVE** needs ≥ 2 genuine approvals **and** zero error/missing votes.
206
+ With three voters that means exactly 3A or 2A+1R.
207
+ - **REJECT** when the council answered: all three voted genuinely but approval
208
+ wasn't reached, or there are ≥ 2 genuine rejects regardless of the third
209
+ seat (2R+1error is still a REJECT — the "no" is already decisive).
210
+ - **CANNOT_ANSWER** for every other mix (2A+1error, 1A+1R+1error, …): the
211
+ mechanism itself is broken. That is NOT a "no", and it can never be treated
212
+ as a "yes": repair the broken part and run again.
213
+ - A reply that still doesn't parse after its single repair shot is **not** a
214
+ reject ballot — it marks that seat as an error, which blocks approval and
215
+ can force CANNOT_ANSWER. There is no path where a broken reply helps
216
+ anything pass.
217
+ - Each ballot also carries a self-reported `confidence` (0..1): recorded for
218
+ the human reader, never used in the tally.
219
+ - The pinned default policy is `majority2of3`; stricter policies exist (see
220
+ Architecture), never looser ones. There is no lenient mode, by design.
221
+ All of the above is pinned by `test/council.test.ts`.
222
+
223
+ ## Ballot-integrity laws
224
+
225
+ These rules exist because real failures happened while this product was in
226
+ daily internal use (the incident records themselves live outside the shipped
227
+ package). Four of them govern the ballot itself; three carry the short names
228
+ `W1`–`W3` used across the codebase and the docs, while the fourth — refused
229
+ launches leaving records — arose from a specific council ruling and carries
230
+ no short name. A fifth rule, `W4`, governs releases instead and is stated
231
+ where it applies, in the Release process section at the end of this document.
181
232
 
182
233
  - **Convener recusal (W1).** Every consult/swarm/review run resolves the
183
234
  convening execution chain through the engine's own session store
184
235
  (`session.get` parent links — never a self-report) and scans it for drafting
185
236
  evidence of the artifact (write/edit parts targeting it, or content matches).
186
- A kinship hit stamps the run `independence=NOT-INDEPENDENT`: the ballot is
237
+ A kinship hit marks the run `independence=NOT-INDEPENDENT`: the ballot is
187
238
  preserved as data but is not in any effective path. A chain that cannot be
188
239
  read stamps `UNVERIFIABLE` — unreadable is never silently clean.
189
- - **Refused launches leave records (v1.4.0, council 16a2):** an explicit `--run-id`
190
- whose tool-staged `launch-context.json` is missing or malformed is REFUSED, with a
191
- `launch-refusal.json` written into the private run dir — a pinned id claiming a
192
- tool launch that never staged its finding is a provenance lie, not an absence.
193
- The bare CLI lane (no `--run-id`) keeps the named `UNVERIFIABLE` leg, and once the
194
- chamber runner regenerates its run record, that face outvouches any contradicted
195
- staged claim ("record wins").
240
+ - **Refused launches leave records.** An explicit `--run-id` whose tool-staged
241
+ `launch-context.json` is missing or malformed is REFUSED, with a
242
+ `launch-refusal.json` written into the private run dir — a pinned id claiming
243
+ a tool launch that never staged its finding is a provenance lie, not an
244
+ absence. The bare CLI lane (no `--run-id`) keeps the named `UNVERIFIABLE`
245
+ leg, and once the chamber runner regenerates its run record, that record
246
+ outranks any contradicted staged claim ("record wins").
196
247
  - **Absence is a disability (W2).** Before any tally is honored, every declared
197
248
  voter/worker must have left a terminal row (`ok|error|timeout` + detail) in
198
249
  the run's `TERMINALS.txt`. A declared seat without its row is counted
199
250
  `dead-without-record=N` on the first receipt line and forces `CANNOT_ANSWER` —
200
251
  never folded into an approve or a reject. A worker session ending with empty
201
252
  content is a failure, not a done ballot.
202
- - **The ruler rides the ballot (W3).** Each terminal record embeds
253
+ - **The scoring rules travel with the vote (W3).** Each terminal record embeds
203
254
  `instrument: {rulesHash, components}` — the sha256 of every persona prompt /
204
255
  criteria text the build actually sends (councilors, architect, judge, repair
205
- grammar, chamber roles). `rulesHash` flips with any ruler edit, so
256
+ grammar, chamber roles). `rulesHash` flips with any rule edit, so
206
257
  "re-evaluate under the new rules" is reproducible and comparable; the
207
- `rules=<12hex>` label rides the receipt face and `sibyl_status` lines.
258
+ `rules=<12hex>` label rides every result line and `sibyl_status` output.
208
259
 
209
260
  ## Security notes
210
261
 
@@ -213,16 +264,29 @@ advisory), the full data-egress map, and the honestly-named limitations.
213
264
  Summary:
214
265
 
215
266
  - Consult/swarm send the artifact text to your configured model pool providers.
216
- - `sibyl_time_probe` fetches nothing before the URL passes the egress gate
217
- (v1.5.0): reserved/internal address shapes, single-label hosts, non-http(s)
218
- schemes, userinfo and non-default ports are refused by rule name; a blocked
219
- source is a failed receipt, never consent.
267
+ - `sibyl_time_probe` fetches nothing before the URL passes the egress gate:
268
+ reserved/internal address shapes, single-label hosts, non-http(s) schemes,
269
+ userinfo and non-default ports are refused by rule name; a blocked source is
270
+ a failed receipt, never consent.
220
271
  - The plugin writes under its state paths (the runs file and the per-run space) and,
221
272
  for chamber/exam lanes, sealed run directories under `lane.runRoot`
222
- (default `/tmp`), every plugin-created dir `0700` (mkdir mode + explicit chmod:
223
- unreachable to group/other regardless of umask — v1.4.0 private-dir law) —
273
+ (default `/tmp`), every plugin-created dir `0700` (mkdir mode + explicit
274
+ chmod: unreachable to group/other regardless of umask) —
224
275
  role homes, transcripts, pidfiles, CHECKSUMS.
225
276
  - Voter/worker turns run with `bash`, `edit`, and `write` disabled per prompt.
277
+ - Supply chain: CI actions are SHA-pinned, installs run with
278
+ `--ignore-scripts`, and a weekly `npm audit`
279
+ (`.github/workflows/scheduled-audit.yml`, which also accepts repo
280
+ workflow_dispatch runs) is witnessed server-side by the pre-publish
281
+ liveness gate (a green audit run, recent within the gate's pinned
282
+ freshness window and bound to the exact commit being released — the
283
+ judgment table and the operative defaults are locked together in
284
+ `test/publish-gate.test.ts`, which imports the defaults from
285
+ `scripts/publish-gate.mjs`). Honest status ledger of every vector,
286
+ open or closed: [OPEN-VECTORS.md](OPEN-VECTORS.md).
287
+ - Engine sessions this product spawns are retained under bounded ages
288
+ (`npm run reap`, dry-run by default; audit conveners >= 90 days per owner
289
+ ruling, reconciliation data preserved in the sealed manifest).
226
290
 
227
291
  ## Architecture (`src/`)
228
292
 
@@ -239,51 +303,57 @@ index.ts plugin entry: parse options → share one RunStore + client
239
303
  ├── council/ councilor personas + tallyVotes (majority2of3 default,
240
304
  │ unanimous exported); pure aggregation, zero IO
241
305
  ├── state/ RunStore: atomic RMW runs.json, per-run space dirs;
242
- │ v1.1 chamber records: EOF-append ledger (A5), face-last
243
- │ regeneration, CHECKSUMS + spotcheck (A4)
306
+ │ chamber records: EOF-append ledger, final report
307
+ │ regenerated last, CHECKSUMS + spotcheck
244
308
  ├── swarm/ planner (ARCHITECT schema) → minter (deterministic worker
245
309
  │ roster) → dispatcher (dependency waves, stagger,
246
310
  │ suspend-on-rate-limit, resume) → aggregate (report +
247
311
  │ verdict derivation; full drafts never inlined)
248
- ├── lane/ v1.1 E1/E4: isolated opencode-run launcher (L1 env pinning,
312
+ ├── lane/ isolated opencode-run launcher (env pinning,
249
313
  │ argv-only spawn, pidfile-only kill) + seating policy prefilter
250
- ├── chamber/ v1.1 民主 protocol (evidence -> blind clash -> cross-critique
251
- │ -> drawn judge, bounded rounds) + 集中 synthesis (ONE voice,
252
- │ fail-closed, dissent sealed by path+hash)
253
- ├── exam/ v1.1 E3: scenario-as-data behavioral exams, mechanical
314
+ ├── chamber/ deliberation protocol (evidence -> blind clash ->
315
+ │ cross-critique -> drawn judge, bounded rounds) + synthesis
316
+ │ (one voice, fail-closed, dissent sealed by path+hash)
317
+ ├── exam/ scenario-as-data behavioral exams, mechanical
254
318
  │ transcript/disk signal grading, canary veto
255
319
  ├── personas.ts registry: 3 councilors + ARCHITECT + judge/repair texts,
256
- │ model slots — the single source the W3 face hashes
257
- ├── terminal.ts W2 terminal-row grammar (ok|error|timeout), parse + the
258
- │ dead-without-record declared-minus-recorded diff
259
- ├── instrument.ts W3 ruler face: sha256 set of the live prompt/criteria
320
+ │ model slots — the single source of every scoring-rule
321
+ │ text (rule W3 hashes these)
322
+ ├── terminal.ts rule W2 terminal-row grammar (ok|error|timeout), parse +
323
+ │ the dead-without-record declared-minus-recorded diff
324
+ ├── instrument.ts rule W3 ruler: sha256 set of the live prompt/criteria
260
325
  │ texts + deterministic rulesHash fold
261
- ├── independence.ts W1 convener-recusal: engine-store chain walk +
326
+ ├── independence.ts rule W1 convener-recusal: engine-store chain walk +
262
327
  │ drafting-evidence scan (INDEPENDENT / NOT-INDEPENDENT /
263
328
  │ UNVERIFIABLE, fail-closed on unreadable)
264
329
  ├── options.ts zod v4 schema + parseOptions (never throws); home of the
265
330
  │ config-default single sources (DEFAULT_TIMEOUT_MS)
266
- ├── cli.ts v1.1 sibyl-chamber bin: run | status | spotcheck | kill
267
- ├── util.ts v1.5.0 single definitions: withTimeout + errMessage
331
+ ├── cli.ts sibyl-chamber bin: run | status | spotcheck | kill
332
+ ├── util.ts single definitions: withTimeout + errMessage
268
333
  └── tools/ sibyl_consult / sibyl_swarm / sibyl_status / sibyl_review
269
334
  + audit primitives (anchor / time probe / attribution)
270
335
  glue + shared helpers (model-slot chain, artifact reader)
271
336
  ```
272
337
 
273
- ## vs. swarm
338
+ ## Relationship to other multi-agent tools
274
339
 
275
- This is **not** the oh-my-openagent swarm. It has no `team_*` tools, no
340
+ If you have seen "swarm"-style toolkits — for example the multi-agent
341
+ orchestration bundled with [oh-my-openagent](https://github.com/code-yeongyu/oh-my-openagent),
342
+ which coordinates long-lived named agents through shared tools and a message
343
+ bus — this is **not** one of those. It has no `team_*` tools, no
276
344
  cross-agent message bus, and no fleet-orchestration dependency of any kind —
277
- it is a clean-room implementation (~5K lines of product code, comments excluded) that happens to
278
- share the problem space. `sibyl_swarm` is a single tool call driving a
279
- PLAN→MINT→DISPATCH→AGGREGATE pipeline over ordinary child sessions.
345
+ it was written from scratch (~5K lines of product code, comments excluded,
346
+ `src/` only) and happens to share the problem space. `sibyl_swarm` is a
347
+ single tool call driving a PLAN→MINT→DISPATCH→AGGREGATE pipeline over
348
+ ordinary child sessions.
280
349
 
281
350
  ## Development
282
351
 
283
352
  ```bash
284
353
  npm run typecheck # tsc --noEmit, strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes
285
- npm test # full unit suite, fully offline (no network, no LLM; 480 at v1.5.0 — the run prints the exact count)
286
- npm run coverage # offline suite + coverage floor ratchet (lines 55 / branches 80 / funcs 90 — floors only move up; CI door)
354
+ npm test # full unit suite, fully offline (no network, no LLM; the run prints the exact count)
355
+ npm run reap # session-retention reaper: dry-run manifest by default; -- --apply deletes via the engine API
356
+ npm run coverage # offline suite + coverage floor ratchet (floors only move up; CI door)
287
357
  npm run build # esbuild bundle → dist/index.js + dist/cli.js (ESM)
288
358
  node smoke/run-smoke.mjs # offline smoke of the shipped surface (see smoke/README.md)
289
359
  ./ship.sh # the full release gate: all of the above + portability scan of the packed tarball
@@ -296,8 +366,11 @@ Live end-to-end evidence (real opencode, real model sessions):
296
366
  planted defect classes; council wall time 95.6 s ≈ slowest single voter
297
367
  (Σ 168.2 s), proving true parallel fan-out.
298
368
  - `.omo/evidence/t11/` (internal working evidence, not shipped) — failure paths, 31/31 assertions: a bad model slot
299
- produced 3 error votes → fail-closed **REJECT 0A/0R/3E**; SIGINT mid-run left
369
+ produced 3 error votes, and the run failed closed; SIGINT mid-run left
300
370
  the store valid with the interrupted run honestly frozen at `running`.
371
+ (The recorded tally predates the judgment-table law pinned in
372
+ `test/council.test.ts`; under today's locked table an all-error ballot is
373
+ classified CANNOT_ANSWER, never REJECT — see the fail-closed policy table.)
301
374
  - `smoke/` ships a deterministic offline re-check of the build + entry +
302
375
  status surface for CI use.
303
376
 
@@ -313,6 +386,48 @@ developed under the name MAGI and renamed to Sibyl-System before its first relea
313
386
 
314
387
  MIT
315
388
 
316
- ## Release wheel
317
-
318
- Tags v* require a packet receipt (docs/release/<ver>.md with a real `gate: PASS` line) and a lease; after cloning run `scripts/install-hooks.sh` — hooks are per-clone and ship empty. **W4 (2026-10-05):** the hook reads the receipt AND `package.json`'s version exclusively from the pushed tag's **object body** (`git show <oid>:<path>`); working-tree reads are forbidden — the tag must carry its own paperwork, and the in-tag version must equal the tag name (ghost-version class killed). **Tag-body hygiene (2026-10-24):** annotated-tag message bodies are scanned on push — home-path and email shapes refuse by rule name (the message is public release content; anchor facts by hash/run-id, not machine paths). Regression-locked in `test/hook-prepush.test.ts` (ref lines fed via stdin).
389
+ ## Release process
390
+
391
+ Publishing a version takes three steps:
392
+
393
+ 1. write a release record (`docs/release/v<version>.md` — named after the tag, v included) listing the verification
394
+ evidence, including a literal `gate: PASS` line;
395
+ 2. take the release lease (`scripts/release-lease.sh`) — a lock file in the
396
+ releaser's own `~/.config/sibyl/` that stops the same maintainer from
397
+ tagging two releases at once (it is a per-user guard, not a distributed
398
+ lock);
399
+ 3. create the tag and push it.
400
+
401
+ Nothing more is done by hand: pushing the tag is the last human step. A CI
402
+ workflow triggered by the tag then re-verifies the paperwork and publishes to
403
+ npm. Enforcement comes in two honest layers:
404
+
405
+ - **Local (fast-fail, bypassable):** a push hook (rule `W4`) checks that the
406
+ tag carries its own proof — it reads the release record and
407
+ `package.json`'s version from the exact files the pushed tag itself points
408
+ at (`git show <oid>:<path>`), never from the working copy, and requires the
409
+ version inside the tag to equal the tag name. Tag messages are scanned too:
410
+ home-directory paths and email-address shapes are refused by rule name,
411
+ because tag messages are public — reference facts by hash or run id.
412
+ The hook also refuses a tag push unless the local release lease (step 2)
413
+ names exactly that version. Hooks are per-clone and must be installed
414
+ (`scripts/install-hooks.sh`), and
415
+ `git push --no-verify` can skip them — they save you a rejected push, they
416
+ are not the door.
417
+ - **Server-side (the door):** the CI publish workflow re-runs the tag
418
+ paperwork — tag name must equal `package.json`'s version, and the release
419
+ record `docs/release/v<version>.md` (tag name, v included) must exist in the tagged tree carrying a
420
+ literal `gate: PASS` line (`scripts/tag-paperwork.mjs`, locked by
421
+ `test/tag-paperwork.test.ts`) — and additionally refuses unless a recent
422
+ security-audit run — scheduled or dispatched by the repo itself — finished
423
+ green on the exact commit being released (the commit binding and the
424
+ pinned freshness window are the "Supply chain" bullet in Security notes).
425
+ Skipping the local hook does not skip this; of the hook's four
426
+ checks, two have no server echo — the tag-message hygiene scan, and the
427
+ lease check, which cannot echo by design (the lease is an operator-local
428
+ per-user file, step 2). The npm publish itself runs under a CI identity
429
+ with provenance, so the registry records exactly which repo commit
430
+ produced the package.
431
+ - All of it is regression-locked in tests (`test/hook-prepush.test.ts` feeds
432
+ the hook the same push information git sends it; `test/publish-gate.test.ts`
433
+ locks the server-side door's judgment table).
package/dist/cli.js CHANGED
@@ -14288,8 +14288,8 @@ function registry() {
14288
14288
  var globalRegistry = globalThis.__zod_globalRegistry;
14289
14289
 
14290
14290
  // node_modules/zod/v4/core/compile.js
14291
- var INVALID = Symbol.for("zod.compile.invalid");
14292
- var FALLBACK_FLAG = Symbol.for("zod.compile.fallback");
14291
+ var INVALID = /* @__PURE__ */ Symbol.for("zod.compile.invalid");
14292
+ var FALLBACK_FLAG = /* @__PURE__ */ Symbol.for("zod.compile.fallback");
14293
14293
  var ZodCompileAsyncError = class extends Error {
14294
14294
  constructor(message = "z.compile does not support async refinements, transforms, or checks") {
14295
14295
  super(message);
@@ -18077,7 +18077,7 @@ var nonoptionalProcessor = (schema, ctx, _json, params) => {
18077
18077
  const seen = ctx.seen.get(schema);
18078
18078
  seen.ref = def.innerType;
18079
18079
  };
18080
- var UNREPRESENTABLE_DEFAULT = Symbol();
18080
+ var UNREPRESENTABLE_DEFAULT = /* @__PURE__ */ Symbol();
18081
18081
  function serializeDefaultValue(value, schema, ctx, json2, params) {
18082
18082
  let unrepresentable = false;
18083
18083
  const serialized = JSON.stringify(value, (_, val) => {
@@ -20841,7 +20841,7 @@ function fromJSONSchema(schema, params) {
20841
20841
  }
20842
20842
 
20843
20843
  // node_modules/zod/v4/core/visit.js
20844
- var RESOLVING = Symbol("z.visit/resolving");
20844
+ var RESOLVING = /* @__PURE__ */ Symbol("z.visit/resolving");
20845
20845
  function visit(schema, fnOrHandlers) {
20846
20846
  const fn = typeof fnOrHandlers === "function" ? fnOrHandlers : (node2, rewritten) => {
20847
20847
  const h = fnOrHandlers[node2._zod.def.type];
package/dist/index.js CHANGED
@@ -12952,8 +12952,8 @@ function registry() {
12952
12952
  var globalRegistry = globalThis.__zod_globalRegistry;
12953
12953
 
12954
12954
  // node_modules/zod/v4/core/compile.js
12955
- var INVALID = Symbol.for("zod.compile.invalid");
12956
- var FALLBACK_FLAG = Symbol.for("zod.compile.fallback");
12955
+ var INVALID = /* @__PURE__ */ Symbol.for("zod.compile.invalid");
12956
+ var FALLBACK_FLAG = /* @__PURE__ */ Symbol.for("zod.compile.fallback");
12957
12957
  var ZodCompileAsyncError = class extends Error {
12958
12958
  constructor(message = "z.compile does not support async refinements, transforms, or checks") {
12959
12959
  super(message);
@@ -16741,7 +16741,7 @@ var nonoptionalProcessor = (schema, ctx, _json, params) => {
16741
16741
  const seen = ctx.seen.get(schema);
16742
16742
  seen.ref = def.innerType;
16743
16743
  };
16744
- var UNREPRESENTABLE_DEFAULT = Symbol();
16744
+ var UNREPRESENTABLE_DEFAULT = /* @__PURE__ */ Symbol();
16745
16745
  function serializeDefaultValue(value, schema, ctx, json2, params) {
16746
16746
  let unrepresentable = false;
16747
16747
  const serialized = JSON.stringify(value, (_, val) => {
@@ -19505,7 +19505,7 @@ function fromJSONSchema(schema, params) {
19505
19505
  }
19506
19506
 
19507
19507
  // node_modules/zod/v4/core/visit.js
19508
- var RESOLVING = Symbol("z.visit/resolving");
19508
+ var RESOLVING = /* @__PURE__ */ Symbol("z.visit/resolving");
19509
19509
  function visit(schema, fnOrHandlers) {
19510
19510
  const fn = typeof fnOrHandlers === "function" ? fnOrHandlers : (node2, rewritten) => {
19511
19511
  const h = fnOrHandlers[node2._zod.def.type];
@@ -21505,7 +21505,7 @@ async function artifactHash(raw, dir) {
21505
21505
  }
21506
21506
  function buildAuditTool(deps) {
21507
21507
  return tool({
21508
- description: "SIBYL independent audit lane: convenes the three-councilor consult from a FRESH engine-provisioned root session (structurally non-kin to any drafting chain), so the ballot enters the effective path without the caller's identity touching it. Emits an audit receipt \u2014 auditor session id (retained, DB-reconcilable), artifact sha256 bound at start and end, verdict face. REFUSES (no receipt) if the fresh convener resolves to anything but INDEPENDENT, or the artifact drifts mid-ballot (VOID).",
21508
+ description: "SIBYL independent audit lane: convenes the three-councilor consult from a FRESH engine-provisioned root session (structurally non-kin to any drafting chain), so the ballot enters the effective path without the caller's identity touching it. Emits an audit receipt \u2014 auditor session id (retained >=90 days, then reconcilable via the sealed reap manifests), artifact sha256 bound at start and end, verdict face. REFUSES (no receipt) if the fresh convener resolves to anything but INDEPENDENT, or the artifact drifts mid-ballot (VOID).",
21509
21509
  args: {
21510
21510
  artifact: tool.schema.string().min(1).describe("Path to the artifact, or its inline content (multi-line)."),
21511
21511
  goal: tool.schema.string().min(1).describe("The question the audit must answer.")
@@ -21556,7 +21556,7 @@ async function auditExecute(deps, args, ctx) {
21556
21556
  return [
21557
21557
  `SIBYL AUDIT RECEIPT run=${runId}`,
21558
21558
  ` verdict=${v?.verdict ?? "unknown"} (${counts}) rules=${rules}`,
21559
- ` auditor-convener=${auditor} (fresh engine-provisioned root; session row RETAINED for DB reconciliation)`,
21559
+ ` auditor-convener=${auditor} (fresh engine-provisioned root; session row retained >=90 days, after which sealed reap manifests carry the reconciliation face)`,
21560
21560
  ` requested-by=${caller} \u2014 recorded as ledger data only; it neither convenes nor votes`,
21561
21561
  ` artifact-sha256=${shaStart} (re-hashed after the ballot, identical \u2014 the receipt binds to these bytes)`,
21562
21562
  "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sibyl-system",
3
- "version": "1.5.1",
3
+ "version": "1.6.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Sibyl-System: democratic-centralism review chamber (one conclusion, one voice) + fail-closed council voting + dynamic swarm plugin for OpenCode",
@@ -33,7 +33,8 @@
33
33
  "build": "esbuild src/index.ts --bundle --format=esm --platform=node --outfile=dist/index.js && esbuild src/cli.ts --bundle --format=esm --platform=node --banner:js='#!/usr/bin/env node' --outfile=dist/cli.js",
34
34
  "test": "node --import ./tools/register-ts.mjs --test test/*.test.ts",
35
35
  "coverage": "node --import ./tools/register-ts.mjs --test --experimental-test-coverage \"--test-coverage-include=src/**\" --test-coverage-lines=55 --test-coverage-branches=80 --test-coverage-functions=90 test/*.test.ts",
36
- "prepack": "npm run build"
36
+ "prepack": "npm run build",
37
+ "reap": "node --no-warnings scripts/session-reap.mjs"
37
38
  },
38
39
  "dependencies": {
39
40
  "@opencode-ai/plugin": "1.19.4",
@@ -41,7 +42,7 @@
41
42
  },
42
43
  "devDependencies": {
43
44
  "@types/node": "^22",
44
- "esbuild": "^0.25.0",
45
+ "esbuild": "^0.28.2",
45
46
  "typescript": "^5"
46
47
  },
47
48
  "bin": {