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 +208 -93
- package/dist/cli.js +4 -4
- package/dist/index.js +6 -6
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -2,7 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/sibyl-system) 
|
|
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`**
|
|
14
|
-
|
|
15
|
-
- **`sibyl_audit`** — the outside lane as a product organ: completion
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
41
|
+
## Session hygiene
|
|
24
42
|
|
|
25
|
-
Council voters, swarm workers and the swarm judge run as **child sessions of
|
|
26
|
-
calling session** (`parentID` from the tool context): the TUI session
|
|
27
|
-
lists root sessions only, so no
|
|
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
|
-
##
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 (
|
|
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
|
|
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? }` |
|
|
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 |
|
|
129
|
-
| `modelPolicy` | `{ allowedPrefixes: ["local-"] }` |
|
|
130
|
-
| `chamber` | see below |
|
|
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
|
-
|
|
150
|
-
|
|
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
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
- A reply that still doesn't parse after its single repair shot
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
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
|
|
190
|
-
|
|
191
|
-
`launch-refusal.json` written into the private run dir — a pinned id claiming
|
|
192
|
-
tool launch that never staged its finding is a provenance lie, not an
|
|
193
|
-
The bare CLI lane (no `--run-id`) keeps the named `UNVERIFIABLE`
|
|
194
|
-
chamber runner regenerates its run record, that
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
|
223
|
-
unreachable to group/other regardless of umask
|
|
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
|
-
│
|
|
243
|
-
│
|
|
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/
|
|
312
|
+
├── lane/ isolated opencode-run launcher (env pinning,
|
|
249
313
|
│ argv-only spawn, pidfile-only kill) + seating policy prefilter
|
|
250
|
-
├── chamber/
|
|
251
|
-
│ -> drawn judge, bounded rounds) +
|
|
252
|
-
│ fail-closed, dissent sealed by path+hash)
|
|
253
|
-
├── exam/
|
|
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
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
|
267
|
-
├── util.ts
|
|
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
|
-
##
|
|
338
|
+
## Relationship to other multi-agent tools
|
|
274
339
|
|
|
275
|
-
|
|
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
|
|
278
|
-
share the problem space. `sibyl_swarm` is a
|
|
279
|
-
PLAN→MINT→DISPATCH→AGGREGATE pipeline over
|
|
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;
|
|
286
|
-
npm run
|
|
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
|
|
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
|
|
317
|
-
|
|
318
|
-
|
|
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,
|
|
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
|
|
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.
|
|
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.
|
|
45
|
+
"esbuild": "^0.28.2",
|
|
45
46
|
"typescript": "^5"
|
|
46
47
|
},
|
|
47
48
|
"bin": {
|