core
The shared foundation every ADLC tool imports: exit-code helpers, provider selection, and the ledger, ticket, rail, and mutation primitives.
core
Package: @adlc/core · Role: the shared library every gate imports.
What it is
@adlc/core is the foundation the whole toolkit stands on: every gate imports
from it, and none keeps a private copy of what it exports. When a second tool
needs a helper, the helper is promoted into core, with a test and no new
runtime dependency, rather than copied; a defect in a core primitive is fixed
in core so every caller gets the fix. That discipline is what keeps
twenty-plus independently published tools behaving identically at their
boundaries.
Downstream tools inherit five things from core: the exit-code convention, LLM provider selection, the append-only ledger, the ticket/rail/glob engine, and the mutation operators used by prosecution.
Exit-code convention
This is the contract. Every gate that builds on core reports its result the same
way, so an orchestrator, hook, or CI job can branch on the exit code without
parsing output. Core exposes it as three helpers: pass(), opError(), and
gateFail().
pass(). The gate passes or the command succeeds.1: opError(). Operational error (bad input, unreadable file, failed LLM call).2: gateFail(). The gate fails (the finding is real).The split between 1 and 2 matters: an operational error means the gate couldn't run, while a gate failure means it ran and found a problem. Collapsing them would let a broken environment read as a clean pass.
Provider selection
Tools that run an LLM pass do not embed provider logic. They call core's
detectProvider / complete / fan helpers, which auto-detect a provider from
the environment. Detection runs in a fixed priority order (cheapest and lowest
latency first, per ADR-0007), matching the first provider whose key is present:
| Order | Provider | Env var |
|---|---|---|
| 1 | anthropic | ANTHROPIC_API_KEY |
| 2 | openai | OPENAI_API_KEY |
| 3 | gemini | GEMINI_API_KEY |
| 4 | agy | ADLC_AGY (Antigravity CLI subprocess; feature-flag gated, last so API-key providers win) |
Force a specific provider with ADLC_PROVIDER (env) or a per-invocation
--provider flag, which takes precedence over the env var. Model tiers
(cheap / mid / frontier) resolve to provider-specific model ids and can be
overridden with ADLC_MODEL_CHEAP, ADLC_MODEL_MID, and ADLC_MODEL_FRONTIER.
The keyless --prompt-only philosophy
No ADLC gate requires an API key to be useful. Every LLM-backed tool exposes
--prompt-only, backed by core's promptOnly() helper: it prints the exact
prompt the tool would send and exits 0. You can paste that prompt into any
harness (a chat window, a different provider, an air-gapped review) and feed
the answer back. Keys make the gate automatic; they are never a gate to entry.
Rail-engine primitives
The rest of core is the deterministic machinery the gates share:
- Ledger (
.adlc/):appendEntry/readEntriesback the append-only gate evidence, withsha256/hashFilesfor content hashing. Malformed lines are reported, never silently swallowed. - Tickets & rails (
.adlc/tickets.json):loadTickets,validateTicket,topoSort, andcomputeFloat(critical-path method) turn the ticket graph into an executable, dependency-ordered plan. - Scope & glob:
globMatch(*,**),inScope, andscopesOverlapare the conservative matchersrails-guarduses to decide whether an edit is inside a ticket's declared scope. - Git:
gitDiff,changedFiles,isDirty, pluscoChangeandchurnfor the logical-coupling signals hot-file and merge-forecast tools read. - Mutation:
mutate.OPERATORS(invert-comparison, bool-flip, null-return, off-by-one, logic-swap) andgenerateMutants/applyMutantpowerhollow-test,review-calibration, andgate-fuzzing.
test-kit (@adlc/core/test-kit)
Fixture helpers for node:test suites; typed by lib/test-kit.d.mts.
tmp(t, prefix?)→ a fresh realpath'd directory underos.tmpdir(), removed by a hook registered ont.after()(retrying transientENOTEMPTY/EBUSY,FIXTURE_RM_OPTIONS). Fails closed: atwithout a callable.after(none,null, a prefix string, an options object, or the context a describe-levelbefore()receives) throws aTypeErrorand creates nothing.gitRepo(t, options?)→{ dir, git, g }: a repo insidetmp(t)with a test identity,commit.gpgsign=false,gc.auto=0andgc.autoDetach=false(no detached maintenance child writing into.gitduring removal).optionsis{ prefix, branch, userEmail, userName }or a prefix string. Same context rule astmp.createScope()→ a context ({ after, dispose }) for fixtures that outlive one test callback: create inbefore(),await scope.dispose()inafter(). Registering on a disposed scope throws.withScopedContext(fn)→ runsfn(ctx)with a fresh scope and disposes it whenfnsettles; for code that calls test functions outside anode:testcallback.runBin(binPath, args?, options?)→spawnSyncof a Node script withDEFAULT_SCRUBBED_ENV(signing keys, bypass flags) removed unlessallowEnvnames them.
Go deeper
The full import surface: packages/core.