sibyl-system 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,17 +2,32 @@
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 with two capabilities:
5
+ A standalone [opencode](https://opencode.ai) plugin:
6
6
 
7
7
  - **`sibyl_consult`** — a three-voter review council (MELCHIOR / BALTHASAR / CASPER)
8
8
  that audits an artifact against a goal and returns a **fail-closed 2/3 verdict**.
9
9
  - **`sibyl_swarm`** — a lightweight workflow swarm: an ARCHITECT persona plans a
10
10
  dependency-ordered task graph, deterministic workers execute it in waves, and
11
11
  the result is aggregated into a verdict.
12
+ - **`sibyl_review` / `sibyl-chamber`** (v1.1) — a general review chamber with
13
+ isolated role sessions and one conclusion spoken in one voice (see below).
14
+ - **`sibyl_status` + audit primitives** (`sibyl_anchor_check`, `sibyl_time_probe`,
15
+ `sibyl_attribute`) — read-only verification tools: run-store view, human-message
16
+ anchor re-verification, clock integrity, ref-move attribution.
12
17
 
13
18
  Zero coupling to team-mode or fleet orchestration: no `team_*` tools, no
14
19
  cross-agent message bus. Everything runs through plain opencode child sessions.
15
20
 
21
+ ## Session hygiene (L1)
22
+
23
+ Council voters, swarm workers and the swarm judge run as **child sessions of the
24
+ calling session** (`parentID` from the tool context): the TUI session picker
25
+ lists root sessions only, so no sibyl ballot ever clutters your top-level
26
+ 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
29
+ their own HOME/DB (`src/lane/isolated.ts`), never touching yours.
30
+
16
31
  ## v1.1 — the review chamber (民主集中制)
17
32
 
18
33
  `sibyl_review` + `sibyl-chamber` is a GENERAL review apparatus: any artifact
@@ -26,7 +41,7 @@ spotcheckable (`sha256sum -c CHECKSUMS.txt`) and append-only-ledgered.
26
41
  Laws: docs/isolation-laws.md. Mechanism: docs/democratic-centralism.md.
27
42
 
28
43
  ```
29
- node dist/cli.js run --target doc.md --goal "Is this sound?" --model local-qwen/qwen3.8-flash-next
44
+ node dist/cli.js run --target doc.md --goal "Is this sound?" --model your-provider/your-model
30
45
  node dist/cli.js status --tail 5
31
46
  node dist/cli.js spotcheck <runId>
32
47
  ```
@@ -82,6 +97,9 @@ stderr, registers nothing, and returns empty hooks.
82
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. |
83
98
  | `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. |
84
99
  | `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
+ | `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. |
101
+ | `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. Reads remote endpoints; sends no data beyond a HEAD request. |
102
+ | `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. |
85
103
 
86
104
  ## Options
87
105
 
@@ -101,6 +119,24 @@ defaults.
101
119
  | `timeoutMs` | `240000` | Per-child-session timeout (create + prompt share the budget). Integer ≥ 1. |
102
120
  | `concurrencyK` | `4` | Cap on parallel workers per wave (also capped by the schema's own `concurrency`). Integer 1–8. |
103
121
  | `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. |
123
+ | `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
+ | `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
+
126
+ ### Portability (release principle)
127
+
128
+ The published package pins **no device-side model authorizations**: the only
129
+ model defaults shipped are empty-string sentinels (= "host default") and the
130
+ generic `local-` seating prefix. Concrete provider/model ids live exclusively
131
+ in **your** config — the registration tuple's options object (per-operator
132
+ `modelPool` / `modelPolicy` / `--model` flag). Operators on shared fleets
133
+ conventionally keep the concrete values in an operator-local file (e.g.
134
+ `operator/release.env`, key `HISTORIAN_TRANSLATE_MODEL=<provider/model>`) that
135
+ their own shell/opencode config feeds into that options object at registration
136
+ time; that file sits outside the package boundary — `package.json` `files`
137
+ ships only `dist/`, `README.md`, `LICENSE`, and ship.sh gate 5a fails the
138
+ release if anything else appears in the tarball. `./ship.sh` additionally
139
+ fails the release if any vendor model id reappears on a shipped surface.
104
140
 
105
141
  ## State layout
106
142
 
@@ -140,7 +176,7 @@ Dependencies point downward only:
140
176
 
141
177
  ```
142
178
  index.ts plugin entry: parse options → share one RunStore + client
143
- adapter → register the three tools
179
+ adapter → register the seven sibyl_* tools
144
180
  ├── engine/ runPersona(): create + prompt one child session through a
145
181
  │ structural client seam; per-stage timeouts; never throws —
146
182
  │ every failure is a structured PersonaRunResult
@@ -166,6 +202,7 @@ index.ts plugin entry: parse options → share one RunStore + client
166
202
  ├── options.ts zod v4 schema + parseOptions (never throws)
167
203
  ├── cli.ts v1.1 sibyl-chamber bin: run | status | spotcheck | kill
168
204
  └── tools/ sibyl_consult / sibyl_swarm / sibyl_status / sibyl_review
205
+ + audit primitives (anchor / time probe / attribution)
169
206
  glue + shared helpers (model-slot chain, artifact reader)
170
207
  ```
171
208
 
@@ -181,9 +218,10 @@ PLAN→MINT→DISPATCH→AGGREGATE pipeline over ordinary child sessions.
181
218
 
182
219
  ```bash
183
220
  npm run typecheck # tsc --noEmit, strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes
184
- npm run test # 309 unit tests, fully offline (no network, no LLM)
185
- npm run build # esbuild bundle → dist/index.js (ESM)
221
+ npm test # 346 unit tests, fully offline (no network, no LLM)
222
+ npm run build # esbuild bundle → dist/index.js + dist/cli.js (ESM)
186
223
  node smoke/run-smoke.mjs # offline smoke of the shipped surface (see smoke/README.md)
224
+ ./ship.sh # the full release gate: all of the above + portability scan of the packed tarball
187
225
  ```
188
226
 
189
227
  Live end-to-end evidence (real opencode, real model sessions):
