sibyl-system 1.2.2 → 1.3.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 (4) hide show
  1. package/README.md +57 -14
  2. package/dist/cli.js +393 -199
  3. package/dist/index.js +572 -175
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -5,7 +5,8 @@
5
5
  A standalone [opencode](https://opencode.ai) plugin:
6
6
 
7
7
  - **`sibyl_consult`** — a three-voter review council (MELCHIOR / BALTHASAR / CASPER)
8
- that audits an artifact against a goal and returns a **fail-closed 2/3 verdict**.
8
+ that audits an artifact against a goal and returns a **fail-closed verdict**
9
+ (`APPROVE` / `REJECT` / `CANNOT_ANSWER`).
9
10
  - **`sibyl_swarm`** — a lightweight workflow swarm: an ARCHITECT persona plans a
10
11
  dependency-ordered task graph, deterministic workers execute it in waves, and
11
12
  the result is aggregated into a verdict.
@@ -24,8 +25,10 @@ Council voters, swarm workers and the swarm judge run as **child sessions of the
24
25
  calling session** (`parentID` from the tool context): the TUI session picker
25
26
  lists root sessions only, so no sibyl ballot ever clutters your top-level
26
27
  session list. Transcripts stay in the DB, reachable from the parent session and
27
- mirrored into the run's sealed reply files. An empty caller session falls back
28
- to top-level creation. `sibyl_review` roles run fully isolated under /tmp with
28
+ mirrored into the run's sealed reply files. A seatless (CLI/headless) caller
29
+ falls back to top-level creation, and its voter rows are **garbage-collected
30
+ on success** (the reply files are the durable record; failed votes keep their
31
+ DB rows for salvage). `sibyl_review` roles run fully isolated under /tmp with
29
32
  their own HOME/DB (`src/lane/isolated.ts`), never touching yours.
30
33
 
31
34
  ## v1.1 — the review chamber (民主集中制)
@@ -44,6 +47,7 @@ Laws: docs/isolation-laws.md. Mechanism: docs/democratic-centralism.md.
44
47
  node dist/cli.js run --target doc.md --goal "Is this sound?" --model your-provider/your-model
45
48
  node dist/cli.js status --tail 5
46
49
  node dist/cli.js spotcheck <runId>
50
+ node dist/cli.js run --target - --profile exam --scenario scenarios/<id>.json ... # behavioral exam venue (bell preflight mandatory)
47
51
  ```
48
52
 
49
53
  ## Install
@@ -94,7 +98,7 @@ stderr, registers nothing, and returns empty hooks.
94
98
  | Tool | Arguments | Behavior |
95
99
  |------|-----------|----------|
96
100
  | `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. |
97
- | `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`. With `judge: true`, one extra judge pass may replace the derived verdict — an unrecognized or failed judge reply keeps the derived one. |
101
+ | `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. |
98
102
  | `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. |
99
103
  | `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`. |
100
104
  | `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. |
@@ -119,7 +123,7 @@ defaults.
119
123
  | `timeoutMs` | `240000` | Per-child-session timeout (create + prompt share the budget). Integer ≥ 1. |
120
124
  | `concurrencyK` | `4` | Cap on parallel workers per wave (also capped by the schema's own `concurrency`). Integer 1–8. |
121
125
  | `staggerMs` | `2000` | Delay between worker launches within a wave, to avoid rate-limit bursts. Integer ≥ 0. |
122
- | `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. |
126
+ | `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. |
123
127
  | `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. |
124
128
  | `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). |
125
129
 
@@ -140,6 +144,8 @@ fails the release if any vendor model id reappears on a shipped surface.
140
144
 
141
145
  ## State layout
142
146
 
147
+ - **Chamber ledger**: `~/.sibyl/chamber-ledger.jsonl` — append-only, one JSON
148
+ record per line, serial-stamped at append (A5); `sibyl_status` reads its tail.
143
149
  - **Runs file**: `~/.sibyl/runs.json` (Windows: `%USERPROFILE%\.sibyl\runs.json`) —
144
150
  one record per run (id, kind, status, verdict tally, operator notes), co-located
145
151
  with `spaces/` under the one `~/.sibyl` state root, so npm upgrades (which ship a
@@ -158,16 +164,45 @@ fails the release if any vendor model id reappears on a shipped surface.
158
164
  Plainly stated:
159
165
 
160
166
  - Approval needs **≥ 2 of 3 approvals and zero error/missing votes**.
161
- Anything else — including 2A + 1 error or 2A + 1 missing — is **REJECT**.
167
+ - When the council genuinely spoke (all three ballots substantive, or ≥ 2
168
+ substantive rejects) and approval was not reached, the verdict is **REJECT**.
169
+ - When error/missing seats leave no decision (2A + 1 error, 2A + 1 missing,
170
+ a tie with an error seat), the verdict is **CANNOT_ANSWER** — a wounded
171
+ instrument, NOT a substantive reject, and it can never approve. Repair the
172
+ infra and re-run (since v1.2; pinned by `test/council.test.ts`).
162
173
  - Error votes, missing votes, and malformed verdicts all count against approval.
163
174
  - A reply that still doesn't parse after its single repair shot becomes a
164
175
  0-confidence REJECT ballot with reason `verdict-unparseable: …`.
165
176
  - There is no lenient mode, by design.
166
177
 
178
+ ## Ballot-integrity laws (W1–W4, drafted 2026-10-04/05 after the campaign postmortems; shipped in v1.3.0)
179
+
180
+ - **Convener recusal (W1).** Every consult/swarm/review run resolves the
181
+ convening execution chain through the engine's own session store
182
+ (`session.get` parent links — never a self-report) and scans it for drafting
183
+ evidence of the artifact (write/edit parts targeting it, or content matches).
184
+ A kinship hit stamps the run `independence=NOT-INDEPENDENT`: the ballot is
185
+ preserved as data but is not in any effective path. A chain that cannot be
186
+ read stamps `UNVERIFIABLE` — unreadable is never silently clean.
187
+ - **Absence is a disability (W2).** Before any tally is honored, every declared
188
+ voter/worker must have left a terminal row (`ok|error|timeout` + detail) in
189
+ the run's `TERMINALS.txt`. A declared seat without its row is counted
190
+ `dead-without-record=N` on the first receipt line and forces `CANNOT_ANSWER` —
191
+ never folded into an approve or a reject. A worker session ending with empty
192
+ content is a failure, not a done ballot.
193
+ - **The ruler rides the ballot (W3).** Each terminal record embeds
194
+ `instrument: {rulesHash, components}` — the sha256 of every persona prompt /
195
+ criteria text the build actually sends (councilors, architect, judge, repair
196
+ grammar, chamber roles). `rulesHash` flips with any ruler edit, so
197
+ "re-evaluate under the new rules" is reproducible and comparable; the
198
+ `rules=<12hex>` label rides the receipt face and `sibyl_status` lines.
199
+
167
200
  ## Security notes
168
201
 
169
202
  - Consult/swarm send the artifact text to your configured model pool providers.
170
- - The plugin writes only under its state paths: the runs file and the per-run space.
203
+ - The plugin writes under its state paths (the runs file and the per-run space) and,
204
+ for chamber/exam lanes, sealed run directories under `lane.runRoot`
205
+ (default `/tmp`) — role homes, transcripts, pidfiles, CHECKSUMS.
171
206
  - Voter/worker turns run with `bash`, `edit`, and `write` disabled per prompt.
172
207
 
173
208
  ## Architecture (`src/`)
@@ -184,7 +219,9 @@ index.ts plugin entry: parse options → share one RunStore + client
184
219
  │ validation → exactly one repair → fail-closed REJECT
185
220
  ├── council/ councilor personas + tallyVotes (majority2of3 default,
186
221
  │ unanimous exported); pure aggregation, zero IO
187
- ├── state/ RunStore: atomic RMW runs.json, per-run space dirs
222
+ ├── state/ RunStore: atomic RMW runs.json, per-run space dirs;
223
+ │ v1.1 chamber records: EOF-append ledger (A5), face-last
224
+ │ regeneration, CHECKSUMS + spotcheck (A4)
188
225
  ├── swarm/ planner (ARCHITECT schema) → minter (deterministic worker
189
226
  │ roster) → dispatcher (dependency waves, stagger,
190
227
  │ suspend-on-rate-limit, resume) → aggregate (report +
@@ -196,9 +233,15 @@ index.ts plugin entry: parse options → share one RunStore + client
196
233
  │ fail-closed, dissent sealed by path+hash)
197
234
  ├── exam/ v1.1 E3: scenario-as-data behavioral exams, mechanical
198
235
  │ transcript/disk signal grading, canary veto
199
- ├── state/ RunStore + v1.1 chamber records: EOF-append ledger (A5),
200
- │ face-last regeneration, CHECKSUMS + spotcheck (A4)
201
- ├── personas.ts registry: 3 councilors + ARCHITECT, model slots
236
+ ├── personas.ts registry: 3 councilors + ARCHITECT + judge/repair texts,
237
+ │ model slots — the single source the W3 face hashes
238
+ ├── terminal.ts W2 terminal-row grammar (ok|error|timeout), parse + the
239
+ │ dead-without-record declared-minus-recorded diff
240
+ ├── instrument.ts W3 ruler face: sha256 set of the live prompt/criteria
241
+ │ texts + deterministic rulesHash fold
242
+ ├── independence.ts W1 convener-recusal: engine-store chain walk +
243
+ │ drafting-evidence scan (INDEPENDENT / NOT-INDEPENDENT /
244
+ │ UNVERIFIABLE, fail-closed on unreadable)
202
245
  ├── options.ts zod v4 schema + parseOptions (never throws)
203
246
  ├── cli.ts v1.1 sibyl-chamber bin: run | status | spotcheck | kill
204
247
  └── tools/ sibyl_consult / sibyl_swarm / sibyl_status / sibyl_review
@@ -210,7 +253,7 @@ index.ts plugin entry: parse options → share one RunStore + client
210
253
 
211
254
  This is **not** the oh-my-openagent swarm. It has no `team_*` tools, no
212
255
  cross-agent message bus, and no fleet-orchestration dependency of any kind —
213
- it is a clean-room implementation (~3.5K lines of product code, comments excluded) that happens to
256
+ it is a clean-room implementation (~5K lines of product code, comments excluded) that happens to
214
257
  share the problem space. `sibyl_swarm` is a single tool call driving a
215
258
  PLAN→MINT→DISPATCH→AGGREGATE pipeline over ordinary child sessions.
216
259
 
@@ -218,7 +261,7 @@ PLAN→MINT→DISPATCH→AGGREGATE pipeline over ordinary child sessions.
218
261
 
219
262
  ```bash
220
263
  npm run typecheck # tsc --noEmit, strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes
221
- npm test # 346 unit tests, fully offline (no network, no LLM)
264
+ npm test # full unit suite, fully offline (no network, no LLM; ~420 at v1.3.0 — the run prints the exact count)
222
265
  npm run build # esbuild bundle → dist/index.js + dist/cli.js (ESM)
223
266
  node smoke/run-smoke.mjs # offline smoke of the shipped surface (see smoke/README.md)
224
267
  ./ship.sh # the full release gate: all of the above + portability scan of the packed tarball
@@ -250,4 +293,4 @@ MIT
250
293
 
251
294
  ## Release wheel
252
295
 
253
- 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.
296
+ 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). Regression-locked in `test/hook-prepush.test.ts` (ref lines fed via stdin).