Deterministic context planning for coding agents.
Aperture runs before your coding agent. Given a task description and a repository, it picks the files the agent should load — in full, as a structural summary, as a behavioral summary, or merely as "reachable" — fits that selection into a token budget, flags missing information, scores how feasible the task looks, and emits a reproducible, hashed manifest the agent can consume.
It is not a coding agent. It is not a RAG system. It is a compiler front-end for context selection: deterministic, explainable, cacheable, and gated.
Coding agents fail in three predictable ways:
- Too much context. They get the whole repository dumped in, waste
tokens on
vendor/,node_modules/, and machine-generated files, and lose the plot. - Too little context. They get a single file, miss the callers, miss the config, and hallucinate interfaces that don't exist.
- Wrong context. They get something that looks relevant — same directory, same verb in the name — but the actual target lives two packages over.
Aperture fixes all three in the same way: by reading the task carefully, scoring every candidate file against a deterministic weighted formula, assigning each candidate a load mode, and fitting the whole thing into an explicit token budget — and then writing down exactly what it did and why.
Concretely, Aperture gives you:
- Reproducibility. Identical task + identical repo + identical config
produces a byte-identical
manifest_hash. The manifest FILE itself still carries per-run fields (generated_at,host,pid,wall_clock_started_at) for operator diagnostics, but those are excluded from the hash input — the planning decisions are stable even when the surrounding metadata isn't. - Explainability. Every selection, every load mode, every exclusion,
every gap carries rationale metadata.
aperture explainrenders the whole decision tree as plain text. - Rule-based gap detection. Nine categories (missing spec, missing tests, missing config, unresolved symbol, ambiguous ownership, missing runtime path, missing external contract, oversized context, task underspecified) — all rule-based, none LLM-driven.
- Feasibility scoring. A 0.0–1.0 score with numeric sub-signals (coverage, anchor resolution, task specificity, budget headroom, gap penalty), capped at 0.40 when any blocking gap fires.
- Threshold gates. Fail a plan with exit 7 when feasibility is below
--min-feasibility, exit 8 when a blocking gap fires with--fail-on-gaps, exit 9 on budget underflow. Downstream CI can cut a run before an agent burns tokens on an unwinnable task. - Tokenizer-aware budgeting. Counts real tokens for the target
model:
cl100k_base/o200k_base/p50k_base/r50k_basefor OpenAI families (embedded, no network), conservativeceil(len/3.5)for Claude and unrecognized models. - Warm-cache speed. A persistent AST cache under
.aperture/cache/means a second plan on the same tree runs in ~30 ms on 500 files, ~320 ms on 5 000 files. - Agent integration. Plan → write a merged markdown manifest +
task file → invoke
claudeorcodexwith the prompt piped on stdin. User-declared custom adapters in.aperture.yamlwork the same way.
If forced to choose between "clever" and "predictable," Aperture chooses predictable.
- Go 1.23 or newer.
- A POSIX shell for
maketargets (WSL or Git Bash on Windows). - Optional, only if you invoke
aperture run <name>: the adapter binary for<name>must already be on$PATH. Aperture does not install or bundleclaude/codex/ any user-declared adapter — it only orchestrates them. For the built-in adapters, see the Claude Code CLI and the OpenAI Codex CLI respectively;aperture planandaperture explainneed none of these.
git clone https://github.com/dshills/aperture.git
cd aperture
make build # builds bin/aperture (version-stamped)Or install directly:
# Go default ($GOBIN or $GOPATH/bin):
make install
# Explicit destination:
make install INSTALL_DIR=/usr/local/bin
make install INSTALL_DIR=~/.local/bin # ~ expands via /bin/shaperture version prints the semver, git commit, and build timestamp.
go install github.com/dshills/aperture/cmd/aperture@latest(No ldflag stamping this way — the binary reports dev / unknown /
unknown. Use make install for a stamped build.)
Given any Go repository:
# 1. Write the task as a file or pass it inline.
cat > TASK.md <<'EOF'
# Add OAuth refresh
Add OAuth refresh handling to the GitHub provider in
internal/oauth/provider.go. Include unit tests and update the
README.
EOF
# 2. Plan. Emits a JSON manifest on stdout.
aperture plan TASK.md --model claude-sonnet --budget 120000
# 3. Read it.
aperture plan TASK.md --format markdown --out .aperture/plan.md
# 4. Explain it.
aperture explain TASK.md
# 5. Run an agent with it.
aperture run claude TASK.md --fail-on-gaps --min-feasibility 0.65The run command:
- Walks the repo (caching AST analysis under
.aperture/cache/). - Scores every file against the task.
- Assigns each file a load mode (
full,structural_summary,behavioral_summary,reachable). - Detects nine classes of gaps.
- Emits the JSON manifest + the Markdown manifest + a merged
run-<id>.mdprompt under.aperture/manifests/. - Invokes
claude --print --permission-mode bypassPermissionswith the merged prompt piped on stdin.
Section anchors like §7.4.2.1 or §7.7.3 point at the normative specification at
specs/initial/SPEC.md. The README summarizes what those sections say; the SPEC is the source of truth for every behavior Aperture must preserve.
Every selected file carries one load mode:
| Mode | Meaning |
|---|---|
full |
Raw content should be loaded verbatim. Used for highly relevant, reasonably-sized, central files. |
structural_summary |
Package / types / interfaces / functions / imports. For Go files where architecture matters more than exact content. |
behavioral_summary |
Imports + side-effect tags + exported API surface + test relationships + size band. Deterministic, no LLM. |
reachable |
Not loaded by default; surfaced as discoverable follow-up context. Doesn't consume the budget. |
Each file scores 0.0–1.0 via a weighted sum of eight factors:
| Factor | Default weight | What it measures |
|---|---|---|
s_mention |
0.25 | Task text contains the file's path or basename |
s_filename |
0.12 | Jaccard similarity between basename tokens and anchor set |
s_symbol |
0.20 | Anchors matching exported Go identifiers (case-insensitive substring) |
s_import |
0.12 | Two-pass: the file imports packages that score highly on other factors |
s_package |
0.10 | File's package path or sibling matches an anchor |
s_test |
0.08 | Test file associated with a high-scoring production file |
s_doc |
0.07 | Jaccard of doc token bag (first 2 KiB) vs. anchor set |
s_config |
0.06 | Config-shaped filename, weighted by action type |
Weights sum to exactly 1.0. Override in .aperture.yaml under
scoring.weights (sum must remain 1.0 ± 0.001). The resolved weight
set is part of the manifest hash.
Derived from the task text via an ordered rule table:
| Priority | Type | Triggered by (any of) |
|---|---|---|
| 1 | bugfix |
fix, bug, broken, regression, crash, panic, error is, fails to, should not, incorrect |
| 2 | test-addition |
add tests, write tests, test coverage, unit tests, integration tests, missing tests |
| 3 | documentation |
document, docs, readme, comments, godoc, javadoc |
| 4 | migration |
migrate, migration, upgrade, downgrade, backfill, rename column, drop column, schema change |
| 5 | refactor |
refactor, rewrite, restructure, clean up, cleanup, extract, split, deduplicate |
| 6 | investigation |
investigate, explore, understand, research, look into, diagnose, why does, how does |
| 7 | feature |
add, implement, support, introduce, new, create, enable |
| 8 | unknown |
default |
Priority is strict: "investigate why the new fix breaks" classifies as
bugfix because rule 1 wins over rule 6.
| Category | Fires when |
|---|---|
missing_spec |
Feature/refactor/migration/investigation task + no SPEC.md / AGENTS.md found |
missing_tests |
Feature/bugfix/refactor/migration + no _test.go selected at score ≥ 0.50 |
missing_config_context |
Task mentions config/env/settings + no config file selected |
unresolved_symbol_dependency |
Task names a Go identifier that isn't exported anywhere in the repo |
ambiguous_ownership |
Top-scoring file's package has ≥2 peers over 0.60, no clear owner ≥0.80 |
missing_runtime_path |
Feature/bugfix/migration + runtime anchors + no selected file carries an io:* side-effect tag |
missing_external_contract |
Task mentions API/RPC/schema + no *openapi* / *swagger* / *schema* / *api* file selected |
oversized_primary_context |
Budget underflow, or a highly-relevant file was demoted from full |
task_underspecified |
No candidate reaches score 0.60. (The SPEC lists three triggers, but the anchors<2 and action=unknown triggers are already folded into feasibility's task_specificity sub-signal; firing the gap on them would double-count the same weakness once in the gap list and again in the feasibility math.) |
feasibility = clamp01(
0.40 · coverage
+ 0.25 · anchor_resolution
+ 0.20 · task_specificity
+ 0.15 · budget_headroom
) - gap_penalty
Clamped to ≤0.40 whenever any blocking gap fires. Bands:
≥ 0.85— high feasibility0.65–0.84— moderate0.40–0.64— weak< 0.40— poor
Every sub-signal value is emitted numerically in
manifest.feasibility.sub_signals so the score is auditable.
The manifest hash is sha256 over the compact, lexicographically-
key-sorted JSON form of the manifest with these fields stripped:
manifest_hash, manifest_id, generated_at, aperture_version,
host, pid, wall_clock_started_at. Identical inputs → identical
hash, even across Go toolchain versions. The test suite asserts this
over 20 consecutive runs.
Ordering is always ascending, byte-wise, over the normalized
repository-relative path — forward-slash separators, no leading ./,
NFC Unicode.
aperture plan [TASK_FILE] Generate a manifest
aperture explain [TASK_FILE | --manifest <path>] Render reasoning
aperture run <agent> [TASK_FILE] Plan and invoke an adapter
aperture cache clear Remove .aperture/{cache,index,summaries}
aperture version Print build identity
| Flag | Default | Notes |
|---|---|---|
--repo <path> |
cwd | Repository root. Honored verbatim. |
-p, --prompt <text> |
Inline task text; mutually exclusive with TASK_FILE. |
|
--model <id> |
config or unset | Drives tokenizer dispatch. |
--budget <int> |
config | Token ceiling. |
--format <json|markdown> |
json |
|
--out <path> |
stdout | |
--fail-on-gaps |
off | Exit 8 on any blocking gap. |
--min-feasibility <float> |
0 (off) | Exit 7 if score below. |
--config <path> |
<repo>/.aperture.yaml |
Same as plan, plus --out-dir <path> for where to persist the JSON
manifest, Markdown manifest, and merged run-<id>.md prompt
(defaults to .aperture/manifests/ under the repo).
The adapter sees these environment variables:
APERTURE_MANIFEST_PATH # absolute path to JSON manifest
APERTURE_MANIFEST_MARKDOWN_PATH # absolute path to Markdown manifest
APERTURE_TASK_PATH # task file or tempfile for inline tasks
APERTURE_PROMPT_PATH # merged prompt (Markdown + --- + task)
APERTURE_REPO_ROOT
APERTURE_MANIFEST_HASH # hex sha256, no "sha256:" prefix
APERTURE_VERSION
Plus anything in the agent's env: block in .aperture.yaml.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Unexpected internal failure |
| 2 | Invalid command-line arguments |
| 3 | Unreadable task file |
| 4 | Invalid repository root |
| 5 | Malformed .aperture.yaml |
| 6 | Manifest serialization or schema validation failure |
| 7 | --min-feasibility threshold not met |
| 8 | Blocking gap present with --fail-on-gaps |
| 9 | Budget underflow — the effective context budget is smaller than the smallest viable cost of the highest-scoring candidate. "Underflow" in the §7.6.5 sense: the budget runs out of headroom before ONE real selection can fit, not a token-count overflow. |
| 10 | Recognized-but-unsupported model (no tokenizer tables) |
| 11 | Unknown agent name in aperture run |
| 12 | Adapter failed to start (exec not found / permission denied) |
| * | Any other value: adapter's own exit code, propagated verbatim |
.aperture.yaml at the repo root (override with --config). All fields
optional:
version: 1
defaults:
model: claude-sonnet-4-6 # or gpt-4o, gpt-4, codex-*, o1-mini, …
budget: 120000
reserve:
instructions: 6000
reasoning: 20000
tool_output: 12000
expansion: 10000
# Added to the built-in defaults unless exclude_disable_defaults: true.
exclude:
- vendor/**
- node_modules/**
- "**/*.min.js"
# Set to true to REPLACE the default exclusions entirely.
exclude_disable_defaults: false
languages:
go:
enabled: true
markdown:
enabled: true
thresholds:
min_feasibility: 0.70
fail_on_blocking_gaps: true
output:
directory: .aperture/manifests
format: json
# Weights must sum to 1.0 ± 0.001.
scoring:
weights:
mention: 0.25
filename: 0.12
symbol: 0.20
import: 0.12
package: 0.10
test: 0.08
doc: 0.07
config: 0.06
# Promote any of the nine gap categories to blocking severity.
gaps:
blocking:
- oversized_primary_context
- missing_spec
agents:
claude:
command: claude
args: []
pass_task_as_arg: false # default for built-in claude/codex
mode: non-interactive # or "interactive"
codex:
command: codex
args: []
pass_task_as_arg: false
# User-declared adapter — pass_task_as_arg defaults to true here.
my-wrapper:
command: /usr/local/bin/my-wrapper
args: ["--mode", "plan"]
env:
MY_TOKEN: xyzUnknown top-level keys are rejected (exit 5). Weights that don't sum
to 1.0 within 0.001 are rejected. The fully-resolved config is hashed
into manifest.generation_metadata.config_digest, so a config change
changes the manifest hash.
Every emitted manifest is validated against schema/manifest.v1.json
before write. A validation failure exits 6 and nothing is persisted.
manifest_id vs manifest_hash. These two fields do different
jobs and should not be confused:
manifest_id(apt_<16 hex>) is a per-run correlation handle. It's derived from random bytes at generation time and differs on every run, even when the planning decisions are byte-identical. Use it to tie a manifest to its log lines, its merged-prompt file (run-<id>.md), and the adapter invocation it drove.manifest_hash(sha256:<64 hex>) is the semantic identity of the plan. It's computed over the normalized manifest with all per-run fields (manifest_id,generated_at,host,pid,wall_clock_started_at,aperture_version,manifest_hashitself) stripped. Identical inputs produce identical hashes across runs, across machines, and across patch-level Aperture builds. This is the field CI should pin, diff, and gate on — notmanifest_id.
reachable selections are files Aperture believes are relevant but
didn't have budget for — the caller should know they exist and can
pull them in on demand. They don't consume the token budget, but they
are not invisible either. aperture run wires them through to the
downstream agent in three places:
- JSON manifest
reachablearray. Each entry carries the path, the score, and arationaleexplaining why it was kept reachable rather than dropped (usually "budget exceeded" or "load-mode downgraded after full/summary didn't fit"). Agents that parse the manifest directly (viaAPERTURE_MANIFEST_PATH) get the full list. - Markdown manifest
## Reachablesection. The Markdown form renders the same list as a bulleted section below## Selections, with the score and one-line rationale per entry. This is the form LLM-based agents typically read, because it's part of the merged prompt. - Merged
run-<id>.mdprompt. The file passed on stdin to the adapter is the Markdown manifest concatenated with---and the task text. The reachable section appears verbatim in that prompt, so any downstream agent — whetherclaude,codex, or a custom wrapper — sees the reachable list without needing to open a second file.
This means a coding agent can ask for a reachable file by path
during its run and trust that Aperture already vetted it as relevant.
The contract is: reachable files are pre-approved context, they just
didn't fit this budget. They are not speculative suggestions and not
fallbacks — they're the same ranked candidate set as full and the
summary modes, re-routed because the ceiling binds.
Aperture runs locally by default. It makes no network calls on repo contents. Tokenizer tables are embedded at build time; there is no fallback to a remote fetch.
⚠ Trust model for
aperture run. Theagents.<name>.commandentries in.aperture.yamlare shell commands Aperture will execute with the user's own privileges. A hostile.aperture.yamlcan put arbitrary commands there (e.g. a PR that changesagents.claude.commandto a curl|sh). v1 has no automatic trust gate — a formal approval flow is deferred to a post-v1 release. Until then, the trust contract is:
aperture planandaperture explainare safe on any repo — they never exec an adapter.aperture run <name>is unsafe on untrusted input. Treat.aperture.yamlexactly like a Makefile: review it before invokingaperture runfrom a fresh clone or a PR branch.- In CI on fork PRs: use
aperture plan, neveraperture run. The adapter invocation path is explicitly designed to be skipped when the caller can't vouch for.aperture.yaml's provenance.
Inline tasks are written to $TMPDIR/aperture-task-<id>.txt with
O_CREATE|O_EXCL|O_WRONLY, so a symlink planted at the target path
fails the open rather than being followed. Cleanup runs in three
layers:
- Deferred cleanup on every normal return path.
- Signal handlers (
SIGINT,SIGTERM,SIGHUP) on Unix delete the tempfile before the process exits.SIGKILLintentionally bypasses this — the kernel can't be negotiated with, and - A 24-hour orphan sweep at the start of every
aperture runremovesaperture-task-*.txtfiles older than that threshold, bounding any SIGKILL leak (and covering Windows where step 2 is a no-op). The sweep is oneReadDirplus aStatper matching entry — O($TMPDIRsize) but with a narrow filename-prefix filter, and never fails the run.
Adapter commands run with the user's own privileges. Aperture does not escalate, sandbox, or restrict what the downstream agent does.
Measured via make bench on the reference fixtures:
| Fixture | Files | Cold plan | Warm plan | p95 |
|---|---|---|---|---|
| small | 500 | 86 ms | 31 ms | 33 ms |
| medium | 5 000 | 1129 ms | 322 ms | 326 ms |
SPEC §8.2 targets: small ≤10 s cold / ≤1 s warm; medium ≤60 s cold / ≤5 s warm. Both fixtures run roughly 100× under target on an Apple M-series; CI runners will be slower but still comfortably inside the gates.
The cache is keyed by sha256(path, size, mtime, selection_logic_version)
and stored as JSON sidecar files under .aperture/cache/. Binding to
selection_logic_version (currently "sel-v1") instead of the build
version means a docs-only or CLI-message patch bump of Aperture no
longer invalidates every AST parse on disk — only a change to the
§7.4/§7.6 scoring or selection rules bumps sel-v1 and wipes the
cache. A schema-drift sentinel at .aperture/cache/VERSION still lets
the binary invalidate the whole cache in one stat call when it sees an
older on-disk format.
make help # list all targets
make build # ./bin/aperture
make test # go test ./...
make lint # golangci-lint run ./...
make fmt # gofmt -s -w .
make bench-prepare # regenerate testdata/bench/{small,medium}/
make bench # run the §8.2 harness
make bench-clean # remove generated fixturesTests:
- Unit tests in every package.
- Property tests over randomized inputs (
task.Parsedeterminism,budget.Countmonotonicity,ExitCodeErrorwrap semantics). - Fuzz tests for the dispatch and sanitization boundaries
(
budget.Resolve,agent.WriteInlineTaskFileIn). - Golden tests pinning the §11.1 JSON shape and §7.9.3 Markdown section order.
- Determinism tests asserting 20 consecutive runs produce byte-identical normalized manifests.
go test -race ./...passes on the concurrent cache paths.
Run the fuzz targets locally:
go test -run '^$' -fuzz FuzzResolve -fuzztime 30s ./internal/budget/...
go test -run '^$' -fuzz FuzzWriteInlineTaskFile_ManifestIDIsPathConstrained \
-fuzztime 30s ./internal/agent/...Standard library first. External dependencies are added only when
they implement a well-specified format impractical to reimplement
(YAML, JSON Schema) or replace more than 500 lines of in-tree code
(cobra for CLI, tiktoken-go for BPE). No ORMs, no logging frameworks
beyond log/slog, no HTTP clients beyond net/http, no cgo.
Violations are caught by depguard in CI.
v1.1 is a strictly-additive release that closes the credibility gaps called out by the post-v1.0 external review. The v1.0 contract — manifest shape, exit codes, determinism, CLI flags — is preserved unchanged.
aperture eval— score plans against committed ground-truth fixtures, compare against a ripgrep baseline, and gate CI on regression. Seedocs/eval.md.aperture eval loadmode— empirical calibration of thebehavioral_summaryvsfulldemotion rule, with a §7.5.2 threshold advisor. Seedocs/loadmode.md.- Mention dampener —
s_mentionis clamped when no other signal agrees, defusing gameable-mention false positives.selection_logic_versionbumped tosel-v2. --scope <path>— project a plan onto a monorepo subtree. Fingerprint still covers the full tree; supplementals stay admissible across the scope boundary. Seedocs/scope.md.- Tier-2 languages — TypeScript, JavaScript, and Python
get module-level symbol/import extraction via tree-sitter.
See
docs/tier2.md. aperture diff— section-by-section comparison of two manifests. Read-only — never touches the planner or repository. Seedocs/diff.md.-tags notier2— pure-Go fallback build that skips tier-2 analysis, forCGO_ENABLED=0environments.- Prebuilt binaries for linux/{amd64,arm64}, darwin/{amd64,arm64}, and windows/amd64 via goreleaser.
See CHANGELOG.md for the full list and
specs/v1.1/SPEC.md for the normative
delta contract.
Aperture is harness-neutral — anything that can shell out to the CLI
can use it. examples/claude-code-skill/ ships one concrete
integration: a Claude Code skill that teaches Claude when and how to
invoke aperture before starting a coding task. See
examples/claude-code-skill/README.md.
If your project uses a CLAUDE.md or AGENTS.md to brief coding
agents, add the block below to tell the agent to gate itself through
aperture before touching code. The language is harness-neutral — it
works with Claude Code, Codex, or anything else that reads these
files. Replace <agent> with the name of the adapter declared in
your .aperture.yaml (typically claude or codex).
## Pre-coding: run aperture
Before starting any non-trivial code change in this repo, run aperture
to plan context and check feasibility:
aperture plan TASK.md --format markdown
If a `TASK.md` doesn't exist, pass the task inline:
aperture plan -p "<one-line task description>" --format markdown
Interpret the output:
- `feasibility.score < 0.70` or any `severity: blocking` gap → stop and
fix the task description or repo state. Do not lower the threshold to
get past the gate.
- `load_mode: full` selections are the files to read first.
- `reachable` files are discoverable follow-ups, not pre-loaded context.
For an integrated run that plans, persists the manifest, and invokes
the agent in one step:
aperture run <agent> TASK.md --fail-on-gaps --min-feasibility 0.70v1.1 feature-complete. All seven v1.1 implementation phases ship with passing unit + integration + property + fuzz + golden
- determinism tests under
make testandgolangci-lint run ./.... v1's SPEC lives atspecs/initial/SPEC.md; v1.1's additive contract atspecs/v1.1/SPEC.md.
Deferred post-v1.1 work:
aperture inspectsubcommand for interactive manifest diffing.- Formal trust-gate (explicit approval flow + persisted trusted-
agents file) for
.aperture.yaml-declared custom adapter commands. - Secret-pattern redaction of manifest contents.
- Per-symbol (rather than file-level) budgeting.
- Side-effect tagging for tier-2 languages.
- Optional LLM summarization (opt-in, clearly flagged, pinned model).
See LICENSE.
{ "schema_version": "1.0", "manifest_id": "apt_<16 hex>", "manifest_hash": "sha256:<64 hex>", "generated_at": "2026-04-18T…Z", "incomplete": false, "task": { "task_id": "tsk_<16 hex>", "source": "TASK.md", "raw_text": "…", "type": "feature", "objective": "first non-empty line", "anchors": ["Provider", "RefreshToken", "oauth", …], "expects_tests": true, "expects_config": false, "expects_docs": false, "expects_migration": false, "expects_api_contract": false }, "repo": { "root": "/abs/path/to/repo", "fingerprint": "sha256:<64 hex>", "language_hints": ["go", "markdown", "yaml"] }, "budget": { "model": "claude-sonnet-4-6", "token_ceiling": 120000, "reserved": { "instructions": 6000, … }, "effective_context_budget": 72000, "estimated_selected_tokens": 8200, "estimator": "heuristic-3.5", // or tiktoken:cl100k_base "estimator_version": "v1" }, "selections": [ { "path": "internal/oauth/provider.go", "kind": "file", "load_mode": "full", "relevance_score": 0.91, "score_breakdown": [ {"factor": "mention", "signal": 1.0, "weight": 0.25, "contribution": 0.25}, … ], "estimated_tokens": 405, "rationale": ["direct task mention", "package match", …], "side_effects": ["io:network", "io:time"] } ], "reachable": [ … ], "exclusions": [ {"path": "vendor/**", "reason": "default_pattern"}, … ], "gaps": [ { "id": "gap-1", "type": "missing_tests", "severity": "warning", "description": "…", "evidence": ["…"], "suggested_remediation": ["…"] } ], "feasibility": { "score": 0.82, "assessment": "moderate feasibility", "positives": ["anchor_resolution=0.86", …], "negatives": ["gap_penalty=0.05"], "blocking_conditions": [], "sub_signals": { "coverage": 0.75, "anchor_resolution": 0.86, "task_specificity": 1.00, "budget_headroom": 0.94, "gap_penalty": 0.05 } }, "generation_metadata": { "aperture_version": "1.0.0", "selection_logic_version": "sel-v1", "config_digest": "sha256:…", "side_effect_tables_version": "side-effect-tables-v1", "host": "runner-01", "pid": 1234, "wall_clock_started_at": "2026-04-18T…Z" } }