* feat(lint): #524 defrift lock pins the #514 tools-allowlist content
New scripts/check_tools_allowlist.py + 18 mutation tests, wired into
spec-consistency.yml and the unified pytest manifest. Invariant 1 pins
the exact `tools: Read, Write, Edit, Grep, Glob` frontmatter line on all
six #514 surfaces — the symmetric source+mirror Bash re-add that
previously passed every CI gate green (mirror-sync pins the pair, never
the value; the runtime guard keys on name, never frontmatter) now fails.
Invariant 2 reconciles frontmatter against the runtime channel: a Bucket
A agent must not advertise Bash in a tools key, fail-closed on a
missing/unparseable manifest. Placement of invariant 2 in this lint
follows the #524 proposal (considered and declined: a write-scope-lint
I6, to avoid touching the frozen I1-I5 suite for an optional guard).
Plus the #524 checklist docs: one-line allowlist mention in
docs/PERFORMANCE.md en/zh-TW § plugin agents and the announce script's
plugin-agents line, and the CHANGELOG [Unreleased] backfill covering
#514/#521 (retrospective, by @madtriceps), #523, and this change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPGvAbphbuZ4mhx2Ketj7s
* fix(lint): #524 round-2 — YAML-semantic invariant 2, duplicate-key/CRLF pins, curated manifest diagnostics
Addresses the codex gpt-5.6-sol xhigh round-1 findings (P1 convergent
with the first-party security pass): invariant 2 now parses frontmatter
as YAML, so quoted values, quoted names, flow/block lists, inline
comments, and Bash(...) specifiers no longer evade the Bucket A
reconciliation (BashOutput stays unflagged — exact base-name match);
Bucket A files with unparseable YAML or unrecognized tools shapes fail
closed. Invariant 1 gains a broadened tools-key count (a quoted
"tools": duplicate cannot hide behind the pinned line) and a semantic
YAML belt (last-wins divergence fails even if a line trick slips the
count), and reads raw bytes so a symmetric LF->CRLF conversion is
drift. Valid-JSON non-object manifests get a curated fail-closed
diagnostic instead of a traceback. Suite grows 18 -> 31 with a failing
witness per new branch; CHANGELOG wording corrected accordingly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPGvAbphbuZ4mhx2Ketj7s
* fix(lint): #524 round-3 — node-tree merge/alias authority, col-0 fence, byte-witness tightening
Closes the codex gpt-5.6-sol xhigh round-3 P1s, both rooted in the same
lesson (text parsing is not a sound YAML authority):
- P1-1: an indented `---` inside a `description: |` block scalar was read
as the closing frontmatter fence, truncating the block and hiding a
Bucket A `name` + `tools: Read, Bash` below it. The fence is now a
column-0 `---` only (optional retained CR).
- P1-2: merge/alias detection was a text scan that a `#` inside a quoted
flow key and a `<<` inside a flow mapping both evaded. Replaced with a
composed-node-tree walk: a merge key carries the yaml merge tag on its
scalar node, an alias surfaces as the same node object reached twice
(shared identity). No text parsing.
Also: P2-1 byte-witness now requires the verbatim pinned line as the sole
raw tools line (an escaped/tagged/folded re-spelling that empties
raw_lines still fires). P2-2 (`!!binary`/`!!str` tag tricks) fixed by the
simplified `_scalar_py` — construct from the original composed node so its
resolved tag is honored (a `!!binary` list member becomes bytes, rejected
as a non-string shape; `!!str null` stays the string 'null'). Dropped the
redundant resolve()+reconstruct and the now-unused re import. Suite 39 ->
42 with witnesses for the indented fence, flow-merge-quoted-hash, escaped
duplicate key, and the block-scalar-triple-dash false-positive. CHANGELOG
corrected (duplicate-preserving, 42 tests, three review rounds).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPGvAbphbuZ4mhx2Ketj7s
* fix(lint): #524 self-probe — strip leading BOM so a BOM-prefixed agent can't skip invariant 2
A UTF-8 BOM before the first `---` made the column-0 fence match fail, so
the file read as frontmatter-less and invariant 2 skipped it — while a real
YAML reader (and Claude Code) strips the BOM and sees the frontmatter. A
BOM-prefixed Bucket A agent could smuggle `tools: Read, Bash` through that
skip. _read_raw now strips a leading BOM so the lint and the real consumer
agree. Suite 42 -> 44 (BOM-Bash fail-closed + BOM-clean pass witnesses).
Found by first-party self-probe before the codex round-4 pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPGvAbphbuZ4mhx2Ketj7s
* fix(lint): #524 round-4 — fail closed on parser-dependent duplicate name/tools in invariant 2
codex gpt-5.6-sol xhigh round-4 P2: invariant 2 resolved name/tools with
last-wins (values[-1]), but duplicate YAML keys are parser-dependent. A
Bucket A file with `tools: Read, Bash` then `tools: Read, Grep` passed
(last-wins picks Grep) while a first-wins consumer would grant Bash;
symmetrically a duplicate `name` where only one resolution is Bucket A
could skip the file. Invariant 2 now rejects a duplicate `tools` on any
in-scope file and a duplicate `name` where any resolution is a Bucket A
key — fail closed, matching invariant 1's existing duplicate-tools
rejection. (round-4's other finding, the leading BOM, was already fixed in
645cfa2 — round-4 reviewed the pre-BOM-fix commit and noted the worktree
already carried the fix.) Suite 44 -> 48 with last-wins/first-wins/dup-name
witnesses. CHANGELOG updated (four rounds, 48 tests, full fail-open list).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPGvAbphbuZ4mhx2Ketj7s
* fix(lint): #524 round-5 — merge tag on any node type + RecursionError fails closed
codex gpt-5.6-sol xhigh round-5 found two residual issues:
- P1: _uses_merge_or_alias checked the merge tag only inside the ScalarNode
branch, but PyYAML applies merge semantics for a merge-tagged key of ANY
type — `? !!merge [x]` is a merge-tagged SequenceNode key that injects
`tools: [Read, Bash]` while the scalar-only check returned false. The tag
is now checked on every node before dispatching on type.
- P2: the recursive node walk (and yaml.compose) could raise RecursionError
on pathologically deep valid YAML, crashing the lint with a traceback.
Both now map to fail-closed (compose RecursionError -> None -> error;
walk RecursionError -> True).
Suite 51 -> 53 with merge-tagged-complex-key (seq + map) and
deep-nesting-fails-closed witnesses. CHANGELOG updated (five rounds, 53
tests, full fail-open list).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPGvAbphbuZ4mhx2Ketj7s
* test(lint): #524 round-6 self-probe witnesses — no residual fail-open or false-positive
Adds regression anchors for the round-6 adversarial probes, all confirmed
correct without code change: lowercase `bash` / `BashOutput` are distinct
non-shell tool names (not flagged); `yaml.compose` does not intern
identical scalars, so the alias-by-shared-identity detector has no
false-positive on a clean file repeating a value; a non-string manifest
`agents` key does not break reconciliation of the real Bucket A agents.
Suite 53 -> 56.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPGvAbphbuZ4mhx2Ketj7s
* test(lint): #524 make recursion tests Python-version-independent (CI 3.14 fix)
The deep-nesting test assumed 400 flow-sequence levels trip RecursionError,
but Python 3.14 tolerates far deeper nesting than 3.11, so compose
succeeded and the (clean, merge/alias-free) file legitimately passed —
red-lighting the test on CI's 3.14 while local 3.11 was green. A depth
that does not exceed the interpreter limit is a harmless file and SHOULD
pass. Split into: (1) does-not-crash (contract = check() returns, any
depth), (2) compose-RecursionError->None via monkeypatch, (3)
walk-RecursionError->fail-closed via an iterable that raises on iteration.
All three are deterministic and version-independent. Suite 56 -> 58.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPGvAbphbuZ4mhx2Ketj7s
* fix(lint): #524 round-6 — anchor byte-witness to composed key line (block-scalar false-positive)
codex gpt-5.6-sol xhigh round-6 found one non-blocking false-positive (no
fail-open): _raw_tools_lines text-scanned every frontmatter line for
`tools:`, so a clean allowlisted file whose `description: |` block scalar
documents a `tools: ...` line produced a spurious second match and failed
CI. Replaced with _raw_tools_line anchored to the composed `tools` key
node's start_mark.line (+ _key_nodes helper) — a `tools:`-looking line
inside a block scalar is no longer mistaken for the key. Semantics for
real drift (CRLF, whitespace, escaped/tagged spelling) unchanged; the
node-tree semantic check remains the security floor. Suite 58 -> 59.
CHANGELOG updated (six rounds, 59 tests, convergence: r6 = one
false-positive fixed, zero fail-open).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPGvAbphbuZ4mhx2Ketj7s
* fix(lint): #524 round-7 — rglob nested agents + byte-witness anchored to start_mark.index
codex gpt-5.6-sol xhigh round-7 found two real issues:
- P1 (fail-open): invariant 2's glob("*.md") only scanned immediate
children of each AGENT_DIRS entry, so a nested agents/subdir/x.md with a
Bucket A name + Bash would be missed while the runtime guard (keying on
name regardless of path) would still fence it inconsistently. Now
rglob("*.md"). No nested agent files exist today; this is a forward
guard.
- P2 (byte-witness weakening / false-positive): the witness read the raw
line via start_mark.line indexed into split("\n"), but YAML counts the
Unicode line breaks NEL (U+0085) / LS (U+2028) / PS (U+2029) that
split("\n") does not — the two diverge, which could false-reject an exact
line or let a non-verbatim key line pass. Now anchored to
start_mark.index (byte offset), sliced to the surrounding physical-\n
line, so the two agree by construction. The node-tree semantic check
remains the security floor regardless.
Suite 59 -> 61 with nested-Bucket-A-Bash and Unicode-line-break witnesses.
CHANGELOG updated (seven rounds, 61 tests, full fail-open + false-positive
list).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPGvAbphbuZ4mhx2Ketj7s
* fix(lint): #524 round-8 — fail closed on directory symlinks + bare-CR frontmatter
codex gpt-5.6-sol xhigh round-8 found two more fail-opens:
- P1: rglob does not descend into directory symlinks, so a tracked
agents/nested -> ../payload could hide a Bucket A .md declaring Bash.
Invariant 2 now fails closed on any directory symlink under an agent dir
(these hand-authored trees have no reason for one). Real repo has none.
- P1: _frontmatter split only on \n, so bare-\r (old-Mac) frontmatter —
which YAML parses fine — was read as absent and skipped, hiding a Bucket
A tools: Bash. Fences are now found with splitlines() (recognizes \n,
\r\n, bare \r, and Unicode breaks); the block is still the byte-faithful
substring so compose sees true bytes and the byte-witness (start_mark.index)
stays exact — a bare-CR/CRLF file still fires the witness as drift.
Suite 61 -> 63 with dir-symlink and bare-CR witnesses. CHANGELOG updated
(eight rounds, 63 tests).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPGvAbphbuZ4mhx2Ketj7s
* fix(lint): #524 round-9 — fold invisible/homoglyph tool tokens so padded Bash can't slip
codex gpt-5.6-sol xhigh round-9 (via its own probe) surfaced a real
fail-open in invariant 2: the Bash check is an exact-string membership
test (`"Bash" in declared`), and str.strip() does NOT remove Unicode
format characters (category Cf). So a fenced Bucket A agent declaring
tools: Read, Bash (BOM U+FEFF padding)
tools: Read, Bash (zero-width space U+200B)
tools: Read, Bash (fullwidth homoglyph)
kept the token distinct from `Bash`, passed the lint GREEN, yet reads as
Bash to the eye and to any NFKC-normalizing consumer — exactly the
symmetric-re-add bypass the lock exists to stop.
Fix: new _fold_token strips Cf format chars then NFKC-normalizes each base
tool token before comparison. All six real tool names + the Bash(git:*)
permission form are pure ASCII and pass through byte-identical, so no
legitimate value is altered. Folding lives in _normalized_tools, so it
protects BOTH invariants; invariant 1's additive byte-witness is confirmed
to STILL fire on an invisible-char canonical value (folding the semantic
check did not open a hole there).
Suite 63 -> 68: BOM / ZWSP / ZWNJ / fullwidth padded-Bash witnesses + a
byte-witness-still-fires regression for the invariant-1 companion. CHANGELOG
updated (nine rounds, 68 tests, invisible-char fail-open listed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPGvAbphbuZ4mhx2Ketj7s
* fix(lint): #524 round-10 — fold before the permission-specifier split so fullwidth-paren Bash can't slip
codex gpt-5.6-sol xhigh round-10 found an ordering corollary of the r9
fold: _fold ran AFTER the ASCII "(" split, so a fullwidth-paren specifier
tools: Read, Bash(git:*) (U+FF08 / U+FF09)
was never split (ASCII "(" misses the fullwidth "("), leaving the whole
"Bash(git:*)" as the base token; NFKC then folded it to "Bash(git:*)"
(one token) which != "Bash", so the membership test missed it and the
lint passed green — while an NFKC-normalizing runtime consumer would read
a valid Bash(git:*) grant.
Fix: fold each item FIRST (renamed _fold_token -> _fold, now content-only,
no internal strip), THEN split on ASCII "(" and strip. Fullwidth parens
fold to ASCII before the split, so Bash(git:*) and Bash(git:*) both
reduce to "Bash". ASCII Bash(git:*), the canonical five, and BashOutput
are all unchanged.
Suite 68 -> 70: fullwidth-paren specifier + fullwidth-name-and-paren
witnesses. CHANGELOG updated (ten rounds, 70 tests, ordering corollary
listed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPGvAbphbuZ4mhx2Ketj7s
* fix(lint): #524 round-11 — fold the whole tools value before ANY split so fullwidth-comma Bash can't slip
codex gpt-5.6-sol xhigh round-11 found the comma-side twin of the r10
paren corollary: _fold still ran per-item AFTER value.split(","), so a
fullwidth comma
tools: Read,Bash (U+FF0C)
kept "Read,Bash" as one item through the ASCII comma split; folding the
item then yielded "Read,Bash" (one token) != "Bash", so the membership
test missed it — while an NFKC-normalizing consumer would split on the
folded comma and grant Bash.
Root cause is the r10 class generalized: splitting on ASCII separators
before folding. Fix: fold ONCE up front — the whole string in the comma
form (so U+FF0C and U+FE50 become "," before split), and each list member
whole in the list form (a fullwidth comma inside a YAML list element is
not a separator, so it correctly stays one token). Then split on "," and
"(". ASCII values, the canonical five, and BashOutput are all unchanged.
Suite 70 -> 73: fullwidth-comma + small-comma + combined
comma/name/paren witnesses. CHANGELOG updated (eleven rounds, 73 tests,
both separator corollaries listed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPGvAbphbuZ4mhx2Ketj7s
* test(lint): #524 round-12 — pin the separator-fold convergence boundary as a non-bug
Round-12 review: codex's automated pass tripped its own cybersecurity
content filter mid-run (it went spelunking in the Claude Code binary) and
returned no verdict, so I resolved convergence by first-party analysis
instead.
Exhaustively scanned the residual surface the r9-r11 fold left open:
(A) every NFKC-stable alternate separator (ideographic/Arabic commas
U+3001/U+060C, division/fraction slashes, semicolons, middle dots) — none
can isolate a bare "Bash" token, because they do NOT fold to the ASCII
","/"(" the split (or any NFKC-normalizing consumer) honors, so
"Read、Bash" stays ONE non-Bash token for everyone (proven: it is a single
YAML scalar, and this lint splits only on ASCII ","); (B) zero codepoints
NFKC-decompose INTO "ash" (no false-positive/hiding risk); (C) the
per-letter compat variants are already folded whole; (D) zero non-Cf
codepoints fold to empty (no token-merging attack). The fullwidth
comma/paren cases fail precisely because NFKC folds THEM into ASCII
separators; the stable ones cannot, so not flagging them is correct.
Added test_nfkc_stable_alt_separator_is_not_a_bash_grant documenting this
boundary as a NON-bug (suite 73 -> 74) so a future maintainer does not
"fix" it by over-broadening the separator set into false positives.
CHANGELOG updated (twelve rounds, 74 tests, convergence boundary recorded).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPGvAbphbuZ4mhx2Ketj7s
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
16 KiB
ARS Performance Notes
Recommended model: the current frontier Claude model (Fable 5 at the time of writing) with Max plan (or equivalent configuration). Current Claude models use adaptive thinking; you no longer set a fixed thinking budget.
The full academic pipeline (10 stages) consumes a large amount of tokens — a single end-to-end run can exceed 200K input + 100K output tokens depending on paper length and revision rounds. Budget accordingly.
Individual skills (e.g.,
deep-researchalone, oracademic-paper-revieweralone) consume significantly less.
Estimated token usage by mode
| Skill / Mode | Input Tokens | Output Tokens | Estimated Cost |
|---|---|---|---|
deep-research socratic |
~30K | ~15K | ~$0.60 |
deep-research full |
~60K | ~30K | ~$1.20 |
deep-research systematic-review |
~100K | ~50K | ~$2.00 |
academic-paper plan |
~40K | ~20K | ~$0.80 |
academic-paper full |
~80K | ~50K | ~$1.80 |
academic-paper-reviewer full |
~50K | ~30K | ~$1.10 |
academic-paper-reviewer quick |
~15K | ~8K | ~$0.30 |
| Full pipeline (10 stages) | ~200K+ | ~100K+ | ~$4-6 |
| + Cross-model verification | +~10K (external) | +~5K (external) | +~$0.60-1.10 |
Estimates based on a ~15,000-word paper with ~60 references. Actual usage varies with paper length, revision rounds, and dialogue depth. Costs measured on Opus 4.x at Anthropic API pricing as of April 2026 — treat as order-of-magnitude anchors under newer models rather than exact quotes.
v3.11 citation verification (#182). The deterministic citation-existence gate calls external bibliographic APIs (Semantic Scholar / OpenAlex / Crossref / arXiv), not the LLM, so it adds no Claude token cost to the figures above — only network latency on first lookup. The persistent SQLite cache (
~/.cache/ars/verification.db, 90-day TTL) means each paper is verified once and reused across drafts; a re-run over an already-cached bibliography does no network work. See SETUP.
Recommended Claude Code settings
| Setting | What it does | How to enable | Docs |
|---|---|---|---|
| Agent Team (optional) | Enables TeamCreate / SendMessage tools for manual multi-agent coordination. ARS's internal parallelization does not require this flag — skills spawn subagents via the built-in Agent tool directly. Only useful if you want to manually orchestrate persistent team workflows across sessions. |
Set CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 (research preview) |
Experimental feature — no stable docs yet |
| Auto mode (recommended) | Auto-accepts most tool actions so long pipeline runs keep moving, while a server-side classifier still blocks actions that escalate beyond what you asked for (e.g. production deploys, force-pushes or direct pushes to main, data exfiltration). Explicit ask rules and classifier blocks can still prompt. The middle ground between manual approval and zero checks. | Launch with claude --permission-mode auto (when available), or set "permissions": { "defaultMode": "auto" } in ~/.claude/settings.json; verify the active mode after startup (research preview) |
Permission modes |
| Skip Permissions | Skips routine tool-use confirmations with no safety checks. Faster than auto mode but removes all guardrails. Intended for ephemeral isolated sandboxes without internet access, not real development machines. | Launch with claude --dangerously-skip-permissions (equivalent to --permission-mode bypassPermissions) |
Permission modes |
⚠️ Choosing a mode: For most unattended pipeline runs, auto mode is the recommended setting. It keeps most long runs moving while a classifier gates dangerous escalations, though ask rules and classifier blocks can still prompt. Auto mode is a research preview: it does not guarantee safety and is not a replacement for human review on sensitive operations, and its behavior may change. Skip Permissions removes that safety net entirely and should only be used in an isolated sandbox without internet access, where you are comfortable with Claude executing file reads, writes, and shell commands with no checks.
v3.7.0 Plugin agents and model routing
When ARS is installed as a Claude Code plugin (/plugin install academic-research-skills), three downstream worker agents are exposed as plugin-shipped subagents: synthesis_agent, research_architect_agent, and report_compiler_agent. Each declares model: inherit in its frontmatter, which means they run under the dispatching session's model rather than a pinned floor:
- An Opus session running the full pipeline gets Opus agents, preserving the integrative depth those agents were designed for.
- A Sonnet session gets Sonnet agents, matching the cost/latency profile of the parent run.
- The agents never silently fall back to Haiku —
inheritresolves through the parent session's model, which is itself gated by the project policy of "no Haiku for ARS runs."
Since #514 (shipped in #521), each of the three also carries a pinned tools allowlist in the same frontmatter — tools: Read, Write, Edit, Grep, Glob, no shell and no network fetch — so dispatch-time capability is least-privilege; the exact value is CI-locked by scripts/check_tools_allowlist.py (#524).
This means plugin-agent token costs track the per-mode estimates above unchanged (with ARS_MODEL_TIERING unset); there is no separate plugin agent surcharge or discount, because dispatched agents inherit the same model the parent run already pays for. Under ARS_MODEL_TIERING=economy, plugin-exposed execution-type agents (e.g. report_compiler_agent) follow the tiering rule instead — one tier below the session model, floor Opus-class (see shared/model_tiering.md). If you change the main session model mid-pipeline (e.g., downshift to Sonnet for a long revision pass), the next agent dispatch picks up the new floor automatically.
Other ARS agents (bibliography_agent, literature_strategist_agent, etc.) are not plugin-exposed in v3.7.0; they remain in-skill prompt templates that the main session executes inline, with no separate model routing layer by default. The opt-in ARS_MODEL_TIERING switch (#517) adds a dispatch-time routing rule on top: when a tiering direction applies to a role, the session dispatches it as a subagent pinned to the target tier (inline roles included — dispatch-as-subagent is the mechanism); with the flag unset, this paragraph describes behavior unchanged. See shared/model_tiering.md. Wider plugin-agent coverage is deferred to a future release.
Long-running session management
The full academic pipeline is designed for human-in-the-loop execution, with mandatory user confirmation at every stage. In practice, a full run often spans hours to days — longer than Anthropic's prompt cache TTL (5 minutes). Two consequences:
- Cache misses between checkpoints are normal. When a stage checkpoint pauses longer than 5 minutes, the next stage reads its context uncached. This is an unavoidable cost of human-paced pipelines.
- Cross-session resume relies on Material Passport. ARS does not maintain its own orchestrator state between sessions. To resume in a new session, paste your Material Passport YAML back; the orchestrator reads
compliance_history[]and stage completion markers to locate your breakpoint.
v3.6.2 Sprint Contract reviewer cost (always-on for full / methodology-focus)
The Schema 13 sprint contract gate splits each reviewer agent's run into Phase 1 (paper-content-blind, commits scoring plan) + Phase 2 (paper-visible review). For modes that ship templates (full panel 5 + methodology-focus panel 2), each reviewer therefore costs roughly two LLM turns instead of one. Reserved modes (re-review / calibration / guided / quick) keep pre-v3.6.2 behaviour.
| Skill / Mode | Effect on tokens | Notes |
|---|---|---|
academic-paper-reviewer full |
~+30-40% input + small output bump per reviewer × 5 reviewers | Each reviewer reads the contract template + paper metadata in Phase 1, then full paper in Phase 2 |
academic-paper-reviewer methodology-focus |
Same shape, panel 2 | Two reviewers (EIC + methodology) each run two phases |
| Synthesizer (always one) | +~2-3K input | Reads contract + reviewer outputs to run three-step mechanical protocol |
Empirical measurement pending real review runs at scale. The two-phase shape is non-optional for the gated modes; treat as fixed overhead, not a tunable.
v3.4.0 compliance agent cost
Adding the mode-aware compliance_agent to Stage 2.5 and Stage 4.5 increases full-pipeline SR tokens by approximately:
| Skill / Mode | Input Tokens | Output Tokens | Estimated Cost |
|---|---|---|---|
deep-research systematic-review (2.5 only) |
+~5–8K | +~3–5K | +~$0.15 |
| Full pipeline SR (2.5 + 4.5) | +~10–15K | +~5–8K | +~$0.30 |
academic-paper full (pre-finalize) |
+~3–5K | +~2–3K | +~$0.08 |
These are on top of the existing per-skill costs in the table above (same 15,000-word / 60-reference basis; see footnote on line 23). Cross-model verification costs (if enabled) are unchanged.
v3.6.3 Passport reset boundary (opt-in)
When ARS_PASSPORT_RESET=1 is set, every FULL checkpoint becomes a context-reset boundary. The intended workflow is:
- Run a stage to FULL checkpoint in session A.
- Copy the
[PASSPORT-RESET: hash=<hash>, stage=<completed>, next=<next>]tag from the checkpoint notification. - Start a fresh Claude Code session (session B) and paste
resume_from_passport=<hash>. Optional overrides:resume_from_passport=<hash> stage=<n> mode=<m>. - Session B loads only the passport ledger; no replay of session A's turns. The orchestrator locates the matching
kind: boundaryentry, appends akind: resumeentry to consume it, and continues. The resumed stage is determined by: astage=CLI override if supplied, else the matched option'snext_stagewhen the boundary carries apending_decision(the orchestrator re-prompts the user first), else the recordednextfield.nextMAY benullwhen all decision branches terminate.
When reset beats continuation:
- Long pipelines where session A has accumulated >100K input tokens of context that the next stage does not actually need.
systematic-reviewmode runs where stage independence is cleanly defined by the Material Passport.- Any case where you hit the 5-minute prompt-cache TTL mid-pipeline; a reset lets the next stage start fresh instead of paying a cache miss on a bloated context.
When continuation still wins:
- Short pipelines (< 30K input tokens end-to-end).
- Stages with implicit in-session state that the passport does not capture (e.g., a Socratic dialogue branch the user wants to keep warm).
- When the flag is OFF, continuation is the unchanged pre-v3.6.3 default.
Passport file location convention:
By default, the orchestrator looks for the passport file in ./passports/<slug>/ or matching ./material_passport*.yaml relative to the current working directory. Resolving the hash to a passport file on disk is the integrator's responsibility; the orchestrator loads whichever passport the enclosing tool provides. See §"Passport file location convention" above for the ./passports/<slug>/ default.
The resume command only defines the hash and optional stage/mode overrides:
resume_from_passport=<hash> [stage=<n>] [mode=<m>]
There is no path syntax on the resume command itself. Custom passport locations are configured in the project's CLAUDE.md or handled by the integrator's tooling before the orchestrator is invoked.
Empirical token savings: measurement pending a real systematic-review run with instrumentation. This section will be updated with observed token deltas once available; until then, no numeric claim is made. See ../academic-pipeline/references/passport_as_reset_boundary.md for the full protocol.
Literature corpus ingestion (v3.6.4+)
The Material Passport literature_corpus[] field is populated by user-written adapters, not ARS itself. Three reference adapters ship with v3.6.4: scripts/adapters/folder_scan.py, scripts/adapters/zotero.py, scripts/adapters/obsidian.py. See scripts/adapters/README.md for how to run them and how to write your own.
Performance posture
- Adapters run out-of-band (before an ARS session, not during). Their runtime is the user's problem, not ARS's.
- Adapters must be deterministic: re-running on identical input produces byte-identical output modulo timestamps.
literature_corpus[]entries are sorted bycitation_key; rejections are sorted bysource.- Adapter output size grows linearly with corpus size. A 500-entry Zotero library typically produces a passport of ~300 KB YAML. ARS consumers should lazy-load when the corpus is large.
Ingestion-layer boundaries
- Does not ingest PDFs, extract text, or run OCR.
- Does not call the Zotero Web API, Notion API, or any live service.
- Does not fetch paywalled content or use user credentials to access institutional libraries.
These boundaries are deliberate and reflect the ARS data-layer decision: ARS is a writing/review-layer framework; corpus integration stays in user-owned code. Users who want API-based live-sync adapters are expected to write them themselves, using the three reference adapters as starting points.
Consumer-side integration
As of v3.6.5, two Phase 1 literature agents read literature_corpus[] via the corpus-first, search-fills-gap flow: deep-research/agents/bibliography_agent.md and academic-paper/agents/literature_strategist_agent.md. Both consumers follow the same five-step shared flow and four Iron Rules (Same criteria / No silent skip / No corpus mutation / Graceful fallback on parse failure). Search Strategy reports gain a PRE-SCREENED reproducibility block that enumerates included / excluded / skipped corpus entries with F3 zero-hit and F4 provenance reporting. Consumer integration is presence-based — auto-engages when the passport carries a non-empty literature_corpus[] and parses cleanly; parse failures fall back to external-DB-only flow with a [CORPUS PARSE FAILURE] surface.
See academic-pipeline/references/literature_corpus_consumers.md for the full consumer protocol. citation_compliance_agent corpus integration is deferred (target version TBD post-v3.8).
v3.6.5 corpus consumer cost (presence-gated)
When the Material Passport carries a non-empty literature_corpus[], Phase 1 reads scale with corpus size. The PRE-SCREENED block emit itself is prompt-layer (effectively free); the LLM cost is Step 1 pre-screening — applying the current Inclusion / Exclusion criteria to each corpus entry's title (always present) and any populated optional fields (abstract / tags).
| Corpus size | Step 1 pre-screening (per consumer) | Notes |
|---|---|---|
| Empty / absent | 0 | External-DB-only flow runs unchanged |
| ~50 entries (typical Zotero subset) | +~3-5K input + ~1-2K output | Title + abstract scan |
| ~200 entries | +~10-15K input + ~3-5K output | Title-only scan dominates; abstract scan only when populated |
| ~500 entries (large library) | +~25-40K input + ~8-12K output | Consider trimming the corpus before passport emit |
Step 2 search-fills-gap reduces external-DB cost when uncovered_topics is small (case A), which can offset Step 1 cost. Empirical net delta pending real systematic-review run instrumentation; until then, no aggregate numeric claim is made. Parse failures cost roughly one short turn (parse + emit [CORPUS PARSE FAILURE] + fall back).