# Research Run Ledger

> Execute approved research commands without a shell and preserve tamper-evident command events, exit status, separate stdout/stderr artifacts, exact input/output snapshots, and verified append-only output checkpoint chains. Use when a research workflow, experiment, data conversion, growing JSONL ledger, model invocation, validation, or multi-Skill handoff must be reproducible and auditable at command level.

- Skill: `lx050/research-run-ledger` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add lx050/research-run-ledger`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lx050/research-run-ledger/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: Lx050 (https://skillmd.com/u/lx050)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lx050/research-run-ledger

---


# Research Run Ledger

Record a command as a `run.started`/`run.finished` pair in a JSONL hash chain.
Capture stdout and stderr into exclusive artifacts that are never overwritten.
Version 1.1.3 writes new schema-1.1 events and retains read compatibility with
complete 1.1.2, 1.1.1, 1.1.0, and 1.0.0 run pairs.

## Safety boundary

Treat this Skill as a recorder, not a permission system. Before invoking it:

- confirm that the user authorized the command and its external effects;
- apply the project's download, license, and untrusted-input safety protocol;
- avoid commands that print credentials because stdout/stderr are preserved;
- declare every material input and expected output that matters to the result.

The runner refuses obvious token, password, authorization, or cookie values in
argv and explicit environment assignments. It starts child processes with a
minimal environment allowlist and never writes environment values to the
manifest. These checks reduce accidental disclosure; they do not replace
credential management or command review.

## Record one run

Use an explicit argv vector after `--`. Never reconstruct it as a quoted shell
string.

```bash
python3 scripts/run_ledger.py run \
  --manifest /absolute/run/manifest.jsonl \
  --artifact-dir /absolute/run/logs \
  --manifest-parent-identity 16777232:123456 \
  --artifact-directory-identity 16777232:123457 \
  --run-id RUN-OCR-001 \
  --cwd /absolute/project \
  --input /absolute/data/source.png \
  --output /absolute/results/ocr.json \
  -- python3 /absolute/tools/ocr.py \
  --input /absolute/data/source.png \
  --output /absolute/results/ocr.json
```

For a preflight-to-run handoff, create the empty artifact directory with mode
`0700`, record its POSIX device/inode and the device/inode of
`manifest.parent`, then pass both guards shown above. A guarded directory must
be owned by the current user and must not be group- or other-writable; the
artifact directory must be exactly `0700`. A mismatch stops before the lock,
manifest, or logs are created. Version 1.1.3 requires the two guards
both-or-neither. Unguarded calls remain supported, but do not establish
cross-process continuity with an earlier preflight.

The runner opens the manifest parent and artifact directory as pinned directory
descriptors, rechecks their pathname identities, and creates the lock,
manifest, and logs relative to those descriptors with no-follow/exclusive
semantics. New 1.1.3 `run.started` events record the expected and observed
identities in `publication_boundaries`.
If the platform lacks directory-relative open/stat/mkdir/unlink,
`O_DIRECTORY`, or `O_NOFOLLOW`, the run stops before publication. Cleanup is
also identity-checked immediately before unlink; an already observed
same-name replacement for the lock, empty manifest, or reserved logs is left
untouched. POSIX name-based unlink cannot eliminate a same-user replacement
inside the final stat-to-unlink instruction window, so use this Skill only
with pinned, non-payload-directed child programs in a user-private workspace.

Supply repeatable `--input` and `--output` flags. Inputs must exist and be
regular files. `--output` means that the run's final bytes are a fixed
snapshot: later `verify --check-files` requires exact hash and size equality.
Outputs may be new or existing regular files, but an input and output may not
be the same path. The Skill records an existing output's pre-run snapshot; it
does not grant permission to overwrite it.
At finish it also rechecks every input's local device/inode. Replacing an
input with an identical-byte inode, or making an output alias either the
start or finish input identity, records `integrity_failed`. A 1.1.3 start
stores each input's `identity_before`; its finish stores the bound before and
after identities plus `identity_status`. The verifier derives replacement and
recorded input/output identity-alias violations rather than trusting only a
summary string.

For a JSONL ledger or another file that is intentionally extended by later
recorded runs, declare it with repeatable `--append-output` instead:

```bash
python3 scripts/run_ledger.py run \
  --manifest /absolute/run/manifest.jsonl \
  --artifact-dir /absolute/run/logs \
  --run-id RUN-PROVENANCE-002 \
  --cwd /absolute/project \
  --append-output /absolute/run/provenance.jsonl \
  -- python3 /absolute/tools/register.py \
  --ledger /absolute/run/provenance.jsonl
