AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
翻譯準備中
ARRA Relic Scan: every AI-agent transcript (Claude Code, Codex, OMP, OMX, Gemini, Pi) and every ψ vault on your machine, indexed to Parquet and browsable locally. Nothing leaves it.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗| Check before you index. What an index would read, and a log of every index and check. | See what your agents wrote. Rows by kind, by where the file lives, by agent and month. |
| Every session as a star. Months as rings, repositories as sectors, a query language and ⌘K. | Home, repository, session. The same index as a tree, newest first. |
| Every oracle's memory. ψ vaults and their notes, grouped by folder. | Read a note where it lives. Markdown rendered in the app, nothing copied. |
| Where the gigabytes are. Every .jsonl by folder and size. | Take the metadata with you. Every table as CSV or JSON; month files one click from Finder. |
Every capture comes from a demo index of made-up repositories.
One Rust binary, relic-scan (this repo is still named relic4-scan). It finds every AI-agent transcript on a machine, writes one Parquet file per Bangkok month in the relic4 truth schema, and writes the meta beside them: files, sessions, and every .jsonl on the machine. The design is in RESEARCH.md, the lessons from relic v1–v3 in LEARNED.md, and the exact output rules, traced and verified, in OUTPUT-CHAIN.md.
Grown in the agent-relic-v4 lab (lab/relic4-scan on branch lab-rust-scan, history kept). agent-relic-v4 is the reader: it turns these month files into truth, meta, vectors and search. The verify scripts need its Python environment: uv run --project <agent-relic-v4> python verify/….
Open source under the GNU AGPL v3. Landing page: https://relic.buildwithoracle.com/scan/ New here? docs/TUTORIAL.md walks through scan, then extract, with real outputs. How the relic4-scan, relic5 and relic-extract agents ask each other for changes: docs/TALK.md. One-page summary: docs/TLDR.md. No loop runs the chain any more: docs/MANUAL.md says how to run a pass by hand and how to turn the loop back on. AI agents: start with AGENTS.md (install, run, query with DuckDB, build, rules for changing the code).
Install. One command; it needs Rust 1.89 or newer. install.sh finds cargo even when it is not on PATH, builds in about 30 s and installs to ~/.local/bin/relic-scan. Or download the desktop app from Releases, or build from a checkout with just (below).
curl -fsSL https://raw.githubusercontent.com/Soul-Brews-Studio/relic4-scan/main/install.sh | sh
To install a branch instead of main, pipe to REF=<branch> sh. Run the same line again to upgrade.
Run.
relic-scan # the logo, what is indexed already, and the commands below
relic-scan up --kind claude # index Claude transcripts into ~/.relic-scan/claude, then serve http://127.0.0.1:8787
relic-scan tui --kind claude # the same data in the terminal; o opens the page on the selected row (--mouse: click a row, again to open)
The first run reads everything: on an M5 Mac that is 83,673 files and 67.6 GB in about 45 s. After that a run reads only new bytes and takes about 10 s, most of it spent walking the homes. Without --kind, every agent is indexed into ~/.relic-scan.
| command | what it does |
|---|---|
relic-scan up [--kind K] [--port 8787] | run, then serve |
relic-scan run [--kind K] [--out DIR] | Scan, parse only new bytes, then write meta, map and paths. It ends with a summary card. |
relic-scan log [--diff N] | The scan log: every index and check, newest first; --diff N lists one entry's files. |
relic-scan run --dry-run | Only check: scan, compare with the state, and print what an index would read (new, grown, rewritten, gone, files, GB). It writes nothing and takes no lock. |
relic-scan run --month 2026-10 | Only rows of those Bangkok days, in a folder of their own. Also --day 2026-10-22, or --from and --to (each a day or a month, either one optional). See Only some days. |
relic-scan serve [--kind K] | Serves /map, /tree and /paths on 127.0.0.1 only. The page is read on every request, so a run in another shell shows up on reload. The page's ⟳ index button runs an incremental run in the server (POST /api/run, which needs an X-Relic: 1 header; progress at GET /api/status). Check changes on Smart Scan is the same scan as a dry run (POST /api/check, same header): it shows what Index now would read and writes nothing. Before the first run, every view shows a welcome page with the same button. |
relic-scan tui [--kind K] | The terminal browser (below). |
relic-scan meta / map | Rebuild only the meta tables, or only the page. |
relic-scan scan [--full] [--index F] / parse --index F --out DIR | The two phases alone, for debugging and benchmarks. |
Defaults and safety:
--out: defaults to ~/.relic-scan. With --kind and no --out it is ~/.relic-scan/<kind>, so a kind-only run never shares its state with a run over every agent. serve, tui, meta and map take the same --kind to find it.OUT/.lock. A second run, meta or map on the same directory stops with a message instead of racing the first._state.tsv are written to a temporary file, fsynced, then renamed. The state goes last, so a crash costs a re-read, never a lost or doubled row.RELIC_SCAN_CHUNK bytes to change it). The biggest files are parsed side by side, so reading them whole used to hold gigabytes at once.Desktop app, ARRA Relic Scan (Tauri v2): desktop/ wraps the same page in a native window.
RELIC_SCAN_KIND: all (the default, the same folder as relic-scan run), claude, or a comma-separated list.RELIC_SCAN_OUT: overrides the output directory.cd desktop && cargo build --release && ./target/release/relic-scan-desktop # run it (--view banks|health|map|tree|paths)
cargo install tauri-cli --version '^2' --locked && cargo tauri build # ARRA Relic Scan.app
Installer: cargo tauri build --bundles app,dmg also makes target/release/bundle/dmg/ARRA Relic Scan_<version>_aarch64.dmg, a drag-to-Applications window (660x400, the app at (180,190) and Applications at (480,190), the layout Oracle Pulse uses). Its background is desktop/dmg/background.tiff (1x and 2x), made by desktop/dmg/make-background.sh; it is light because Finder draws the icon labels in black.
Linux (.deb, .AppImage): desktop/linux/Dockerfile builds both on any Docker host (Ubuntu 22.04, so the AppImage runs on newer systems too). From a Mac it can run on another machine through a Docker context over ssh:
docker context create builder --docker "host=ssh://you@your-linux-host" # once
docker --context builder buildx build -f desktop/linux/Dockerfile --target out --output type=local,dest=/tmp/relic-linux .
.dockerignore keeps the build folders out of the upload. The first build installs WebKitGTK and the Tauri CLI; later ones reuse those layers.
How it compares with Relic Scanner and Relic Studio, the SwiftUI apps over relic3: docs/COMPARE.md.
Build from a checkout instead:
cargo build --release && cargo test --release
./target/release/relic-scan up --kind claude
months/_state.tsv records, per file: dev, inode, size, the offset after the last complete line, the line count, the session, the last ts, and a hash of the first 4 KB. Each run classifies every file:
| state | rule | work |
|---|---|---|
| unchanged | same inode, size = offset | nothing is read |
| grown | same inode, size > offset, same prefix | parse from the offset with the saved line count, session and last ts |
| rewritten | other inode, size < offset, or the prefix changed | parse from 0 again; old rows stay, truth is append-only |
| new | not in the state | parse from 0 |
| gone | in the state, not on disk | dropped from the state; its rows stay |
Only months that received rows are rewritten; every other month file is left untouched. The state file is written last, so after a crash the next run re-reads and the line hash folds the duplicates.
Proof: incremental equals cold (verify/incremental.py). The test takes real transcripts and:
| transcripts | cold rows | in incremental, every field equal | extra rows (old versions, append-only) | 3rd run |
|---|---|---|---|---|
| 40 | 1,938 | 1,938 / 1,938 | 1 | 0 bytes read, no month rewritten |
| 300 | 21,257 | 21,257 / 21,257 | 397 | 0 bytes read, no month rewritten |
An M5 Mac, whole machine:
| run | files | read | time |
|---|---|---|---|
| cold | 86,904 | 75.6 GB | 58 s: scan 9, parse 27 (2.8 GB/s, 18 threads), merge 8, prefix hashes and state 14 |
| incremental (2 grown) | 86,904 | 0.00 GB | 13 s: scan 8.5, merge 4.3 (only 2026-10 rewritten) |
For comparison, relic v3 needed 43 s for an incremental run with 37 changed files. Where the next win is:
relic-scan export --to s3://bucket/prefix copies every sealed month, meta/months.json (the seals) and an
export-manifest.json to an S3-compatible store (AWS S3, Garage, MinIO). The endpoint and keys come from the environment
(RELIC_S3_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY), never the command line. It is safe to re-run: each object
carries its sha256, so what is already there is skipped, and a sealed month that differs in the store is refused unless
--overwrite. --dry-run only looks; --verify compares the store with the files; --list prints what the store holds, one line per month (rows, dates, size, sha256, seal), from the two small files the export writes. The same copy has a button: the Export view's "S3 copy" card checks each sealed month against the store and sends what is
missing; the destination is in Settings and the keys come from the environment or ~/.config/relic-scan/s3.env (mode 600). It is the only
part that uses the network, and only when you press it. Plain http is allowed only to this machine or the tailnet.
--month, --day, --from and --to keep only the rows whose Bangkok date is in the range. A row with no time at all is left out.
--out, the run writes ~/.relic-scan/<kinds or all>@<range>, for example ~/.relic-scan/all@2026-10 or ~/.relic-scan/claude@2026-10-22. Pass that path as --out to serve, tui, meta and map; the summary card prints it.~/.relic-scan itself, for example);OUT/_range, and a run asking for a different range stops;--month 2026-10 reads only new bytes.| reads file contents | an M5 Mac, 91,297 files / 76 GB | verdicts | |
|---|---|---|---|
| light | no: names, folder shapes, path rules | 8.3 s | transcript-shaped 86,290 · not-transcript 4,393 · unknown 614 |
| full | first complete lines, plus the real parser as proof | 23.0 s | transcript 85,043 · empty 1,234 · unverified 505 · not-transcript 4,508 |
How a home is found:
.claude*, so .claude_bak, .claudebak, .claude2 and the rest; also .codex*, .omp*, .omx*, .thclaws*, .gemini*, .pi.<root>/<project>/<uuid>.jsonl, or <root>/[YYYY/MM/DD/]rollout-*.How each file is classified:
Verified against truth: 0 lost. No file that produced a truth row was called not-transcript, in either mode.
splitlines does, and each line is hashed (blake2b-8) including its newline.engine.py and store.parse_chunk: text, thinking, tools, redaction, ts, sid and engine.| run | files | GB | time | result vs relic4 truth |
|---|---|---|---|---|
| sample (seed 7) | 125 | — | 1.4 s | 13,602/13,602 rows, every field byte-equal |
| all files of one Mac jt sent | 50,188 | 43 | 17 s (parse ~11 s, merge 5 s) | 4,228,336 rows found; text/body/red/kind/call_id/name/err/type/role/engine equal on all; sid differs on 2 and ts on 14 (copy-dependent, OUTPUT-CHAIN §3); 776 rows newer than truth |
--jt-compat stops a file at a line over 8 MB, as jt does. By default parse skips files the scan called not-transcript; --all includes them.
The last step of run, or relic-scan meta --out DIR on its own. It reads the month files and _state.tsv and writes two tables. It is a Rust port of agent-relic-v4's relic4 meta --from-scan, with the same columns and path rules (sql.py MACROS), and needs no truth import, Python or DuckDB.
| file | one row per | columns |
|---|---|---|
DIR/meta/files.parquet | file in the state | agent, path, home, size, lines, last_ms, file_sid, base, tier, engine, proj, parent_sid, cat, n_rows, text_rows, first_ts, last_ts, plus rows by type |
DIR/meta/sessions.parquet | file name (base, tier), copies folded | base, tier, sid, parent_sid, engine, proj, cat, probe, first_ts, last_ts, last_ms, copies, machines, agents, size, n_rows, text_rows, text_msgs, months, plus rows by type |
Rows by type are six columns: user_text, assistant_text, thinking, tool_calls, tool_results and tool_errors. Errors are results that failed, so they are also counted in tool_results.
meta/, not next to the months, because relic4 truth import --from-scan reads every DIR/*.parquet as a month.meta/cache/YYYY-MM.tsv. A month is aggregated again only when its Parquet file changed (size or mtime).msgkey there; the two counts are the same.Every .jsonl on the machine: run also writes meta/jsonl.parquet, with one row per .jsonl file the walk saw. Columns: path, place, home, home_kind, origin, vault, agent, verdict, tier, size, mtime_ms and parsed. place is one of three values:
The same walk that finds the homes collects the loose files, so they add no extra pass. Loose files are few, so the scan always reads their first lines, even in the light scan. On an M5 Mac that gives 103,657 .jsonl: 91,298 in homes, 61 in 22 ψ vaults, and 12,298 elsewhere (mostly water-api and arthur-data). Of the loose files, the 14 whose content looks like a transcript are all test fixtures in repos. No real transcript sits outside a home.
Claude only (run --kind claude, an M5 Mac, 2026-10-22):
| run | files | read | sessions | time |
|---|---|---|---|---|
| cold | 83,673 | 67.6 GB, 19.3 M lines, 8.5 M raw rows | 43,006 | 46.7 s: scan 8.2, parse 21.7 (3.1 GB/s), merge 6.8, meta 1.4 |
| incremental | 83,673 unchanged | 0 GB | 43,006 | 9.8 s: scan 9.0, meta 0.5 (no month re-aggregated) |
Proof: equal to relic4's meta (verify/meta_vs_python.py). It compares every column of every row against relic4 truth import --from-scan followed by relic4 meta --from-scan on the same DIR. Files are matched on path, sessions on (base, tier).
| An M5 Mac, 2026-10-22 | files | sessions | result | time |
|---|---|---|---|---|
| cold (no cache) | 86,905 | 45,094 | every row and column equal | 1.9 s (Python: truth import 19.9 s + meta 2.5 s) |
after an incremental run (2026-10 rewritten) | 86,905 | 45,094 | every row and column equal | 0.9 s (1 month aggregated again) |
serve (or up) serves one page on 127.0.0.1, in the shape of Relic Studio. A left sidebar holds six views; under 1100 px it folds to icons. At its foot it shows whether a run is going and when the last one was.
/, /scan): a coverage ring (transcript files indexed out of those found), the Index now and Check changes controls with a live progress bar, and two stat strips: files indexed, discovered, not transcripts and outside homes; then sessions, rows, indexed size and last run.
.jsonl with _state.tsv by size and inode, reading no content. An index is that scan, then reading only the new bytes, rewriting the months that got rows, and rebuilding meta, map and paths. Check changes runs only the scan: new, grown, rewritten, gone and unchanged files, and the files and GB that Index now would read. It writes nothing; the result stays on the page until the next index..jsonl that holds it (main session, subagent, workflow agent), and where that file lives (own home, mirror, archive, rescue, copy). Click a slice to go in and the centre to go back; "file kind first" swaps the first two rings. A line copied into several homes is one row, under the first home.GET /api/log./, or the search button top right): one box over what the page holds: views, actions (index, check, output, contents, theme), sessions (by id, repo or project; empty shows the latest), repos, homes, every .jsonl path, scan-log entries, and filters in the map/tree query language. Each result opens its view; some carry actions: open folder (shows the file in Finder; the server refuses any path the index does not hold), copy the resume command, copy the path, and check or index just that file or home (only= on the API). ⌘↵ runs the first action.Map, tree and paths are below. Map and tree share the query box, the side panel and the URL hash, so a query carries over. The theme button cycles auto, dark and light; auto follows the system. In tree and paths only the page scrolls; the side panel sits beside it, never inside it.
run rebuilds the page (DIR/meta/map.html, about 4 MB for 43,006 sessions, data inlined) and DIR/meta/paths.json. relic-scan map rebuilds both alone in 0.2 s. Opened as a file, map.html?view=tree shows the tree. The paths view loads /paths.json, so it needs the server.
/map, the star map:
<uuid>/subagents/ folder they sit in. 99% of them have a parent.repo: proj: sid: home: tier:main|sub|wfmonth:2026-07 after:2026-09-01 before:rows>1000 text msgs copies user asst think calls results errors tools, e.g. think>0 errors>=20- before a term negates it, "..." quotes a value, and all terms must match (AND).claude --resume line./tree: jsonl home → repo → session → satellites. Each level shows sessions, rows, a bar of rows by type and the last activity. A session is listed under every home that holds a copy. A result of up to 400 sessions opens itself down to the repos.
/paths: every .jsonl the scan saw, as a folder tree. Chains of single folders fold into one row, and ~ stands for your home. Each folder shows files, rows, size and a status bar:
Query words match the path. Filters: is:indexed|skipped|vault|loose, verdict:not-transcript, name:agent-, mb>100, size>1e9, rows>10000, rows=0, after: and before: (modified, Bangkok time). A small result opens every folder it touches. Clicking a file shows its status, verdict, size, rows and modified time, with a button to copy its path.
relic-scan ops: what the relic chain (scan, extract, push, hub index, sync check) is doing right now. It reads the chain log (~/Library/Logs/relic-chain.log, or ~/.local/state/relic-chain.log; RELIC_CHAIN_LOG changes it), the running processes, the hub's status (RELIC_OPS_HUB, default the tailnet hub) and the S3 job, and prints each stage with its time against its typical time and an ETA. --watch redraws every 2 seconds, --json prints the snapshot that the Activity view (/api/ops) and other tools read, --no-hub makes no network call.
relic-scan tui: the same data in the terminal, with six views on the number keys:
| key | view |
|---|---|
1 | overview: totals, rows by type, sessions per month by tier |
2 | repos: sessions (main / sub / workflow), rows, thinking, errors, last activity. Enter lists the repo's sessions. |
3 | sessions, newest first. Enter opens one; c copies claude --resume <sid>. |
4 | paths: folders holding .jsonl, with indexed / skipped / other. Enter lists the folder's files; Enter on a file copies its path. |
6 | ops: the same snapshot as relic-scan ops, redrawn every 2 seconds. |
5 | s3: the S3 copy of the sealed months (destination, key status, one row per object). c checks against the store and sends nothing; s sends what is missing and never replaces a month that differs. Same job as the Export view's card. |
Other keys:
/ filters the list (words, AND) and Esc clears the filter.↑↓, j k, g G and PgUp PgDn move.⌫ goes back and q quits.It sets no background colour, so it reads the same on dark and light terminals. Colour in the CLI output appears only on a terminal and is off with NO_COLOR.
A transcript only ever grows, so the index keeps a ledger of what it has read: OUT/_ledger.tsv, appended and never
rewritten, one line per version of a file (the first line's hash, the line count, a hash chain over every line, the path).
A new or changed path is answered before it is parsed: same (a copy of a version read elsewhere: not read),
update (a known version is its start: only the new lines are read), past changed (flagged, read again, old rows
kept) or never. A moved folder or another machine's mirror costs a hash instead of a parse. The first run after an
upgrade builds the ledger from _state.tsv by hashing each file up to where it was read.
ψ folder it passes (an oracle's vault, or a link to one). After each index, every
real vault is read once: its markdown notes go to OUT/meta/vaults.json, plain JSON, readable by jq, DuckDB or the
page. Each vault lists its notes (path inside the vault, section such as memory/learnings, title from the first
# heading, size, date) and the repos that reach it through a link. Nothing in a vault is changed.
jq '.vaults[] | {name, notes, path}' ~/.relic-scan/meta/vaults.json
ψ/memory/,
ψ/inbox/, …), newest first and filterable. A note opens in a reader that renders its markdown in the page (front matter,
headings, lists and tasks, code, tables, quotes, links); Show in Finder is one click from there. The page renders
everything escaped, and /api/note returns only markdown files inside a listed vault (up to 2 MB).~/Downloads; and the month files (OUT/YYYY-MM.parquet) with their sizes, each one click from Finder
(/api/months, /api/reveal?what=month&m=2026-10).A justfile holds every build (just lists them):
gh repo clone Soul-Brews-Studio/relic4-scan && cd relic4-scan
just build # target/release/relic-scan
just install # copies it into ~/.local/bin
just test # every test
just app # the desktop app and its .dmg for this Mac (just app-intel: for Intel Macs)
just linux # .deb and .AppImage in Docker (just linux builder: on the docker context named builder)
src/: the relic-scan crate (CLI, server, TUI, and the page it serves).desktop/: the Tauri desktop app, its icons, the DMG background and the Linux build (desktop/linux/Dockerfile).site/: the landing page (site/public/) and its deploy config; see docs/DESIGN.md.docs/: DESIGN.md (the look of the app and the site), RESEARCH.md (the design and its
sources), LEARNED.md, OUTPUT-CHAIN.md (what phase 2 must reproduce) and
COMPARE.md.verify/: the full-corpus proofs in Python.install.sh: builds and installs the CLI; justfile: every build from a checkout.No file is longer than 500 lines; a long one is split into a folder of modules.
src/run.rs: relic-scan run (scan, diff, parse, merge, meta, map, scan log); src/lib.rs keeps the arguments and the shared helpers.src/parse/: the reader (mod.rs), the raw parts (parts.rs), the month merge (merge.rs), tests.src/meta/: the tables (mod.rs), the path rules (rules.rs), the per-month pass and its cache (agg.rs), tests.src/tui/: state and loading (mod.rs, load.rs), keys (keys.rs), drawing (draw.rs), the S3 view (s3view.rs).src/page/: the page as small files: the shell index.html, three CSS files, and the scripts in the order they run
(core, map, shell, tree, paths, dash, contents, disk, scanlog, settings, main, palette). map.rs puts them
back together into one self-contained page; there is no build step and no network. Shared helpers live in core.js:
human() for bytes, relicPost() for every write to the server, and overlay() for every popup.cargo test runs 22 tests in under a second, with no other repo needed:
splitlines and the jt cut of a last line;\r replaces itself;--from after --to are refused); rows are kept by Bangkok day; a folder keeps its range and a full index never takes one; a one-day run gives exactly that day's rows of a full parse, and still reads to the end of the file.The full-corpus proofs (verify/*.py) compare against agent-relic-v4 and need its Python environment. In addition, a 4,023-file fixture (7.1 GB, including the 24 biggest files) gave every month, files.parquet, sessions.parquet and jsonl.parquet equal to the previous binary's. Peak RSS fell from 5.81 GB to 3.72 GB at the same speed.
transcript_full.jsonl preference.~/DownloadsPOST /api/save.csv.jsonOUT/meta/settings): the folders to walk, paths to skip and the depth, edited as chips (type and Enter, a suggestion, or Choose folder…, which opens the desktop's own folder picker through the server), an automatic Check changes every 15, 30 or 60 minutes while the server runs (it never indexes on its own), and the theme. Saved in OUT/_config.json (GET/POST /api/config); relic-scan run and Index now use it, and a flag on the command line wins./scans): every index and check from OUT/_scans.jsonl, newest first: new, grown, rewritten and gone files, bytes read, time, and rows added. Open one to see its files (the 2,000 biggest reads are kept per entry). relic-scan log prints the same list, and relic-scan log --diff N lists the files of entry N./banks): one card per jsonl home with sessions, rows, files, size, origin and newest session; a card opens the tree on that home. Below them is a repository index with org/repo, sessions and rows; a row opens the map on that repo./health): checks computed from the index, each with a link to the matching view: