Literate AI

Repository documentation

0.7.0 release evidence closure — one hierarchical ledger over every stage and host dialog

Why this document exists

The attempted 0.5.2 release failed and left no usable record of why. Reconstructing it afterwards found only unrelated leftovers: pip traceback bytecode and litai-driver-failure-*.log files written ~1:51pm for RebuildSourceCacheDerivationManifest.lifecycle_plan_identity. The actual ~3:12pm host-E2E samples crash was unrecoverable, and the sample scratch that held its generated source, build output, and runtime transcripts was already deleted.

That is not bad luck; it is the current design. Every layer of the release stack deliberately discards or anonymizes its evidence:

Layer What survives a failure today
make release → litai release check a typed error plus _release_gate_failure_detail: one collapsed 2000-byte tail of stdout or stderr
passing legacy-local gate stdout_digest / stderr_digest only — digests of bytes nobody kept
make release-check → run_checkpointed_gates.py gate names recorded "ok" in RELEASE_CHECKPOINT; no output, no pins, no pointers
each Make gate recipe the child's exit status; its stdout/stderr go to the terminal and die with the scrollback
make samples (sample_runner.main with no --runtime-root) nothing: the whole run executes inside tempfile.TemporaryDirectory(prefix="literate-ai-conformance-"), removed on the way out including on failure
litai rebuild driver failure complete driver output written by _retain_driver_diagnostic to an anonymous tempfile.mkstemp(prefix="litai-driver-failure-") in the OS temp root — unindexed, unnamed, unreferenced, and reaped by the OS
installed-e2e, install-check, installed-project-e2e, wheel-check, roundtrip-host nothing: shutil.rmtree(workspace, ignore_errors=True) / TemporaryDirectory on every path
Standard lifecycle failures failure_diagnostics[...] — an in-memory dict that dies with the process
--target local SSH workers worker_id, endpoint, duration_ms, and two digests; remote transcripts stay on the worker
--target github run_id / run_url / conclusion — a URL, not evidence

scripts/fanout_samples.py is the one component that already behaves correctly: it writes a per-worker log, always retains failed workspaces, and has --retain-workspaces for successful ones. It is the prior art this plan generalizes.

The consequence is the one Jordan named: an LLM (or a human) triaging a failed release has to troll a large filesystem for correlated timestamps, and usually loses. The fix is not more logging. It is a closure: one hierarchical, pointer-based record where release is the outermost node and every sub-workflow, stage, gate, sample, variant, and host dialog beneath it records its own pins and the paths to its own retained output.

The model

One ledger per outermost operation

A single evidence run is created by the outermost litai operation (normally release check, but equally rebuild, build, or samples when invoked directly) and inherited by everything it spawns:

_build/evidence/
  20260825T211200Z-9f3ac1/          # <utc>-<short digest of the root operation identity>
    ledger.ndjson                   # append-only, flushed per record: the authority
    index.json                      # derived closure, rewritten on completion *and* on failure
    n0001/ … n0042/                 # one short directory per node (Windows MAX_PATH safe)
      stdout.log stderr.log         # retained transcripts, redacted
      pins.json outputs.json        # this node's exact identities and pointers
    latest -> ../20260825T211200Z-9f3ac1   # pointer file on Windows, symlink elsewhere

ledger.ndjson is append-only and flushed per record precisely because the 0.5.2 run died mid-gate: any format that is only valid once written completely is a format that is absent exactly when it matters. index.json is derived and rebuildable (litai release evidence index).

Node schema

New module src/literate_ai/evidence_ledger.py, new schemas literate-ai/evidence-ledger-node@1 and literate-ai/evidence-ledger@1 registered in schemas/v2 and schema_catalog (with compatibility.json and the migration characterization baseline updated in the same change):

{
  "schema": "literate-ai/evidence-ledger-node@1",
  "node_id": "n0031",
  "path": "release/gate/release-check/samples/hello-component/python/host-e2e",
  "parent_id": "n0030",
  "operation": "sample.host-e2e",
  "state": "failed",
  "host": {"kind": "local", "platform": "linux-x86_64"},
  "started_at": "2026-08-25T21:12:03Z",
  "ended_at": "2026-08-25T21:12:47Z",
  "duration_ms": 44118,
  "pins": {
    "plan_identity": "urn:…", "policy_identity": "urn:…",
    "prepared_revision": "sha256:…", "lifecycle_driver_identity": "sha256:…",
    "component_lock_identities": ["urn:…"], "source_cache_key": "urn:…",
    "flavor_selectors": ["flavor://literate-ai/lang-python"],
    "coding_cli": "claude", "model": "…", "wheel_digest": "sha256:…"
  },
  "outputs": [
    {"role": "stdout", "path": "n0031/stdout.log", "bytes": 91233,
     "digest": "sha256:…", "media_type": "text/plain", "retention": "retained"},
    {"role": "scratch", "path": "/tmp/litai-samples-9f3ac1/hello-component/python",
     "bytes": 20481223, "digest": null, "media_type": "inode/directory",
     "retention": "retained"},
    {"role": "diagnostic", "path": "n0031/driver-failure.log", "…": "…"}
  ],
  "error": {"code": "sample.host_e2e_failed", "message": "…", "excerpt": "…"},
  "children": ["n0032"]
}