```

An append-only output may be created, remain unchanged, or grow. If it already
exists, its old byte prefix and runtime file identity must survive the command.
The next run must start from the preceding recorded checkpoint; unrecorded
growth blocks the run before `run.started`. Never use `--append-output` merely
because an ordinary output might change later.

Use `--env KEY=VALUE` only for non-sensitive, reproducibility-relevant
settings. Prefer files or approved credential helpers for authentication.
Environment values are passed to the process but only key names are logged.

For an ordinary run, the wrapper returns the command's exit code. A nonzero
command still produces a complete finish event and log artifacts. A recorder
or launch error returns `2`. If a declared input changes or a declared output
is missing after execution, the finish event is `integrity_failed` and the
wrapper returns `3`, even when the child process returned zero. Do not
reinterpret that result as a successful research run.

Before `run.started` and before launching the child, the writer constructs a
pessimistic finish-event size envelope from the bound paths and fixed
collection budgets. It then reserves two event slots and enough total
manifest bytes for the exact start plus a maximum-size finish. If the
envelope, pair slots, or pair bytes do not fit, it returns `2` without running
the command. Post-run records retain the start-bound paths, so a symlink or
ancestor-path replacement cannot expand or redirect the finish record.

## Verify a run ledger

```bash
python3 scripts/run_ledger.py verify \
  --manifest /absolute/run/manifest.jsonl
```

This rejects an empty or structurally incomplete ledger with exit `2` and
checks the JSONL hash chain, event ordering, start/finish pairing, required
payloads, and the
current stdout/stderr artifact existence and hashes. Add `--check-files` when
current input and output paths should still satisfy their contracts. Snapshot
outputs and inputs require exact equality. Append-only outputs are grouped by
path: every recorded checkpoint must be a byte prefix of the next, and the
current file must exactly equal the latest checkpoint. This rejects truncation,
old-region edits, prepends, skipped intermediate states, and unrecorded tails.

Schema 1.0 outputs retain exact snapshot semantics. For a known legacy path
that was in fact appended by multiple recorded runs, use an explicit
verifier-only compatibility policy:

```bash
python3 scripts/run_ledger.py verify \
  --manifest /absolute/run/manifest.jsonl \
  --check-files \
  --legacy-append-output /absolute/run/provenance.jsonl
```

The report labels this `legacy-verifier-override` with
`legacy-byte-prefix-only` assurance. It does not claim the old run declared
append intent and cannot exempt changed inputs, outputs missing at their
recorded finish, discontinuous checkpoints, or unrecorded current growth.

Stop on any integrity error or mismatch. At the run's start and finish
observation points, inputs, snapshot outputs, append-only outputs, the
manifest, and current log artifacts must not be the same file, including
resolved-path and hardlink aliases. Later `verify` checks recorded bytes, not
continued inode identity: an archival relink with identical bytes may still
pass and must not be described as proof that roles remain physically separate.
Do not append to a tampered manifest, reuse a run ID, reuse log artifact names,
remove a lock whose writer may still be active, or replace an artifact to make
verification pass. Create a new run after resolving the cause.

The loader is bounded line-streaming: it never joins the complete manifest in
memory. It applies fixed 16 MiB total-manifest, 1 MiB per-event, 4,096-event,
nesting, node, structural-token, integer-token and float-token budgets before
accepting events. It requires strict UTF-8, exact event/payload keys, and
rejects duplicate keys, booleans in integer fields, and non-finite numbers.
A budget, syntax, non-UTC timestamp, or chronology failure is a
malformed-ledger stop, never a successful or merely mismatched run. Timestamps
must use `Z`; event `recorded_at` values are nondecreasing and each pair obeys
`started_at <= start recorded_at <= ended_at <= finish recorded_at`.

Before resolving paths or creating artifacts, `run` also enforces fixed count
and aggregate UTF-8 budgets: argv 512/256 KiB, inputs 256/128 KiB, snapshot
outputs 256/128 KiB, append outputs 256/128 KiB, and explicit environment
items 128/128 KiB.

Read
[references/run-manifest-contract.md](references/run-manifest-contract.md)
when integrating the event schema or interpreting verifier exit codes and
proof boundaries.

## Handoff

Return the manifest path, `run_id`, start and finish event hashes, process exit
code, stdout/stderr artifact paths and hashes, declared output hashes,
append-transition result and policy source when applicable, and verification
result. Label missing outputs, changed inputs, prefix violations, replacements,
launch failures, and nonzero exits explicitly; never reinterpret them as
successful research results.