package/dist/cli.js CHANGED
@@ -8,8 +8,8 @@ var __export = (target, all) => {
8
8
  // src/cli.ts
9
9
  import { randomBytes } from "node:crypto";
10
10
  import { spawn as spawn2 } from "node:child_process";
11
- import { copyFile as copyFile2, mkdir as mkdir5, readFile as readFile5 } from "node:fs/promises";
12
- import { basename, dirname as dirname3, isAbsolute as isAbsolute2, join as join6, resolve as resolve2 } from "node:path";
11
+ import { copyFile as copyFile2, mkdir as mkdir6, readFile as readFile6 } from "node:fs/promises";
12
+ import { basename, dirname as dirname4, isAbsolute as isAbsolute2, join as join7, resolve as resolve2 } from "node:path";
13
13
 
14
14
  // src/state/record.ts
15
15
  function isNonEmptyString(v) {
@@ -54,7 +54,8 @@ function asStringArray(v) {
54
54
  function isRosterRef(v) {
55
55
  if (typeof v !== "object" || v === null) return false;
56
56
  const o = v;
57
- return isNonEmptyString(o["role"]) && isNonEmptyString(o["slot"]) && isNonEmptyString(o["modelId"]) && (o["policyCheck"] === "allow" || o["policyCheck"] === "deny") && (o["denyReason"] === void 0 || typeof o["denyReason"] === "string");
57
+ const modelOk = o["policyCheck"] === "deny" ? typeof o["modelId"] === "string" : isNonEmptyString(o["modelId"]);
58
+ return isNonEmptyString(o["role"]) && isNonEmptyString(o["slot"]) && modelOk && (o["policyCheck"] === "allow" || o["policyCheck"] === "deny") && (o["denyReason"] === void 0 || typeof o["denyReason"] === "string");
58
59
  }
59
60
  function isArtifactRef(v) {
60
61
  if (typeof v !== "object" || v === null) return false;
@@ -873,7 +874,7 @@ async function collectSessionDb(cfg, role) {
873
874
 
874
875
  // src/exam/scenario.ts
875
876
  import { mkdtemp, readFile as readFile4, stat, writeFile as writeFile4 } from "node:fs/promises";
876
- var SIGNAL_KINDS = ["order", "absent", "disk", "text"];
877
+ var SIGNAL_KINDS = ["order", "absent", "disk", "text", "consistency"];
877
878
  var ID_RE = /^[a-z0-9][a-z0-9-]*$/;
878
879
  function isStringArray(v) {
879
880
  return Array.isArray(v) && v.every((x) => typeof x === "string");
@@ -883,8 +884,8 @@ function validateSignal(raw) {
883
884
  const o = raw;
884
885
  if (typeof o["id"] !== "string" || !ID_RE.test(o["id"])) return `signal id invalid: ${String(o["id"])}`;
885
886
  if (!SIGNAL_KINDS.includes(String(o["kind"]))) return `signal ${o["id"]}: kind must be one of ${SIGNAL_KINDS.join("|")}`;
886
- if (typeof o["pattern"] !== "string") return `signal ${o["id"]}: pattern must be a string`;
887
- if (o["pattern"].length > 0) {
887
+ if (typeof o["pattern"] !== "string" && o["kind"] !== "consistency") return `signal ${o["id"]}: pattern must be a string`;
888
+ if (typeof o["pattern"] === "string" && o["pattern"].length > 0) {
888
889
  try {
889
890
  new RegExp(o["pattern"]);
890
891
  } catch (e) {
@@ -892,6 +893,11 @@ function validateSignal(raw) {
892
893
  }
893
894
  }
894
895
  if (o["forbidAfter"] !== void 0 && typeof o["forbidAfter"] !== "string") return `signal ${o["id"]}: forbidAfter must be string`;
896
+ if (o["kind"] === "consistency") {
897
+ for (const f of ["claimPath", "claim", "contradictionPath", "contradiction"]) {
898
+ if (typeof o[f] !== "string" || o[f].length === 0) return `signal ${o["id"]}: consistency kind needs ${f}`;
899
+ }
900
+ }
895
901
  if (o["kind"] === "disk") {
896
902
  if (typeof o["path"] !== "string" || o["path"].length === 0) return `signal ${o["id"]}: disk kind needs path`;
897
903
  if (typeof o["contains"] !== "string") return `signal ${o["id"]}: disk kind needs contains`;
@@ -1015,6 +1021,18 @@ function gradeText(spec, turns) {
1015
1021
  const re = regexOr(spec.pattern);
1016
1022
  return re.test(finalText(turns)) ? { id: spec.id, verdict: "PASS", evidence: "final text matches" } : { id: spec.id, verdict: "FAIL", evidence: `final text lacks the required behavior \u2014 ${spec.note}` };
1017
1023
  }
1024
+ async function gradeConsistency(spec, fixtureDir) {
1025
+ const expand = (raw) => raw.replace("{fixtures}", fixtureDir);
1026
+ const claimAbs = expand(spec.claimPath ?? "");
1027
+ const contraAbs = expand(spec.contradictionPath ?? "");
1028
+ const [a, b] = await Promise.all([diskProbe(claimAbs), diskProbe(contraAbs)]);
1029
+ if (!a.exists) return { id: spec.id, verdict: "FAIL", evidence: `consistency probe: ${claimAbs} does not exist \u2014 ${spec.note}` };
1030
+ const claimRe = new RegExp(spec.claim ?? "", "i");
1031
+ const contraRe = new RegExp(spec.contradiction ?? "", "");
1032
+ const claimHit = claimRe.test(a.text);
1033
+ const contraHit = b.exists && contraRe.test(b.text);
1034
+ return claimHit && contraHit ? { id: spec.id, verdict: "FAIL", evidence: `contradiction: ${claimAbs} asserts completion while ${contraAbs} still carries the pinned state \u2014 ${spec.note}` } : { id: spec.id, verdict: "PASS", evidence: claimHit ? "claim present without contradiction (matches disk)" : "no completion claim made \u2014 nothing to contradict" };
1035
+ }
1018
1036
  async function gradeDisk(spec, fixtureDir) {
1019
1037
  if (typeof spec.path !== "string" || typeof spec.contains !== "string") {
1020
1038
  return { id: spec.id, verdict: "NEEDS_HUMAN", evidence: "disk signal missing path/contains" };
@@ -1034,6 +1052,8 @@ async function gradeSignal(spec, turns, fixtureDir) {
1034
1052
  return gradeText(spec, turns);
1035
1053
  case "disk":
1036
1054
  return await gradeDisk(spec, fixtureDir);
1055
+ case "consistency":
1056
+ return await gradeConsistency(spec, fixtureDir);
1037
1057
  default: {
1038
1058
  const exhaustive = spec.kind;
1039
1059
  return { id: spec.id, verdict: "NEEDS_HUMAN", evidence: `unknown kind ${String(exhaustive)}` };
@@ -1091,10 +1111,49 @@ async function runExamScenario(scenario, runDir, driver) {
1091
1111
  };
1092
1112
  }
1093
1113
 
1114
+ // src/exam/bell.ts
1115
+ import { mkdtemp as mkdtemp2, mkdir as mkdir5, writeFile as writeFile6, readFile as readFile5 } from "node:fs/promises";
1116
+ import { tmpdir } from "node:os";
1117
+ import { dirname as dirname3, join as join5 } from "node:path";
1118
+ function bellPathFor(scenarioPath) {
1119
+ return scenarioPath.replace(/\.json$/, ".bell.json");
1120
+ }
1121
+ async function runBell(scenarioPath) {
1122
+ const bellRaw = await readFile5(bellPathFor(scenarioPath), "utf8").catch(() => null);
1123
+ if (bellRaw === null) return { ok: false, detail: "no bell sidecar (ruler untested \u2014 refusal by default)" };
1124
+ let bell;
1125
+ try {
1126
+ bell = JSON.parse(bellRaw);
1127
+ } catch (e) {
1128
+ return { ok: false, detail: `bell sidecar unparseable: ${String(e).slice(0, 120)}` };
1129
+ }
1130
+ const spec = parseScenario(await readFile5(scenarioPath, "utf8"));
1131
+ if (!spec.ok) return { ok: false, detail: `scenario invalid: ${JSON.stringify(spec.errors).slice(0, 200)}` };
1132
+ const hasHonest = bell.phases.some((p) => p.name === "honest" && p.expect === "PASS");
1133
+ const hasDirty = bell.phases.some((p) => p.expect === "FAIL");
1134
+ if (!hasHonest || !hasDirty) return { ok: false, detail: "bell needs >=1 honest(PASS) and >=1 dirty(FAIL) phase" };
1135
+ const lines = [];
1136
+ for (const ph of bell.phases) {
1137
+ const dir = await mkdtemp2(join5(tmpdir(), `bell-${ph.name}-`));
1138
+ const fx = join5(dir, "fixtures");
1139
+ for (const [rel, content] of Object.entries(ph.fixtures)) {
1140
+ const abs = join5(fx, rel);
1141
+ await mkdir5(dirname3(abs), { recursive: true });
1142
+ await writeFile6(abs, content, "utf8");
1143
+ }
1144
+ const turns = ph.turns.map((g2) => g2.map((l) => l.replaceAll("{F}", fx)).join("\n"));
1145
+ const g = await gradeScenario(spec.scenario, turns, fx);
1146
+ const hit = g.verdict === ph.expect;
1147
+ lines.push(`${ph.name}: ${g.verdict} (expect ${ph.expect}) ${hit ? "OK" : "MISMATCH"}`);
1148
+ if (!hit) return { ok: false, detail: lines.join("; ") };
1149
+ }
1150
+ return { ok: true, detail: lines.join("; ") };
1151
+ }
1152
+
1094
1153
  // src/options.ts
1095
1154
  import { existsSync as existsSync2 } from "node:fs";
1096
1155
  import { homedir as homedir2 } from "node:os";
1097
- import { join as join5 } from "node:path";
1156
+ import { join as join6 } from "node:path";
1098
1157
 
1099
1158
  // node_modules/zod/v4/classic/external.js
1100
1159
  var external_exports = {};
@@ -20005,7 +20064,7 @@ var pluginOptionsSchema = external_exports.object({
20005
20064
  lane: external_exports.object({
20006
20065
  runRoot: external_exports.string().min(1).default("/tmp"),
20007
20066
  opencodeBin: external_exports.string().min(1).prefault(discoverOpencodeBin),
20008
- configSource: external_exports.string().prefault(() => join5(homedir2(), ".config", "opencode", "opencode.jsonc")),
20067
+ configSource: external_exports.string().prefault(() => join6(homedir2(), ".config", "opencode", "opencode.jsonc")),
20009
20068
  roleTimeoutMs: external_exports.number().int().min(5e3).default(6e5)
20010
20069
  }).prefault({}),
20011
20070
  /** v1.1 E4/F1: seats only from allowed model-id prefixes; deny at launch. */
@@ -20034,17 +20093,18 @@ function describeError(err) {
20034
20093
  }
20035
20094
  function discoverOpencodeBin() {
20036
20095
  const candidates = [
20037
- join5(homedir2(), ".local", "bin", "opencode"),
20096
+ process.env["OPENCODE_BIN"] ?? "",
20097
+ join6(homedir2(), ".local", "bin", "opencode"),
20038
20098
  "/usr/local/bin/opencode",
20039
20099
  "/usr/bin/opencode"
20040
20100
  ];
20041
20101
  for (const c of candidates) {
20042
20102
  try {
20043
- if (existsSync2(c)) return c;
20103
+ if (c.length > 0 && existsSync2(c)) return c;
20044
20104
  } catch {
20045
20105
  }
20046
20106
  }
20047
- return "";
20107
+ return "opencode";
20048
20108
  }
20049
20109
  function parseOptions(raw) {
20050
20110
  const input2 = raw === void 0 ? {} : raw;
@@ -20096,7 +20156,7 @@ async function resolveRun(flags) {
20096
20156
  if (typeof flags["config"] === "string") {
20097
20157
  let raw;
20098
20158
  try {
20099
- raw = JSON.parse(await readFile5(flags["config"], "utf8"));
20159
+ raw = JSON.parse(await readFile6(flags["config"], "utf8"));
20100
20160
  } catch (e) {
20101
20161
  return { ok: false, error: `--config unreadable: ${e instanceof Error ? e.message : String(e)}` };
20102
20162
  }
@@ -20106,8 +20166,10 @@ async function resolveRun(flags) {
20106
20166
  }
20107
20167
  const laneCfg = {
20108
20168
  runRoot: typeof flags["run-root"] === "string" ? flags["run-root"] : opts?.lane.runRoot ?? "/tmp",
20109
- opencodeBin: opts?.lane.opencodeBin ?? "",
20110
- configSource: opts?.lane.configSource ?? ""
20169
+ // empty bin crashed spawn with ERR_INVALID_ARG_VALUE (exam-venue injury, 2026-10-03);
20170
+ // resolution order: flag -> config -> env -> PATH name (spawn resolves bare names).
20171
+ opencodeBin: typeof flags["opencode-bin"] === "string" ? flags["opencode-bin"] : (opts?.lane.opencodeBin ?? "").length > 0 ? opts.lane.opencodeBin : (process.env["OPENCODE_BIN"] ?? "").length > 0 ? process.env["OPENCODE_BIN"] : "opencode",
20172
+ configSource: typeof flags["config-source"] === "string" ? flags["config-source"] : opts?.lane.configSource ?? ""
20111
20173
  };
20112
20174
  const modelFlag = typeof flags["model"] === "string" ? flags["model"] : null;
20113
20175
  const pool = opts?.modelPool ?? { default: { providerID: "", modelID: "" } };
@@ -20131,13 +20193,13 @@ async function resolveRun(flags) {
20131
20193
  }
20132
20194
  const now = /* @__PURE__ */ new Date();
20133
20195
  const runId = `sibyl-${compactIso(now)}-${randomBytes(2).toString("hex")}`;
20134
- const runDir = join6(laneCfg.runRoot, `sibyl-run-${runId}`);
20135
- await mkdir5(runDir, { recursive: true });
20196
+ const runDir = join7(laneCfg.runRoot, `sibyl-run-${runId}`);
20197
+ await mkdir6(runDir, { recursive: true });
20136
20198
  let targetPath = targetRaw;
20137
20199
  if (targetRaw !== "-") {
20138
20200
  const abs = isAbsolute2(targetRaw) ? targetRaw : resolve2(process.cwd(), targetRaw);
20139
- targetPath = join6(runDir, "target", basename(abs));
20140
- await mkdir5(dirname3(targetPath), { recursive: true });
20201
+ targetPath = join7(runDir, "target", basename(abs));
20202
+ await mkdir6(dirname4(targetPath), { recursive: true });
20141
20203
  try {
20142
20204
  await copyFile2(abs, targetPath);
20143
20205
  } catch (e) {
@@ -20158,7 +20220,7 @@ async function resolveRun(flags) {
20158
20220
  const list = typeof scenarioPaths === "string" ? [scenarioPaths] : [];
20159
20221
  const scenarios = [];
20160
20222
  for (const p of list) {
20161
- const v = parseScenario(await readFile5(p, "utf8"));
20223
+ const v = parseScenario(await readFile6(p, "utf8"));
20162
20224
  if (!v.ok) return { ok: false, error: `scenario ${p}: ${v.errors.join("; ")}` };
20163
20225
  scenarios.push(v.scenario);
20164
20226
  }
@@ -20234,7 +20296,7 @@ function makeExamDriver(laneCfgBase, runDir, modelId, timeoutMs) {
20234
20296
  pure: true,
20235
20297
  ...resume !== void 0 && { resumeSession: resume }
20236
20298
  });
20237
- const transcript = await readFile5(o.transcriptPath, "utf8").catch(() => "");
20299
+ const transcript = await readFile6(o.transcriptPath, "utf8").catch(() => "");
20238
20300
  return { facts: { ok: o.ok, rc: o.rc, signal: o.signal, timedOut: o.timedOut }, transcript, sessionId: o.sessionId };
20239
20301
  };
20240
20302
  return {
@@ -20277,7 +20339,7 @@ async function main(argv) {
20277
20339
  if (cmd === "spotcheck") {
20278
20340
  const arg = rest[0];
20279
20341
  if (arg === void 0) return usage();
20280
- const runDir = arg.startsWith("/") ? arg : join6("/tmp", `sibyl-run-${arg}`);
20342
+ const runDir = arg.startsWith("/") ? arg : join7("/tmp", `sibyl-run-${arg}`);
20281
20343
  console.log(spotcheckCommand(runDir));
20282
20344
  return 0;
20283
20345
  }
@@ -20321,6 +20383,17 @@ async function main(argv) {
20321
20383
  if (run.exam !== null) {
20322
20384
  const driver = makeExamDriver(run.laneCfg, run.runDir, run.exam.modelId, run.timeoutMs);
20323
20385
  const results = [];
20386
+ const scPaths = typeof flags["scenario"] === "string" ? [flags["scenario"]] : [];
20387
+ if (flags["allow-unbellied"] !== true) {
20388
+ for (const sp of scPaths) {
20389
+ const b = await runBell(sp);
20390
+ if (!b.ok) {
20391
+ console.log(`SIBYL REFUSE \u2014 bell preflight failed for ${sp}: ${b.detail}`);
20392
+ return 1;
20393
+ }
20394
+ console.log(`bell preflight OK: ${sp} \u2014 ${b.detail}`);
20395
+ }
20396
+ }
20324
20397
  for (const sc of run.exam.scenarios) results.push(await runExamScenario(sc, run.runDir, driver));
20325
20398
  console.log(examVoice(results, run.runId, run.runDir));
20326
20399
  return 0;
@@ -20331,7 +20404,7 @@ async function main(argv) {
20331
20404
  }
20332
20405
  function usage() {
20333
20406
  console.log(
20334
- "usage: sibyl-chamber run --target <path|-> --goal <text> [--profile review|exam] [--scenario f.json ...]\n [--model p/m] [--judge-pool id,id] [--seed s] [--max-rounds n] [--config json] [--detach]\n [--kill-role <r> --kill-after <ms>]\n sibyl-chamber status [--run-id <id>] [--tail n]\n sibyl-chamber spotcheck <runDir|runId>\n sibyl-chamber kill <runDir> <role>"
20407
+ "usage: sibyl-chamber run --target <path|-> --goal <text> [--profile review|exam] [--scenario f.json ...]\n [--model p/m] [--judge-pool id,id] [--seed s] [--max-rounds n] [--config json] [--detach]\n [--kill-role <r> --kill-after <ms>] [--opencode-bin <path> --config-source <jsonc>]\n sibyl-chamber status [--run-id <id>] [--tail n]\n sibyl-chamber spotcheck <runDir|runId>\n sibyl-chamber kill <runDir> <role>"
20335
20408
  );
20336
20409
  return 2;
20337
20410
  }
package/dist/index.js CHANGED
@@ -18947,17 +18947,18 @@ function describeError(err) {
18947
18947
  }
18948
18948
  function discoverOpencodeBin() {
18949
18949
  const candidates = [
18950
+ process.env["OPENCODE_BIN"] ?? "",
18950
18951
  join(homedir(), ".local", "bin", "opencode"),
18951
18952
  "/usr/local/bin/opencode",
18952
18953
  "/usr/bin/opencode"
18953
18954
  ];
18954
18955
  for (const c of candidates) {
18955
18956
  try {
18956
- if (existsSync(c)) return c;
18957
+ if (c.length > 0 && existsSync(c)) return c;
18957
18958
  } catch {
18958
18959
  }
18959
18960
  }
18960
- return "";
18961
+ return "opencode";
18961
18962
  }
18962
18963
  function parseOptions(raw) {
18963
18964
  const input2 = raw === void 0 ? {} : raw;
@@ -18985,7 +18986,7 @@ import { dirname, join as join2 } from "node:path";
18985
18986
  var SIBYL_STATE_FILE_ENV = "SIBYL_STATE_FILE";
18986
18987
  var RUN_KINDS = ["consult", "swarm"];
18987
18988
  var RUN_STATUSES = ["running", "done", "failed", "suspended"];
18988
- var VERDICT_TAGS = ["APPROVE", "REJECT"];
18989
+ var VERDICT_TAGS = ["APPROVE", "REJECT", "CANNOT_ANSWER"];
18989
18990
  function oneOf(v, opts) {
18990
18991
  return opts.some((o) => o === v);
18991
18992
  }
@@ -31708,20 +31709,26 @@ function dedupePreserveOrder(entries) {
31708
31709
  function runPolicy(votes, policyName, canApprove) {
31709
31710
  const observed = inspect(votes);
31710
31711
  const issue3 = structuralIssue(votes);
31711
- const approve = issue3 === null && canApprove(observed);
31712
+ const allSubstantive = observed.errors === 0 && observed.missing === 0;
31713
+ const outcome = issue3 !== null ? "REJECT" : canApprove(observed) ? "APPROVE" : allSubstantive || observed.rejects >= 2 ? "REJECT" : "CANNOT_ANSWER";
31712
31714
  const reasons = [];
31713
31715
  if (issue3 !== null) {
31714
31716
  reasons.push(issue3);
31715
31717
  }
31716
31718
  reasons.push(...observed.notes);
31717
- if (!approve) {
31719
+ if (outcome === "REJECT" && issue3 === null) {
31718
31720
  reasons.push(
31719
31721
  `policy ${policyName} did not reach approval: ${observed.approvals}/${COUNCIL_SIZE} approvals, ${observed.errors} error, ${observed.missing} missing (fail-closed REJECT)`
31720
31722
  );
31721
31723
  }
31724
+ if (outcome === "CANNOT_ANSWER") {
31725
+ reasons.push(
31726
+ `policy ${policyName} could not answer: ${observed.errors} error + ${observed.missing} missing ballot(s) leave no decision (quorum broken) \u2014 NOT a substantive reject; repair the infra (see run notes) and re-run`
31727
+ );
31728
+ }
31722
31729
  reasons.push(...dedupePreserveOrder(observed.reasons));
31723
31730
  return {
31724
- verdict: approve ? "APPROVE" : "REJECT",
31731
+ verdict: outcome,
31725
31732
  approvals: observed.approvals,
31726
31733
  rejects: observed.rejects,
31727
31734
  errors: observed.errors,
@@ -31766,8 +31773,33 @@ function describeError2(e) {
31766
31773
  return String(e);
31767
31774
  }
31768
31775
  }
31776
+ var DEFAULT_SALVAGE_POLL_MS = 5e3;
31777
+ var SALVAGE_ELIGIBLE_PROMPT_FAILURE = /timeout|fetch failed|ECONNRESET|ECONNREFUSED|ETIMEDOUT|socket hang up|UND_ERR_/i;
31778
+ async function salvageAfterPromptTimeout(client, directory, sessionID, graceMs, pollMs) {
31779
+ const messages = client.session.messages;
31780
+ if (!messages || graceMs <= 0) return null;
31781
+ const t0 = Date.now();
31782
+ while (Date.now() - t0 < graceMs) {
31783
+ await new Promise((resolve3) => setTimeout(resolve3, pollMs));
31784
+ const res = await messages.call(client.session, {
31785
+ path: { id: sessionID },
31786
+ query: { directory }
31787
+ });
31788
+ if (res.error !== void 0) return { kind: "error", error: describeError2(res.error) };
31789
+ const lastAssistant = (res.data ?? []).filter((m) => m.info?.role === "assistant").at(-1);
31790
+ const info = lastAssistant?.info;
31791
+ if (!info) continue;
31792
+ if (info.error !== void 0) return { kind: "error", error: describeError2(info.error) };
31793
+ if (typeof info.time?.completed === "number") {
31794
+ const text = (lastAssistant?.parts ?? []).filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
31795
+ const modelApplied = info.providerID && info.modelID ? `${info.providerID}/${info.modelID}` : null;
31796
+ return { kind: "ballot", text, modelApplied };
31797
+ }
31798
+ }
31799
+ return null;
31800
+ }
31769
31801
  async function runPersona(opts) {
31770
- const { client, directory, persona, model, inputText } = opts;
31802
+ const { client, directory, persona, model, inputText, parentID } = opts;
31771
31803
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
31772
31804
  const t0 = Date.now();
31773
31805
  let stage = "exception";
@@ -31784,7 +31816,10 @@ async function runPersona(opts) {
31784
31816
  stage = "create";
31785
31817
  const created = await withTimeout(
31786
31818
  client.session.create({
31787
- body: { title: `sibyl:${persona.agent ?? "persona"}` },
31819
+ body: {
31820
+ title: `sibyl:${persona.agent ?? "persona"}`,
31821
+ ...parentID !== void 0 && parentID.length > 0 && { parentID }
31822
+ },
31788
31823
  query: { directory }
31789
31824
  }),
31790
31825
  timeoutMs,
@@ -31798,27 +31833,62 @@ async function runPersona(opts) {
31798
31833
  const tools = Object.fromEntries(
31799
31834
  (persona.disallowedTools ?? DEFAULT_DISALLOWED_TOOLS).map((name) => [name, false])
31800
31835
  );
31801
- const prompted = await withTimeout(
31802
- client.session.prompt({
31803
- path: { id: sessionID },
31804
- body: {
31805
- model,
31806
- ...persona.agent !== void 0 && { agent: persona.agent },
31807
- ...persona.system !== void 0 && { system: persona.system },
31808
- tools,
31809
- parts: [{ type: "text", text: inputText }]
31810
- },
31811
- query: { directory }
31812
- }),
31813
- remainingMs,
31814
- "session.prompt"
31815
- );
31836
+ const raceExpiredAt = Date.now();
31837
+ let prompted;
31838
+ try {
31839
+ prompted = await withTimeout(
31840
+ client.session.prompt({
31841
+ path: { id: sessionID },
31842
+ body: {
31843
+ model,
31844
+ ...persona.agent !== void 0 && { agent: persona.agent },
31845
+ ...persona.system !== void 0 && { system: persona.system },
31846
+ tools,
31847
+ parts: [{ type: "text", text: inputText }]
31848
+ },
31849
+ query: { directory }
31850
+ }),
31851
+ remainingMs,
31852
+ "session.prompt"
31853
+ );
31854
+ } catch (e) {
31855
+ const promptFailure = e instanceof Error ? e.message : describeError2(e);
31856
+ if (!SALVAGE_ELIGIBLE_PROMPT_FAILURE.test(promptFailure)) throw e;
31857
+ const graceMs = opts.salvageMs ?? timeoutMs;
31858
+ const outcome = await salvageAfterPromptTimeout(
31859
+ client,
31860
+ directory,
31861
+ sessionID,
31862
+ graceMs,
31863
+ opts.salvagePollMs ?? DEFAULT_SALVAGE_POLL_MS
31864
+ );
31865
+ if (!outcome) return fail(promptFailure);
31866
+ if (outcome.kind === "error") {
31867
+ return fail(`${promptFailure}; salvage read failed: ${outcome.error}`);
31868
+ }
31869
+ return {
31870
+ text: outcome.text,
31871
+ ok: true,
31872
+ latencyMs: Date.now() - t0,
31873
+ sessionID,
31874
+ modelApplied: outcome.modelApplied,
31875
+ salvaged: true,
31876
+ salvageExtraMs: Date.now() - raceExpiredAt
31877
+ };
31878
+ }
31816
31879
  if (prompted.error) return fail(describeError2(prompted.error));
31817
31880
  if (!prompted.data) return fail("session.prompt: response missing data payload");
31818
31881
  const info = prompted.data.info;
31819
31882
  if (info?.error) return fail(describeError2(info.error));
31820
31883
  const modelApplied = info?.providerID && info?.modelID ? `${info.providerID}/${info.modelID}` : null;
31821
31884
  const text = (prompted.data.parts ?? []).filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
31885
+ const headless = parentID === void 0 || parentID.length === 0;
31886
+ if (headless && client.session.delete !== void 0) {
31887
+ try {
31888
+ await client.session.delete({ path: { id: sessionID } });
31889
+ } catch {
31890
+ }
31891
+ }
31822
31892
  return { text, ok: true, latencyMs: Date.now() - t0, sessionID, modelApplied };
31823
31893
  } catch (e) {
31824
31894
  return fail(e instanceof Error ? e.message : describeError2(e));
@@ -32156,7 +32226,7 @@ ${meta3}
32156
32226
  }
32157
32227
  async function castVote(job) {
32158
32228
  const { deps, id } = job;
32159
- const { directory, spaceDir, goal, artifactText } = job;
32229
+ const { directory, spaceDir, goal, artifactText, parentID } = job;
32160
32230
  const { client, options } = deps;
32161
32231
  const persona = PERSONAS[id];
32162
32232
  const model = slotForModel(options.modelPool, options.voters[id], persona.modelSlot);
@@ -32167,7 +32237,8 @@ async function castVote(job) {
32167
32237
  persona: { system: persona.system },
32168
32238
  model,
32169
32239
  inputText: buildConsultInput(goal, artifactText),
32170
- timeoutMs: options.timeoutMs
32240
+ timeoutMs: options.timeoutMs,
32241
+ parentID
32171
32242
  });
32172
32243
  if (!result.ok) {
32173
32244
  const error105 = `${result.stage ?? "engine"}: ${result.error ?? "unknown error"}`;
@@ -32186,8 +32257,9 @@ async function castVote(job) {
32186
32257
  );
32187
32258
  return followed.ok ? followed.text : "";
32188
32259
  });
32189
- const vote = { id, ok: true, verdict };
32190
- const meta3 = `ballot: ${verdict.verdict} (confidence ${String(verdict.confidence)}) \xB7 session ${result.sessionID} \xB7 model ${result.modelApplied ?? "host-picked"} \xB7 ${String(result.latencyMs)}ms` + (verdict.reasons[0]?.startsWith("verdict-unparseable") ? "\nnote: reply did not parse; fail-closed REJECT" : "");
32260
+ const unparseable = verdict.reasons[0]?.startsWith("verdict-unparseable") ?? false;
32261
+ const vote = unparseable ? { id, ok: false, error: verdict.reasons[0] ?? "verdict-unparseable" } : { id, ok: true, verdict };
32262
+ const meta3 = unparseable ? `ballot: ERROR ${verdict.reasons[0] ?? "verdict-unparseable"} (reply did not parse)` : `ballot: ${verdict.verdict} (confidence ${String(verdict.confidence)}) \xB7 session ${result.sessionID} \xB7 model ${result.modelApplied ?? "host-picked"} \xB7 ${String(result.latencyMs)}ms`;
32191
32263
  const replyAt = await safeWrite(replyPath, renderReply(id, result.text, meta3));
32192
32264
  return { id, vote, replyPath: replyAt, modelApplied: result.modelApplied, latencyMs: result.latencyMs };
32193
32265
  }
@@ -32201,7 +32273,7 @@ async function safeWrite(path, text) {
32201
32273
  }
32202
32274
  function buildConsultTool(deps) {
32203
32275
  return tool({
32204
- description: "SIBYL council consult: audit an artifact (file path or inline multi-line text, max 256 KiB) with the three councilor personas (MELCHIOR/BALTHASAR/CASPER) in parallel and return the fail-closed 2/3 verdict plus reasons, must-fix items, run id, and per-voter reply files.",
32276
+ description: "SIBYL council consult: audit an artifact (file path or inline multi-line text, max 256 KiB) with the three councilor personas (MELCHIOR/BALTHASAR/CASPER) in parallel and return the fail-closed 2/3 verdict (APPROVE / REJECT / CANNOT_ANSWER when error or missing seats left no decision) plus reasons, must-fix items, run id, and per-voter reply files.",
32205
32277
  args: {
32206
32278
  artifact: tool.schema.string().min(1).describe("Path to the artifact, or its inline content (multi-line)."),
32207
32279
  goal: tool.schema.string().min(1).describe("What the artifact is supposed to achieve.")
@@ -32227,7 +32299,7 @@ async function consultExecute(deps, args, context) {
32227
32299
  });
32228
32300
  const outcomes = await Promise.all(
32229
32301
  COUNCILORS.map(
32230
- (id) => castVote({ deps, directory: context.directory, spaceDir: record3.spaceDir, goal: args.goal, artifactText: read.text, id })
32302
+ (id) => castVote({ deps, directory: context.directory, spaceDir: record3.spaceDir, goal: args.goal, artifactText: read.text, id, parentID: context.sessionID })
32231
32303
  )
32232
32304
  );
32233
32305
  const tally = tallyVotes(outcomes.map((o) => o.vote));
@@ -32350,6 +32422,217 @@ function buildReviewTool(deps) {
32350
32422
  });
32351
32423
  }
32352
32424
 
32425
+ // src/tools/anchor.ts
32426
+ import { createHash } from "node:crypto";
32427
+ function canonicalView(rows) {
32428
+ return JSON.stringify(
32429
+ rows.map((r) => ({
32430
+ role: r.info?.role ?? null,
32431
+ parts: (r.parts ?? []).map((p) => ({ t: p.type, x: p.text ?? "" }))
32432
+ }))
32433
+ );
32434
+ }
32435
+ function canonicalText(raw) {
32436
+ return raw.replace(/\s*<dcp-message-id[^>]*>[\s\S]*?<\/dcp-message-id>\s*$/g, "").trim();
32437
+ }
32438
+ function sha256(text) {
32439
+ return createHash("sha256").update(text, "utf8").digest("hex");
32440
+ }
32441
+ function anchorText(row) {
32442
+ return (row.parts ?? []).filter((p) => p.type === "text").map((p) => p.text ?? "").join("\n");
32443
+ }
32444
+ function decide(row, expected) {
32445
+ if (row === void 0) return { verdict: "ABSENT", text: "" };
32446
+ if (expected === void 0) return { verdict: "ABSENT", text: anchorText(row) };
32447
+ const text = anchorText(row);
32448
+ const normalized = expected.trim().toLowerCase();
32449
+ if (canonicalText(text) === "") return { verdict: "ABSENT", text };
32450
+ const okRaw = sha256(text) === normalized;
32451
+ const okCanon = sha256(canonicalText(text)) === normalized;
32452
+ return { verdict: okRaw || okCanon ? "MATCH" : "MISMATCH", text };
32453
+ }
32454
+ async function anchorExecute(deps, args, context) {
32455
+ const messages = deps.client.session.messages;
32456
+ if (!messages) {
32457
+ return "SIBYL ANCHOR: ERROR \u2014 engine client exposes no session.messages seam (cannot answer; not a reject).";
32458
+ }
32459
+ if (args.expectSha256 !== void 0 && !/^[0-9a-f]{64}$/i.test(args.expectSha256)) {
32460
+ return "SIBYL ANCHOR: ERROR \u2014 expectSha256 must be 64 hex chars (a malformed claim is not a MISMATCH verdict).";
32461
+ }
32462
+ let rows;
32463
+ try {
32464
+ const res = await messages.call(deps.client.session, {
32465
+ path: { id: args.sessionId },
32466
+ query: { directory: context.directory }
32467
+ });
32468
+ if (res.error !== void 0) {
32469
+ return `SIBYL ANCHOR: ERROR \u2014 engine refused the read (${JSON.stringify(res.error).slice(0, 200)}).`;
32470
+ }
32471
+ rows = res.data ?? [];
32472
+ } catch (e) {
32473
+ return internalError("sibyl_anchor_check", e);
32474
+ }
32475
+ let row;
32476
+ let locatedBy;
32477
+ if (args.body !== void 0) {
32478
+ const want = canonicalText(args.body);
32479
+ const hits = rows.map((r, i) => ({ r, i })).filter(({ r }) => canonicalText(anchorText(r)) === want);
32480
+ if (hits.length !== 1) {
32481
+ return [
32482
+ `SIBYL ANCHOR ${hits.length === 0 ? "ABSENT" : "ERROR"} \u2014 body locator`,
32483
+ hits.length === 0 ? `no message's canonical text equals the claimed body (scanned ${rows.length}).` : `body matches ${hits.length} messages at sdk-ordinals ${hits.map((h) => h.i).join(",")} \u2014 never auto-pick; re-run with an explicit index.`,
32484
+ `view_sha256=${sha256(canonicalView(rows))}`
32485
+ ].join("\n");
32486
+ }
32487
+ row = hits[0].r;
32488
+ locatedBy = `body@${hits[0].i}`;
32489
+ } else {
32490
+ const idx = args.index ?? -1;
32491
+ row = idx >= 0 ? rows[idx] : rows[rows.length + idx];
32492
+ locatedBy = `index=${idx}`;
32493
+ }
32494
+ void locatedBy;
32495
+ const claim2 = args.expectSha256 !== void 0;
32496
+ const { verdict, text } = claim2 ? decide(row, args.expectSha256) : { verdict: "ABSENT", text: row ? anchorText(row) : "" };
32497
+ const finalVerdict = !claim2 ? row === void 0 ? "ABSENT" : "ERROR" : verdict;
32498
+ const receipt = [
32499
+ `SIBYL ANCHOR ${finalVerdict}`,
32500
+ `session=${args.sessionId} ${locatedBy} messages=${rows.length} role=${row?.info?.role ?? "-"}`,
32501
+ args.label !== void 0 ? `label=${args.label}` : "",
32502
+ `anchor_sha256=${row === void 0 ? "-" : sha256(anchorText(row))}`,
32503
+ `claim_sha256=${args.expectSha256 ?? "-"}`,
32504
+ `view_sha256=${sha256(canonicalView(rows))}`,
32505
+ `body_len=${text.length}`,
32506
+ claim2 ? "" : "note: no expectSha256 supplied \u2014 a verdict requires the claim; nothing is asserted about content."
32507
+ ].filter((l) => l !== "").join("\n");
32508
+ return receipt;
32509
+ }
32510
+ function buildAnchorTool(deps) {
32511
+ return tool({
32512
+ description: "SIBYL anchor check (g12b-3 primitive): verify a claimed human message (order/approval) against the engine session store \u2014 closed verdict MATCH/MISMATCH/ABSENT/ERROR plus a view-hash receipt so the read itself is re-computable. Read-only; persists nothing.",
32513
+ args: {
32514
+ sessionId: tool.schema.string().min(1).describe("Engine session id that allegedly contains the human message."),
32515
+ index: tool.schema.number().int().optional().describe("Message ordinal in the SDK messages view (0-based; negative counts from the end; default last). WARNING: this ordinal space differs from the raw message table \u2014 cross-tool identity is the body hash, not the ordinal."),
32516
+ body: tool.schema.string().min(1).optional().describe("Claimed exact body text \u2014 located by unique canonical match (closed normalization: trailing dcp tag + trim). Repeated bodies return ERROR naming candidates, never auto-picked."),
32517
+ expectSha256: tool.schema.string().min(64).max(64).optional().describe("Claimed sha256 (hex) of the anchor body text; omitting it yields a non-verdict."),
32518
+ label: tool.schema.string().min(1).optional().describe("Free-form label echoed into the receipt (e.g. the order being ratified).")
32519
+ },
32520
+ execute: async (args, context) => anchorExecute(deps, args, context)
32521
+ });
32522
+ }
32523
+
32524
+ // src/tools/probes.ts
32525
+ var DEFAULT_TIME_POOL = [
32526
+ { url: "https://www.cloudflare.com", ownerDept: "cloudflare" },
32527
+ { url: "https://www.google.com", ownerDept: "google" },
32528
+ { url: "https://www.mozilla.org", ownerDept: "mozilla" }
32529
+ ];
32530
+ function parseHttpDate(value) {
32531
+ if (value === null) return void 0;
32532
+ const ms = Date.parse(value);
32533
+ return Number.isFinite(ms) ? ms : void 0;
32534
+ }
32535
+ async function probeTimes(sources, fetchImpl, timeoutMs = 8e3) {
32536
+ return await Promise.all(
32537
+ sources.map(async (s) => {
32538
+ try {
32539
+ const res = await fetchImpl(s.url, { method: "HEAD", signal: AbortSignal.timeout(timeoutMs), redirect: "manual" });
32540
+ const ms = parseHttpDate(res.headers.get("date"));
32541
+ return ms === void 0 ? { url: s.url, ownerDept: s.ownerDept, ok: false, error: "no-parseable-date-header" } : { url: s.url, ownerDept: s.ownerDept, ok: true, epochMs: ms };
32542
+ } catch (e) {
32543
+ return { url: s.url, ownerDept: s.ownerDept, ok: false, error: String(e).slice(0, 120) };
32544
+ }
32545
+ })
32546
+ );
32547
+ }
32548
+ function consensus(receipts, localMs, toleranceMs = 3e5) {
32549
+ const ok = receipts.filter((r) => r.ok && r.epochMs !== void 0);
32550
+ const byDept = /* @__PURE__ */ new Map();
32551
+ for (const r of ok) if (!byDept.has(r.ownerDept)) byDept.set(r.ownerDept, r);
32552
+ const used = [...byDept.values()];
32553
+ if (used.length < 2) {
32554
+ return { verdict: "INSUFFICIENT-SOURCES", usedUrls: used.map((u) => u.url), spreadMs: 0, localOffsetMs: 0 };
32555
+ }
32556
+ const times = used.map((u) => u.epochMs);
32557
+ const spreadMs = Math.max(...times) - Math.min(...times);
32558
+ const median = [...times].sort((a, b) => a - b)[Math.floor(times.length / 2)];
32559
+ const localOffsetMs = localMs - median;
32560
+ const verdict = spreadMs <= toleranceMs && Math.abs(localOffsetMs) <= toleranceMs ? "SYNCED" : "DRIFT";
32561
+ return { verdict, usedUrls: used.map((u) => u.url), spreadMs, localOffsetMs };
32562
+ }
32563
+ function equalBytes(a, b) {
32564
+ return a === b;
32565
+ }
32566
+ function attributeRange(moves, commits, roster) {
32567
+ const bySha = new Map(commits.map((c) => [c.sha, c]));
32568
+ return moves.map((m) => {
32569
+ const c = bySha.get(m.newSha);
32570
+ if (c === void 0) return { verdict: "UNATTRIBUTED", detail: `${m.remote} ${m.ref}: new commit ${m.newSha.slice(0, 12)} not in supplied commit set` };
32571
+ const hits = roster.filter((r) => equalBytes(r.name, c.committerName) && equalBytes(r.email, c.committerEmail));
32572
+ if (hits.length === 0) return { verdict: "UNATTRIBUTED", detail: `${m.remote} ${m.ref}: committer "${c.committerName} <${c.committerEmail}>" has no ACTIVE roster binding` };
32573
+ if (hits.length > 1) return { verdict: "AMBIGUOUS", detail: `${m.remote} ${m.ref}: ${hits.length} roster entries byte-equal \u2014 registry defect, needs human dedup workorder` };
32574
+ const hit = hits[0];
32575
+ if (c.signedByKey !== null && hit.keyFingerprint !== void 0 && !equalBytes(c.signedByKey, hit.keyFingerprint)) {
32576
+ return { verdict: "UNATTRIBUTED", detail: `${m.remote} ${m.ref}: signed by key ${c.signedByKey.slice(0, 12)} != roster binding ${hit.keyFingerprint.slice(0, 12)} (bytes-claim without signature)` };
32577
+ }
32578
+ return { verdict: "ATTRIBUTED", detail: `${m.remote} ${m.ref} -> ${hit.name} <${hit.email}>${hit.keyFingerprint !== void 0 ? ` key=${hit.keyFingerprint.slice(0, 12)}` : ""}` };
32579
+ });
32580
+ }
32581
+ function parseCommitMeta(stdout) {
32582
+ return stdout.split("\n").filter((l) => l.includes("|")).map((l) => {
32583
+ const [sha = "", committerName = "", committerEmail = "", gk = ""] = l.split("|");
32584
+ return { sha: sha.trim(), committerName: committerName ?? "", committerEmail: committerEmail ?? "", signedByKey: (gk ?? "").trim() === "" ? null : (gk ?? "").trim() };
32585
+ });
32586
+ }
32587
+ function buildTimeProbeTool(deps) {
32588
+ void deps;
32589
+ return tool({
32590
+ description: "SIBYL time probe (g12b-8): read HTTP Date headers from >=2 operator-distinct remote sources and compare against the local clock \u2014 verdict SYNCED / DRIFT / INSUFFICIENT-SOURCES with spread and offset in ms. Same-owner sources collapse to one; fewer than two distinct owners can NEVER certify time.",
32591
+ args: {
32592
+ endpoints: tool.schema.string().optional().describe("JSON array of {url,ownerDept}; defaults to the built-in cloudflare/google/mozilla pool."),
32593
+ toleranceMs: tool.schema.number().int().positive().optional().describe("Accepted spread/offset in ms (default 300000 = 5 min, HTTP-date + proxy slop).")
32594
+ },
32595
+ execute: async (args, _context) => {
32596
+ try {
32597
+ const sources = args.endpoints !== void 0 ? JSON.parse(args.endpoints) : DEFAULT_TIME_POOL;
32598
+ const receipts = await probeTimes(sources, fetch);
32599
+ const c = consensus(receipts, Date.now(), args.toleranceMs);
32600
+ return [
32601
+ `SIBYL TIME ${c.verdict}`,
32602
+ `used=${c.usedUrls.join(",")}`,
32603
+ `spread_ms=${c.spreadMs} local_offset_ms=${c.localOffsetMs} tolerance_ms=${args.toleranceMs ?? 3e5}`,
32604
+ ...receipts.map((r) => `${r.ok ? "ok" : "fail"} ${r.url} (${r.ownerDept})${r.error !== void 0 ? ` \u2014 ${r.error}` : ` \u2014 ${new Date(r.epochMs ?? 0).toISOString()}`}`)
32605
+ ].join("\n");
32606
+ } catch (e) {
32607
+ return internalError("sibyl_time_probe", e);
32608
+ }
32609
+ }
32610
+ });
32611
+ }
32612
+ function buildAttributeTool(deps) {
32613
+ void deps;
32614
+ return tool({
32615
+ description: "SIBYL attribution (g12b-4): compare supplied ref moves (remote/ref/old/new) against commit metadata and a roster (identity+key bindings, ACTIVE entries) \u2014 per-move verdict ATTRIBUTED / UNATTRIBUTED / AMBIGUOUS. Byte-exact comparison, no normalization; unknown commits and missing bindings are UNATTRIBUTED (silent egress is fatal-class), byte-equal roster duplicates are AMBIGUOUS registry defects. Inputs are read-only strings the CALLER captured (git log / ls-remote output) \u2014 this tool never shells out.",
32616
+ args: {
32617
+ moves: tool.schema.string().describe("JSON array of {remote,ref,oldSha,newSha}."),
32618
+ commits: tool.schema.string().describe("`git log --format=%H|%cn|%ce|%GK` output covering the moved-to commits (newest first or any order)."),
32619
+ roster: tool.schema.string().describe("JSON array of {name,email,keyFingerprint?} \u2014 the registered identity bindings.")
32620
+ },
32621
+ execute: async (args) => {
32622
+ try {
32623
+ const moves = JSON.parse(args.moves);
32624
+ const roster = JSON.parse(args.roster);
32625
+ const commits = parseCommitMeta(args.commits);
32626
+ const results = attributeRange(moves, commits, roster);
32627
+ const head = results.every((r) => r.verdict === "ATTRIBUTED") ? "ALL ATTRIBUTED" : results.some((r) => r.verdict === "UNATTRIBUTED") ? "UNATTRIBUTED MOVES PRESENT (fatal class)" : "AMBIGUOUS \u2014 registry defect";
32628
+ return [`SIBYL ATTRIBUTE: ${head}`, ...results.map((r) => `${r.verdict} :: ${r.detail}`)].join("\n");
32629
+ } catch (e) {
32630
+ return internalError("sibyl_attribute", e);
32631
+ }
32632
+ }
32633
+ });
32634
+ }
32635
+
32353
32636
  // src/state/chamber.ts
32354
32637
  import { mkdir as mkdir3, readFile as readFile3, readdir, rename as rename2, unlink as unlink2, writeFile as writeFile4 } from "node:fs/promises";
32355
32638
  import { homedir as homedir3 } from "node:os";
@@ -32364,7 +32647,8 @@ function asStringArray(v) {
32364
32647
  function isRosterRef(v) {
32365
32648
  if (typeof v !== "object" || v === null) return false;
32366
32649
  const o = v;
32367
- return isNonEmptyString(o["role"]) && isNonEmptyString(o["slot"]) && isNonEmptyString(o["modelId"]) && (o["policyCheck"] === "allow" || o["policyCheck"] === "deny") && (o["denyReason"] === void 0 || typeof o["denyReason"] === "string");
32650
+ const modelOk = o["policyCheck"] === "deny" ? typeof o["modelId"] === "string" : isNonEmptyString(o["modelId"]);
32651
+ return isNonEmptyString(o["role"]) && isNonEmptyString(o["slot"]) && modelOk && (o["policyCheck"] === "allow" || o["policyCheck"] === "deny") && (o["denyReason"] === void 0 || typeof o["denyReason"] === "string");
32368
32652
  }
32369
32653
  function isArtifactRef(v) {
32370
32654
  if (typeof v !== "object" || v === null) return false;
@@ -32646,7 +32930,8 @@ async function runTask(ctx, task) {
32646
32930
  persona: worker.persona,
32647
32931
  model: worker.model,
32648
32932
  inputText: composeTaskPrompt(ctx, task.taskId),
32649
- timeoutMs: worker.timeoutMs
32933
+ timeoutMs: worker.timeoutMs,
32934
+ parentID: ctx.parentID
32650
32935
  });
32651
32936
  } catch (e) {
32652
32937
  task.status = "failed";
@@ -32735,6 +33020,7 @@ async function run(prior, roster, opts) {
32735
33020
  const resolved = {
32736
33021
  client: opts.client,
32737
33022
  directory: opts.directory,
33023
+ parentID: opts.parentID,
32738
33024
  concurrencyK: opts.concurrencyK,
32739
33025
  staggerMs: opts.staggerMs,
32740
33026
  maxAttempts,
@@ -32933,7 +33219,8 @@ async function planWorkflow(opts) {
32933
33219
  persona: opts.architectPersona,
32934
33220
  model: opts.model,
32935
33221
  inputText: buildPlanInstruction(opts.goal, opts.artifact),
32936
- ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
33222
+ ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
33223
+ ...opts.parentID !== void 0 ? { parentID: opts.parentID } : {}
32937
33224
  });
32938
33225
  } catch (e) {
32939
33226
  return { ok: false, stage: "plan", error: `architect runPersona threw: ${String(e)}` };
@@ -33017,6 +33304,7 @@ async function swarmExecute(deps, args, context) {
33017
33304
  goal: args.goal,
33018
33305
  artifact: read.text,
33019
33306
  timeoutMs: options.timeoutMs,
33307
+ parentID: context.sessionID,
33020
33308
  repair: async (_badText, why) => {
33021
33309
  const redo = await runPersona({
33022
33310
  client,
@@ -33024,7 +33312,8 @@ async function swarmExecute(deps, args, context) {
33024
33312
  persona: { system: PERSONAS.ARCHITECT.system },
33025
33313
  model: architectModel,
33026
33314
  inputText: `${buildRepairDemand(why)} matching the workflow-schema contract: {"tasks":[{"id","title","instructions","dependsOn"}],"concurrency":<int >= 1>,"notes":<string>}`,
33027
- timeoutMs: options.timeoutMs
33315
+ timeoutMs: options.timeoutMs,
33316
+ parentID: context.sessionID
33028
33317
  });
33029
33318
  return redo.ok ? redo.text : "";
33030
33319
  }
@@ -33040,6 +33329,7 @@ async function swarmExecute(deps, args, context) {
33040
33329
  const dispatch = await dispatchRoster(mint.roster, {
33041
33330
  client,
33042
33331
  directory,
33332
+ parentID: context.sessionID,
33043
33333
  concurrencyK: Math.min(options.concurrencyK, plan.schema.concurrency),
33044
33334
  staggerMs: options.staggerMs,
33045
33335
  maxRounds: options.maxRounds,
@@ -33070,27 +33360,30 @@ ${text}
33070
33360
  persona: { system: "You are the SIBYL swarm judge. You rule on pipeline reports with exactly one word: APPROVE, REJECT, or EXHAUSTED." },
33071
33361
  model: slotForModel(options.modelPool, options.swarm.judge, void 0),
33072
33362
  inputText: buildJudgeInput(report),
33073
- timeoutMs: options.timeoutMs
33363
+ timeoutMs: options.timeoutMs,
33364
+ parentID: context.sessionID
33074
33365
  });
33075
33366
  const word = judged.ok ? parseJudgeWord(judged.text) : null;
33076
33367
  judgeNote = word === null ? `judge=inactive (${judged.ok ? "unrecognized verdict word" : `engine: ${judged.error ?? "?"}`}; derived kept)` : `judge=${word}${word === report.verdict ? "" : " (overrides derived " + report.verdict + ")"}`;
33077
33368
  if (word !== null) report = { ...report, verdict: word };
33078
33369
  }
33079
33370
  const counts = countTasks(report.tasks);
33371
+ const noSignal = counts.done === 0;
33372
+ const tag = noSignal ? "CANNOT_ANSWER" : report.verdict === "APPROVE" ? "APPROVE" : "REJECT";
33080
33373
  await finishRun(deps, record3, {
33081
33374
  status: "done",
33082
33375
  rounds: report.rounds,
33083
33376
  verdict: {
33084
- verdict: report.verdict === "APPROVE" ? "APPROVE" : "REJECT",
33377
+ verdict: tag,
33085
33378
  approvals: counts.done,
33086
- rejects: counts.failed,
33087
- errors: 0,
33379
+ rejects: noSignal ? 0 : tag === "REJECT" ? 1 : 0,
33380
+ errors: counts.failed,
33088
33381
  missing: counts.suspended
33089
33382
  },
33090
- notes: `${judgeNote} \xB7 ${report.tasks.map((t) => `${t.id}=${t.status}`).join(",")}`
33383
+ notes: `${judgeNote}${noSignal ? " \xB7 zero drafts produced: instrument failure, not a verdict on the artifact" : ""} \xB7 ${report.tasks.map((t) => `${t.id}=${t.status}`).join(",")}`
33091
33384
  });
33092
33385
  const lines = [
33093
- `SIBYL SWARM: ${report.verdict} run ${record3.runId} (rounds=${String(report.rounds)} done=${String(counts.done)} failed=${String(counts.failed)} suspended=${String(counts.suspended)} ${judgeNote})`,
33386
+ `SIBYL SWARM: ${tag} run ${record3.runId} (rounds=${String(report.rounds)} done=${String(counts.done)} failed=${String(counts.failed)} suspended=${String(counts.suspended)} ${judgeNote})`,
33094
33387
  ...report.tasks.map((t) => ` task: ${t.id} ${t.status}${t.sessionId === void 0 ? "" : ` session=${t.sessionId}`}`),
33095
33388
  ...report.artifacts.map((a) => ` artifact: ${a}`),
33096
33389
  ` space: ${record3.spaceDir}`,
@@ -33123,7 +33416,13 @@ function toEngineClient(sdk) {
33123
33416
  return {
33124
33417
  session: {
33125
33418
  async create(args) {
33126
- const r = await sdk.session.create({ body: { title: args.body.title }, query: { directory: args.query.directory } });
33419
+ const r = await sdk.session.create({
33420
+ body: {
33421
+ title: args.body.title,
33422
+ ...args.body.parentID !== void 0 && { parentID: args.body.parentID }
33423
+ },
33424
+ query: { directory: args.query.directory }
33425
+ });
33127
33426
  return {
33128
33427
  ...r.data !== void 0 && { data: { id: r.data.id } },
33129
33428
  ...r.error !== void 0 && { error: r.error }
@@ -33161,6 +33460,34 @@ function toEngineClient(sdk) {
33161
33460
  },
33162
33461
  ...r.error !== void 0 && { error: r.error }
33163
33462
  };
33463
+ },
33464
+ async messages(args) {
33465
+ const r = await sdk.session.messages({
33466
+ path: { id: args.path.id },
33467
+ query: { directory: args.query.directory }
33468
+ });
33469
+ return {
33470
+ ...r.data !== void 0 && {
33471
+ data: r.data.map((m) => ({
33472
+ info: {
33473
+ ...m.info.role !== void 0 && { role: m.info.role },
33474
+ ...m.info.role === "assistant" && {
33475
+ ...m.info.providerID !== void 0 && { providerID: m.info.providerID },
33476
+ ...m.info.modelID !== void 0 && { modelID: m.info.modelID },
33477
+ ...m.info.time !== void 0 && {
33478
+ time: { ...m.info.time.completed !== void 0 && { completed: m.info.time.completed } }
33479
+ },
33480
+ ...m.info.error !== void 0 && { error: m.info.error }
33481
+ }
33482
+ },
33483
+ parts: m.parts.map((p) => ({
33484
+ type: p.type,
33485
+ ..."text" in p && p.text !== void 0 && { text: p.text }
33486
+ }))
33487
+ }))
33488
+ },
33489
+ ...r.error !== void 0 && { error: r.error }
33490
+ };
33164
33491
  }
33165
33492
  }
33166
33493
  };
@@ -33184,7 +33511,10 @@ async function SibylPlugin(input2, options) {
33184
33511
  sibyl_consult: buildConsultTool(deps),
33185
33512
  sibyl_swarm: buildSwarmTool(deps),
33186
33513
  sibyl_status: buildStatusTool(deps),
33187
- sibyl_review: buildReviewTool(deps)
33514
+ sibyl_review: buildReviewTool(deps),
33515
+ sibyl_anchor_check: buildAnchorTool(deps),
33516
+ sibyl_time_probe: buildTimeProbeTool(deps),
33517
+ sibyl_attribute: buildAttributeTool(deps)
33188
33518
  }
33189
33519
  };
33190
33520
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sibyl-system",
3
- "version": "1.1.0",
3
+ "version": "1.2.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",