States are pending, running, passed, failed, skipped (checkpoint-resumed or sentinel-current), and unavailable (the node could not be observed — a crashed child, a lost worker). unavailable is load-bearing: a run that dies must still say where it died. Pointer retention is retained, pruned, host-only (still on a worker), or imported (transferred into coordinator custody).

Attachment without a central registry

Two environment variables carry the ledger across every process boundary: LITAI_EVIDENCE_RUN (the run root) and LITAI_EVIDENCE_PARENT (the parent node id). Any child that knows about the ledger opens the same run and appends beneath its declared parent; recursive make gates, run_samples.py, the lifecycle driver, and worker launchers all inherit it for free.

Crucially, coverage does not depend on the child cooperating. A single wrapper — evidence_ledger.record_subprocess(...), built on the existing diagnostics.trace_subprocess redaction — creates the node, tees the child's stdout/stderr to both the terminal and n####/std*.log, and records argv, exit status, and duration. An uninstrumented step is therefore still represented by a node with a complete transcript from its parent's point of view. Deeper instrumentation adds pins and children; it is never a prerequisite for having evidence at all.

Triage surface

One command is the LLM's entry point:

$ litai release evidence explain            # defaults to the latest run
release … failed
  gate release-check (target: local, worker w-linux-01) … failed
    samples … failed
      hello-component/python/host-e2e … failed  sample.host_e2e_failed
        pins: lifecycle_driver sha256:2022e513…  lock urn:…  cli claude  model …
        stderr  _build/evidence/…/n0031/stderr.log (91 KiB)
        scratch /tmp/litai-samples-9f3ac1/hello-component/python (20 MiB, retained)
        driver  _build/evidence/…/n0031/driver-failure.log (412 KiB)

explain prints the root-to-failure path only, bounded to roughly one page: passing siblings collapse to a count. Companion subcommands are index (rebuild index.json from the NDJSON), show <node> (one node's complete record), and prune (retention-budget reclamation). litai release check prints the evidence root when it starts through the existing Python progress/stderr mechanism and renders explain on failure before returning the existing CLI failure; no Makefile shell logic is needed. Exit codes and existing stdout contracts are unchanged.

Retention: the 0.5.2 fix

Retention is the part that actually recovers the crash Jordan lost, and it is a change in policy, not just in recording:

Host and target dialogs

Host dialogs are first-class nodes, not a footnote, because in the 0.5.2 shape the failure was a host dialog:

Blind steps: shell and Make surfaces become instrumented Python

A step that runs outside a Python harness cannot record pins or retain output, so the remaining shell/Make surfaces are where evidence coverage stops. Scoped for 0.7.0 (tracked as RELEASE-EVIDENCE-002):

Correlation with existing records

The ledger indexes existing evidence; it does not replace or re-implement any of it, and it never becomes attestation.

Stages

Each stage is independently landable and leaves the tree green.

  1. Foundation. evidence_ledger.py, both schemas, the environment handoff, the record_subprocess wrapper, and litai release evidence explain|index|show|prune. No existing gate changes behavior.
  2. Release spine. check_release emits the root node and the target/gate/worker nodes for legacy-local, local, and github; transcripts are retained on both paths; prepared-release@2; run_checkpointed_gates.py node mapping; Python surfaces the evidence root and renders the failure explanation on stderr rather than changing Makefile shell logic.
  3. Host dialogs and retention. Sample runner scratch retention with per-sample / per-variant / per-phase nodes; _retain_driver_diagnostic relocation; failure_diagnostics flush; installed-e2e / install-check / installed-project-e2e / wheel-check / roundtrip-host retention; fan-out pointer wiring.
  4. Blind-step harness. RELEASE-EVIDENCE-002's Make/shell replacement and the CI evidence artifact.
  5. Acceptance. The 0.5.2 regression: inject a host-E2E sample failure under make release and assert that a single explain invocation names the failing sample, variant, and phase, its pins, and retained scratch plus complete transcript paths that still exist after the run exits — the precise thing that did not exist on the 0.5.2 branch.

Implementation evidence

Acceptance contract

Out of scope for 0.7.0

Any new remote transport; GitLab release dispatch (still fail-closed per RELEASE-005); an external log-aggregation or tracing service; replacing perf.py; making the ledger load-bearing authority for build authorization or acceptance. The ledger is a triage index over evidence that other records already own.

Risks

Read the Markdown sourceConference homePublished 1.3.0 · Patch 1.3.1 in progress · Development 1.4