sibyl-system 1.6.0 → 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.
Files changed (2) hide show
  1. package/README.md +205 -98
  2. package/package.json +1 -1
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,23 +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.
226
- - Supply-chain seating (v1.6.0): actions SHA-pinned, CI installs with
227
- `--ignore-scripts`, weekly scheduled `npm audit` witnessed server-side by the
228
- pre-publish liveness gate (fresh green run bound to the released commit).
229
- Honest status ledger of every vector, open or closed: [OPEN-VECTORS.md](OPEN-VECTORS.md).
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).
230
287
  - Engine sessions this product spawns are retained under bounded ages
231
288
  (`npm run reap`, dry-run by default; audit conveners >= 90 days per owner
232
- ruling, reconciliation face preserved in the sealed manifest).
289
+ ruling, reconciliation data preserved in the sealed manifest).
233
290
 
234
291
  ## Architecture (`src/`)
235
292
 
@@ -246,52 +303,57 @@ index.ts plugin entry: parse options → share one RunStore + client
246
303
  ├── council/ councilor personas + tallyVotes (majority2of3 default,
247
304
  │ unanimous exported); pure aggregation, zero IO
248
305
  ├── state/ RunStore: atomic RMW runs.json, per-run space dirs;
249
- │ v1.1 chamber records: EOF-append ledger (A5), face-last
250
- │ regeneration, CHECKSUMS + spotcheck (A4)
306
+ │ chamber records: EOF-append ledger, final report
307
+ │ regenerated last, CHECKSUMS + spotcheck
251
308
  ├── swarm/ planner (ARCHITECT schema) → minter (deterministic worker
252
309
  │ roster) → dispatcher (dependency waves, stagger,
253
310
  │ suspend-on-rate-limit, resume) → aggregate (report +
254
311
  │ verdict derivation; full drafts never inlined)
255
- ├── lane/ v1.1 E1/E4: isolated opencode-run launcher (L1 env pinning,
312
+ ├── lane/ isolated opencode-run launcher (env pinning,
256
313
  │ argv-only spawn, pidfile-only kill) + seating policy prefilter
257
- ├── chamber/ v1.1 民主 protocol (evidence -> blind clash -> cross-critique
258
- │ -> drawn judge, bounded rounds) + 集中 synthesis (ONE voice,
259
- │ fail-closed, dissent sealed by path+hash)
260
- ├── 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
261
318
  │ transcript/disk signal grading, canary veto
262
319
  ├── personas.ts registry: 3 councilors + ARCHITECT + judge/repair texts,
263
- │ model slots — the single source the W3 face hashes
264
- ├── terminal.ts W2 terminal-row grammar (ok|error|timeout), parse + the
265
- │ dead-without-record declared-minus-recorded diff
266
- ├── 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
267
325
  │ texts + deterministic rulesHash fold
268
- ├── independence.ts W1 convener-recusal: engine-store chain walk +
326
+ ├── independence.ts rule W1 convener-recusal: engine-store chain walk +
269
327
  │ drafting-evidence scan (INDEPENDENT / NOT-INDEPENDENT /
270
328
  │ UNVERIFIABLE, fail-closed on unreadable)
271
329
  ├── options.ts zod v4 schema + parseOptions (never throws); home of the
272
330
  │ config-default single sources (DEFAULT_TIMEOUT_MS)
273
- ├── cli.ts v1.1 sibyl-chamber bin: run | status | spotcheck | kill
274
- ├── 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
275
333
  └── tools/ sibyl_consult / sibyl_swarm / sibyl_status / sibyl_review
276
334
  + audit primitives (anchor / time probe / attribution)
277
335
  glue + shared helpers (model-slot chain, artifact reader)
278
336
  ```
279
337
 
280
- ## vs. swarm
338
+ ## Relationship to other multi-agent tools
281
339
 
282
- 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
283
344
  cross-agent message bus, and no fleet-orchestration dependency of any kind —
284
- it is a clean-room implementation (~5K lines of product code, comments excluded) that happens to
285
- share the problem space. `sibyl_swarm` is a single tool call driving a
286
- 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.
287
349
 
288
350
  ## Development
289
351
 
290
352
  ```bash
291
353
  npm run typecheck # tsc --noEmit, strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes
292
- npm test # full unit suite, fully offline (no network, no LLM; 497 at v1.6.0 — the run prints the exact count)
354
+ npm test # full unit suite, fully offline (no network, no LLM; the run prints the exact count)
293
355
  npm run reap # session-retention reaper: dry-run manifest by default; -- --apply deletes via the engine API
294
- npm run coverage # offline suite + coverage floor ratchet (lines 55 / branches 80 / funcs 90 — floors only move up; CI door)
356
+ npm run coverage # offline suite + coverage floor ratchet (floors only move up; CI door)
295
357
  npm run build # esbuild bundle → dist/index.js + dist/cli.js (ESM)
296
358
  node smoke/run-smoke.mjs # offline smoke of the shipped surface (see smoke/README.md)
297
359
  ./ship.sh # the full release gate: all of the above + portability scan of the packed tarball
@@ -304,8 +366,11 @@ Live end-to-end evidence (real opencode, real model sessions):
304
366
  planted defect classes; council wall time 95.6 s ≈ slowest single voter
305
367
  (Σ 168.2 s), proving true parallel fan-out.
306
368
  - `.omo/evidence/t11/` (internal working evidence, not shipped) — failure paths, 31/31 assertions: a bad model slot
307
- 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
308
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.)
309
374
  - `smoke/` ships a deterministic offline re-check of the build + entry +
310
375
  status surface for CI use.
311
376
 
@@ -321,6 +386,48 @@ developed under the name MAGI and renamed to Sibyl-System before its first relea
321
386
 
322
387
  MIT
323
388
 
324
- ## Release wheel
325
-
326
- 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sibyl-system",
3
- "version": "1.6.0",
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",