Lightcone CLI rebuild with uv first, container option, datalad compatibility - #190
Merged
Conversation
Lands the sandbox layer and the one verb that exercises it.
Per spec §4, `lc run CMD` is the **probe** verb: byte-for-byte the
recipe environment — same lock, same `.venv` — under the same boundary
recipes will get. A probe has no output, so it needs neither manifests
(layer 2) nor the fabric (layer 4). That is what let layer 5 land ahead
of them, and it makes it the smallest honest consumer of the boundary
that layer 4 will plug recipes into.
No new dependencies.
## The abstraction
**The seam is a pure argv rewrite.** Every mechanism is
`Backend.wrap(policy, argv) -> argv'` — a function turning a command
into *a different command that sandboxes itself*.
```
lc run CMD
│
engine/run.py ── mode check · rename guard · uv hop
│
sandbox.run(backend, policy, argv, prefix=uv…)
│
backend = detect() ← the ONLY sys.platform branch
argv = backend.wrap(…) ← pure
Popen(…) ← mechanism-blind
│
┌───────────────────┼───────────────────┐
LandlockBackend SeatbeltBackend Unavailable
[python, -m [sandbox-exec, argv unchanged
lightcone. -p <sbpl>,
_sandbox_exec, -DREAD_0=…, --,
--policy <json>, *argv]
--, *argv]
```
Seatbelt is natively that shape; **Landlock is not** — it is a
self-restriction, which is precisely what `lightcone/_sandbox_exec.py`
exists to fix. Once both reduce to argv, nothing above the seam branches
on platform, and **every backend is testable on a host that cannot run
it**: `tests/test_sandbox_wrap.py` checks the Landlock wrap *and* the
generated macOS SBPL on Linux CI, with no privileges and nothing
spawned.
Three types stay deliberately distinct — `Policy` (what we will
enforce), `Capability` (what this host can do), `Attestation` (what was
actually enforced, spec §7's manifest field). Collapsing any two is how
a sandbox starts lying.
Prior art surveyed: OpenAI's codex CLI and this design's own earlier
implementation on `redesign_prototype`. They agree on the shape and
disagree on one mechanism; this takes codex's answer.
## Deviations from the spec, all recorded in CLAUDE.md
- **The Landlock policy travels as JSON on argv, not as an inherited
ruleset FD.** §7 specifies building the ruleset before fork and passing
it with `pass_fds`. The shim builds and applies it itself instead —
codex's shape. This makes `wrap()` pure and closes §11's own *blocking*
spike ("does the Landlock FD survive `uv run`'s spawn/exec chain? an FD
cannot be reopened") by making the question moot. It also survives into
a container later, where a host FD cannot.
- **Network is not controlled on either platform**, by decision. The
generated SBPL explicitly allows it and both platforms attest `network:
allowed` — rather than §7's matrix value of `denied`. Nothing pretends
to a control it does not apply.
- **`--require-sandbox` is a bare flag.** §7's `=declared-fs` form would
be the same flag today, since every existing mechanism scopes the
filesystem. It arrives with the first one that doesn't.
- **The denial's system-layer remedy is trimmed** to `uv add` + the
ASTRA input declaration. §7's `[tool.lightcone.image]` TOML arrives with
the container hatch; printing it now would be a copy-pasteable fix that
does nothing.
- **No manifest is written** — a probe has no output.
`Attestation.to_manifest()` exists and is tested; layer 4 is its first
consumer.
- **One tightening §7 does not contemplate:** `/tmp` leaves the write
scope when the project lives under it. Granting it unconditionally would
make such a project's tree writable and silently void the read-only-tree
guarantee. `TMPDIR` points into the private scope regardless, so
`tempfile` works either way.
## Three grants that turned out to be load-bearing
Each found by a real failure while building this, each now pinned by a
test:
| Grant | Without it |
|---|---|
| the realpath'd **ELF loader** in the exec set | *every* dynamically
linked binary fails EACCES — bash and python included |
| **read** on the uv-managed interpreter's install root | `Failed to
import encodings module` — the stdlib sits beside the binary, outside
the project and outside `/usr` |
| **read** on `/dev/urandom` | `failed to get random numbers` — CPython
seeds hash randomization before `main` |
## Verification
`170 passed`, ruff clean, mypy strict clean. The suite splits along the
seam:
- **Pure, every OS** — policy construction, both backends' argv, the
golden SBPL, the denial renderer.
- **Real subprocess, no kernel needed** — the shim's contract: its argv,
and the reserved exit code `97` that means "lc's own setup broke" and
never "the command ran unsandboxed".
- **Real enforcement, no fakes** — `test_sandbox_landlock.py`,
module-level `skipif` on `abi() == 0`. A mocked sandbox proves nothing,
so where Landlock is absent the file skips whole. Policies there are
hand-built so each test grants exactly what it is testing.
- **Pre-exec decisions** — discovery, the containerized refusal, the
rename guard, declared inputs, the uv hop.
Exercised by hand end to end against a real scaffolded project (kernel
7.1, Landlock ABI 9): runs in the project `.venv`; project reads
allowed; in-tree writes denied *with the file verifiably unchanged*;
declared `astra.yaml` inputs become readable; undeclared tools and files
denied with their respective remedies; `$HOME` redirected to a private
scope; no in-tree `__pycache__`; `--sandbox-debug`, `--no-sandbox`,
`--require-sandbox`, and the rename guard all behaving.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01DqTza2GqRLZ4yY5ywAusQH
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Standing in $HOME, `lc run` answered "/home/francois is missing pyproject.toml, uv.lock, .venv — not a project root. Run `lc init` to converge one." — a list of files nobody expected to be there, and advice to scaffold a project in your home directory. The two ways the check fails are different mistakes. A directory with no project markers is the wrong *place*: it now says so and says to cd to the root of one, with no mention of `lc init`. A directory that holds a pyproject.toml or an astra.yaml but lacks the built environment is the right place, unconverged — a fresh clone is exactly this, since git carries no .venv — and there `lc init` really is the answer, so the missing pieces are still named. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DqTza2GqRLZ4yY5ywAusQH
…#174) Two small changes to `lc run`, on top of layer 5. ## The project tree is writable now The boundary is meant to be a no-install stand-in for running the project in a container. What it should catch is a command reaching **outside** the project for a tool, library, or data file the image would not have. Forbidding writes *inside* the project served none of that — a container bind-mounts the working tree read-write — so the project joins the write set. This cannot be narrowed to "everything but `.venv`": Landlock unions rights over ancestors and has no way to subtract, so a writable project is necessarily a writable `.venv`. What keeps the environment honest is `uv sync --exact` at the next convergence, not the sandbox. Two things existed *only* to serve the read-only tree and go with it: - the filter that dropped `/tmp` from the write scope when the project lived under it; - `PYTHONPYCACHEPREFIX`, which redirected bytecode into a `$HOME` deleted after every run, so every in-tree module recompiled every time. A writable tree caches in place — what a container does, and what the scaffolded `.gitignore` already expects. The denial for a readable-but-not-writable path now means *a write outside the project* (a system path, or a declared input) and says so. Verified by hand against a real project: writing `results/out.csv` works; reading an undeclared file under `$HOME`, running `git`, and writing `/etc/...` are all still denied with their remedies. ### One test note worth reading The enforcement suite's `outside` fixture moves from `tmp_path` to `$HOME`. `/tmp` is in the write baseline, so anything `tmp_path` hands us is **granted** — those denial tests had been passing only because a project under `/tmp` used to drop `/tmp` from the policy. Left alone they would have gone green while testing nothing. ## `lc run` outside a project `/home/francois is missing pyproject.toml, uv.lock, .venv — not a project root. Run \`lc init\` to converge one.` listed files nobody expected to be there and advised scaffolding a project in your home directory. The two failures are different mistakes and now get different advice. No project markers at all is the wrong *place*: it says so and says to `cd` to the root of one, with no mention of `lc init`. A `pyproject.toml` or `astra.yaml` present but no built environment is the right place, unconverged — a fresh clone is exactly this, since git carries no `.venv` — and there `lc init` really is the answer. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DqTza2GqRLZ4yY5ywAusQH --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`lc materialize` executes an ASTRA analysis: it plans the graph, runs each recipe under the layer-5 boundary, and commits every output together with the manifest that describes it. Layer 2 folds in, because the fabric is what needs identity and nothing else did. Layer table: rows **2** and **4** → ✅. ## Results are versioned in the repository On the DataLad model — **git carries the pointers and the history, git-annex carries the bytes** — without taking the `datalad` package. Every operation the layer needs is one git or git-annex command, and `uv.lock` is `env_version`'s first term, so 43 transitive packages would stale every output in every project on each boto3 bump. Compatibility is a commitment rather than a coincidence: `lc init` writes a `datalad.dataset.id`, so a project is a DataLad dataset **from birth** (verified: `datalad status` needs no `--force` adoption step), and the run record is the format `datalad rerun` reads. lc never imports datalad, never requires it, and never parses `.datalad/`. git stops being optional — a project without version control had nowhere to put a result. ## Identity ``` env_version = sha256(uv.lock ‖ .python-version ‖ canonical install settings) code_version = sha256(recipe ‖ canonical decisions ‖ env_version) ``` Both length-framed, so a boundary shift between fields cannot produce one digest from two inputs. The git commit is **recorded with every output and hashed into neither** — a commit must not stale the world; per-output code invalidation is available by declaring source files as ASTRA inputs. The lock scan **refuses** path/directory/editable dependencies (the lock records where they were, not what was in them, so two syncs can install different code while every hash agrees), **reports** registry packages built from sdist, and is **advisory** on non-default dependency groups. ## Execution | Module | Role | |---|---| | `plan.py` | spec × universes → tasks; sub-analyses flatten to `<analysis>.<output>`, a second nesting level is refused rather than ignored | | `assets.py` | the output directory, the manifest, and `staleness()` — one predicate, two callers | | `worker.py` | one output: gates, reset, recipe, hash, manifest. Never raises, never writes git. Also the `python -m` entry point the run record names | | `materialize.py` | the driver: dirty-tree refusal, Dask, and the serialized save/restore loop | | `dataset.py` | the one git + git-annex seam, routed through the existing `project._run` | | `identity.py` | the two hashes and the lock scan | - **Dask owns the ordering.** Tasks are submitted with their upstream futures as arguments; there is no ready-set loop and no hand-rolled topological sort in the execution path. - **The driver owns git alone.** Concurrent git operations on one repository race on the index lock — this is `datalad-slurm`'s schedule/finish split. - **A run leaves the tree exactly as clean as it found it.** `ok` → save, `failed`/`blocked`/never-reported → restore, in a `try/finally`. That is what makes the dirty-tree refusal survivable rather than a trap. - **`--check` differs from a worker by one input value**, not by logic: `None` for anything already classified as would-run, meaning "this is going to change". `sandbox/policy.py` gains `recipe_policy()` beside `probe_policy()`, sharing one baseline: a recipe may write its own output directory and nothing else under `results/`. Reads are unchanged — a carve-out there is something Landlock cannot express at all. ## Three things running it turned up that the design did not have - **`.gitattributes` needs a leading `* annex.largefiles=nothing`.** `git annex add` annexes whatever it is handed, so without it the documented save turns `src/fit.py` into a read-only symlink into the object store and the next edit fails with `EACCES`. - **`git annex add -A` is not a thing.** `git add` needs `-A`; git-annex rejects it. A test extracts the line from `data/README.md` and runs it, because that spelling was wrong once. - **`git check-ignore` must be asked with a trailing slash and `--no-index`**, or the `results/*` an older scaffold wrote — which ignores the directory's *contents*, not its name — goes unseen, and a materialize reports success while committing nothing. ## Dependencies `git-annex` (0 transitive; wheel-only, so it sets the CLI's install floor — see the risk note in the plan) and `distributed`. **`datalad` is a dev-group dependency only**: the run record's format fails *silently* (its parser returns nothing on a mismatch and `rerun` then exits 0), so the suite asserts through datalad's own parser **and** runs a real `datalad rerun`, rather than golden-testing our own JSON. ## Verification ``` 348 passed ruff: clean mypy: clean ``` End-to-end from a clean `uv pip install .` into a throwaway venv (not the global tool env): `lc init` → commit → `--check` → `materialize` → idempotent re-run → decision change cascades to dependents → old bytes recovered from an old commit. Landlock enforced and recorded in `hermeticity`, results annexed, manifests in plain git, `datalad status` clean, one `[DATALAD RUNCMD]` commit per output. Also pinned by tests, against a real annex and the real boundary: a recipe cannot write into a sibling output (mutation-checked against `Unavailable()`), a recipe *can* read an annexed input through its symlink, a failing recipe commits nothing and leaves `git status` empty, and `python -m lightcone.engine.worker` imports neither click nor rich. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01GaahDTd6djz9PCm1Rh6SP4 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The note said the residue was unguarded but not that it is unguardable with the tools uv provides — so the next reader's first instinct is the one that was already measured and rejected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GaahDTd6djz9PCm1Rh6SP4
`lc init` pinned `lightcone-cli` into every project's own lock, which is what forced **layer 3** — a launcher that discovers, mode-detects, scrubs `UV_*`, converges, and delegates to the lc inside the project's venv. Remove the pin and there is nothing to delegate to: the `lc` that was invoked is the engine that runs. Layer 3 is now marked removed by decision rather than pending. This reverses spec §2's engine-in-lock rule. The spec is a reference the rebuild drifts from deliberately; the reversal lands as a Recorded decision in `CLAUDE.md` rather than waiting on a spec rewrite. ## What changes A scaffolded `pyproject.toml` declares **no dependencies**. A project's resolution is no longer constrained by the engine's own pins (no astra-tools/click/rich/distributed/git-annex in any project lock), and `requires-python` now comes from the interpreter that wrote `.python-version` instead of from lightcone-cli's metadata — one source, so the bound and the pin cannot disagree. The git-annex wheel floor gates installing the tool and nothing else. ## What the pin bought, and where each guarantee went - **The engine inside `env_version`** — given up deliberately. An lc upgrade now moves nothing; no output ever reads `behind` over an engine release. This completes the reversal that already took `env_version` out of `definition_version`. The engine becomes attestation (`lc_version` in every manifest), not identity. - **Reruns reconstruct the exact engine** — re-provided in the record itself. `cmd` pins the engine by version through an ephemeral `uv run --no-project --with lightcone-cli==<v>`, keeping the **module spelling** so the worker stays out of `lc --help` and off `$PATH`. A dev build's version is unpublished, so its record is the bare module — which only exists in a checkout whose own interpreter imports it. - **Driver/worker skew "structurally impossible"** — moot while workers are in-process threads; noted as a layer-7 connect-probe obligation. - **Layer 6** — the generated image can no longer get its in-image engine from the lock; noted that the Containerfile must install it as an explicit layer and that the version becomes a tag input. ## The worker now owns the project environment It has to. A rerun checks out the lock but never the `.venv`, and **`uv run --no-sync` against a missing `.venv` silently creates an empty one** (measured, uv 0.12.5) — recipes would have run in it while the manifest recorded the lock's `env_version`. `worker.main()` therefore syncs before anything executes, which also inherits the loud stale-lock refusal the outer `uv run --locked` used to provide. `current_project(synced=False)` is the affordance: the lock is repository content, the `.venv` is not. A new test reruns on a **fresh clone**, because the in-place rerun passes with or without the sync. `_engine_path()` is deleted — the suite faked the pin by injecting `src/` onto `PYTHONPATH`, and there is no pin left to fake. `test_the_run_record_is_what_datalad_reads` tightens its `.endswith` to a full-string match so a prefix appearing or vanishing cannot slip past. ## Verification - `uv run pytest` — 413 passed; `ruff` and `mypy --strict` clean. - End-to-end by hand: scaffolded a project (no `lightcone-cli` in `uv.lock`), materialized, `lc status`, then `datalad rerun` on a fresh clone with no `.venv` — the worker created it and the output came back byte-identical. - The pinned shape run literally, from a wheel built off this branch: an ephemeral engine env reproduced the output with a matching `data_version`, and git-annex is present beside that env's interpreter so `put_our_bin_first()` still resolves. Incidentally fixes a latent bug in `eval.yml`: the eval project's pin resolved a *released* engine from PyPI rather than the branch under test. No workflow change needed. ## Follow-up, not in this PR The `UV_*` ambient scrub (spec §4 step 3) is worth having and is independent of the pin — it protects `env_version`'s install-settings term, not the delegation that is gone. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01PGfhkvU3Ee2hhHcwAkeidg --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## What this is
The container hatch, landed whole: declaration, identity, `lc build`,
containerized `lc run` and `lc materialize`, manifest v2, and the `lc
status` headers. The design decisions were taken interactively and are
recorded in CLAUDE.md; the load-bearing ones:
- **Engine on host.** The container is the *recipe's* execution world,
never the engine's — driver, git, annex, dask and classification all
stay the invoked `lc`. This deviates from spec v6.1's
full-stack-in-image rule, recorded: v6.1's reason (the host-sync
deadlock) is solved by converging `.lightcone/venv` *inside* the image,
and the spec's own Perlmutter row ("recipe wrap, step 3 only") is this
exact shape. No delegation machinery, no git through a
`--userns=keep-id` bind mount, no engine version in the image tag.
- **The user never sees a Containerfile.** The whole surface is
Modal-shaped TOML — `base` / `apt-install` / `run-commands` / `env`, a
closed, fully-hashed key set (`pip_install` deliberately has no
equivalent; the lock owns Python). The render exists only in a transient
build context; `LABEL io.lightcone.image` keeps the image
self-describing.
- **The dataset is the image store; runtime stores are caches.** `lc
build` saves the image as a docker-archive at
`.datalad/environments/<tag>/image` (the `datalad containers-add`
layout), annexed and committed — exact bytes to every clone via `git
annex get`, no registry. Execution pins the id read from the archive
itself. A dropped archive never substitutes; a rebuild is a new commit
under a new id.
- **The mount table is the mechanism.** One `OCIBackend` (podman
recommended, docker accepted — data-parameterized spellings, one shape)
maps the same `Policy` to `:ro`/`:rw` mounts with `--network none`; the
attestation (`fs: declared`, `network: denied`) is derived
flag-for-flag. No in-container Landlock, no seccomp probe: the engine
container never gets the tree `:rw`, so mounts alone express the whole
policy. The archive format is chosen so apptainer/singularity become
thin layer-7 backends.
- **The record stays runtime-neutral through the worker.** Verified
against datalad-container's source: templates expand at *record* time
and `rerun` executes the literal string, so a bare-recipe `cmd` was
rejected. The containerized record adds `extra_inputs: [<archive>]`,
which stock `datalad rerun` fetches through the annex before executing.
## Notable traps found on the way
- git-annex routes dotfiles to git regardless of `annex.largefiles` —
without a per-add `annex.dotfiles=true` the archive lands as a full git
blob, silently. Pinned both ways in `test_dataset.py`.
- A standalone `chmod -R /opt` layer copy-on-writes the whole
interpreter into every archive, doubling it; the chmod rides inside the
layers that write `/opt`.
- `boundary.env_argv`'s host-resolved `env` binary need not exist in the
image (NixOS), so world-backends apply the overlay natively via `--env`
(`contains_prefix`, declared on every backend).
- The dirty check now runs *before* the converge in materialize: the
image commit would otherwise sweep the user's staged edits.
## Testing
- `tests/test_image.py`, `test_sandbox_oci.py` — pure (declaration,
document, render ordering, tag sensitivity, mounts, attestation, seam
composition).
- `tests/test_container.py` — the lifecycle against a stubbed
`project._run` (three `ensure_image` strictnesses, refusals
mutation-checked, build/save/commit argv).
- `tests/test_container_smoke.py` — real podman end to end, gated
(`skipif` + `LC_CONTAINER_TESTS_REQUIRED=1` on Linux CI + two guard
tests): build → id/label verification → probe (refusal → success → mount
denial → loopback/egress) → full materialize with manifest assertions →
**`datalad rerun` on a bytes-free clone fetching the archive through the
annex and reproducing the output**.
- 483 tests pass; ruff and mypy clean; `lc --help` still 65 ms.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_012oVCfc4eZdyKqT1zY95Xi2
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The first HPC venue, NERSC-focused, in two commits. ## The SLURM venue (7a) `lc materialize` inside a SLURM allocation now spans every node it was granted: `cluster_for_run()` detects `SLURM_JOB_ID`, binds the Dask scheduler in the driver process to `SLURMD_NODENAME`, and launches one worker per node with a single `srun` on the driver's own interpreter — so driver and workers are the identical installation off the shared filesystem, which is all a worker needs (`lightcone.engine` importable at the driver's version; no git, no annex). Detected, never configured: the allocation *is* the resource declaration, the same shape as mode derivation. No new dependency. ``` salloc -N 4 -C cpu -q regular -t 2:00:00 lc materialize # runs across all 4 nodes # or, fire-and-forget from a login node: sbatch -N 4 -C cpu -q regular -t 2:00:00 --wrap 'lc materialize' ``` A NERSC login node refuses `lc materialize` first thing, naming both commands above; `--check`, `status` and `lc run` work anywhere. A dead srun reports its own exit code; teardown retires workers so srun ends silently. A dask-jobqueue submission venue, if ever wanted, is one more branch in the same ladder plus the config table it genuinely needs. The suite fakes the *host*, never the code: SLURM env vars plus a bash stub for srun drive the real graph through the real detection → bind → launch → teardown path in CI, with real worker processes. A new `LocalCluster(processes=True)` test pins the cross-process pickling contract the recorded decision asked layer 7 to re-verify. ## podman-hpc (7b) A containerized project now runs on NERSC: podman-hpc lands as a spelling inside the existing `OCIBackend` — no new shape — plus one step, `podman-hpc migrate <id>`, placed outside the load branch (a fresh build never loads, yet compute nodes only see migrated images) and run driver-side once for all nodes. Detection prefers the wrapper over bare podman (whose node-local store is exactly what compute nodes cannot see); it is build-capable, making a login node the source of matching-arch archives. A foreign-arch archive refuses before the load, naming both arches and the fix. apptainer/singularity stay deferred; CLAUDE.md records the design headroom they need (Runtime stays facts; honest `network: allowed` for a runtime that cannot deny; the exit-125 note is a podman/docker-family fact). ## Verification - 519 tests pass locally (`uv run pytest`), ruff and mypy clean. - The container smoke suite gains podman-hpc as a parametrization that fires only where the site tool exists — running it on a Perlmutter login node is the spike vehicle; the checklist (srun `--overlap`, `--network none` on compute, `--module` mounts vs `fs: declared`, migrate-by-id, Landlock in the SLES LSM list, Lustre annex timings) is recorded in CLAUDE.md. Deviation from the approved plan, recorded: the plan called for two PRs; repo convention is one PR per layer (#175, #180), so both parts land here as separate commits. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_012oVCfc4eZdyKqT1zY95Xi2 --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…a status fact (#182) ## Summary The last layer of the rebuild table, rescoped during planning from the spec's "`lc verify`, WRROC export" to two mechanisms with **no new verbs**: **The project is the crate.** `ro-crate-metadata.json` lives at the project root as a derived artifact, converged by `lc materialize` the way `uv.lock` is: rendered from repository state, string-compared, committed alone only on a difference. Deposit is `git archive`/`datalad export-archive` on a repository that is already an RO-Crate — nothing copied, no bundle, no `lc export`. Maintenance is derived, never configured: a declared `[project].license` turns it on (RO-Crate requires one; materialize must not refuse science over a missing key, and inventing CC-BY-4.0 — the pre-rebuild default — asserts terms over someone's data). Absence is one report line. **Integrity is a status fact, not a verb.** The hole layer 4 recorded — a hand-edited-and-committed output reads `current` forever because a skip returns the recorded digest (the agent-forged-file scenario) — is closed by history, not hashing: every output is committed, so a hand edit *requires* a commit, and `dataset.last_writer` names it. `lc status` flags any output whose directory's last commit is not its own run record, with sha, author, date and a `git show` remedy. O(history), answers on a bytes-free clone, still exits 0. ## The crate itself - `engine/crate.py`, the one new module — a **pure** builder over `plan.build()` and the manifests; git enters only as an injected `writer` callable. - Targets **Provenance Run Crate 0.5 structurally, not vacuously** (the pre-rebuild exporter passed REQUIRED only because the SHACL shapes are vacuous over an empty target class): outputs sharing a manifest `git_sha` are one run — the driver's single HEAD read makes run identity free — giving `OrganizeAction` + workflow-level `CreateAction` per run, `ControlAction` per execution, `HowToStep` per output id. - The workflow-run `@context` makes `containerImage`/`sha256`/`ContainerImage` real vocabulary (the old exporter emitted them as undefined terms JSON-LD silently drops). The committed image archive is one entity: `File` + `ContainerImage`, config-blob id and payload together. - The manifest stays canonical: in the crate as a `File`, `subjectOf` its `Dataset`; `env_version`/`definition_version`/`hermeticity` are deliberately not transliterated. - Deterministic by construction: sorted iteration, `sort_keys`, no clock (`datePublished` = newest manifest `finished_at`, overriding rocrate's stamp-now default), no `mimetypes` (it reads `/etc/mime.types`, which would make the render host-dependent). - The crate's `Person` is the author of each output's **saving** commit via `last_writer` — never the manifest's `git_sha`, which is the commit the run *started* at. ## Validation Official `rocrate-validator` against the Provenance profile, on a real materialized project: **REQUIRED clean**; RECOMMENDED failures pinned as a *set* (`test_crate_smoke.py::_FLOOR`) of checks the crate cannot truthfully satisfy (the workflow's id is its in-crate path and the shape wants `^http`; an annex-stored image has no registry; lc knows no publisher/affiliation). A new failure is a regression; a disappearing one is the floor to shrink. Gated with `LC_CRATE_TESTS_REQUIRED=1` on every CI runner — the validator is a dev dep, so none may skip. ## Also in here - The worker stamps `started_at`/`finished_at` (millisecond ISO 8601 — crate consumers parse `endTime` with at most three fractional digits). Attestation like `lc_version`; `classify` never reads them; `SCHEMA_VERSION` stays 1. - `materialize.run_subject` is now the one spelling of the run record's subject, shared by `run_record` (composer) and the foreign-write check (comparator) — byte-identical to the previous inline string, and the datalad parser + real `datalad rerun` tests all pass. - Recorded rejections, with reasons in CLAUDE.md: no `lc verify` (O(bytes), blind on unfetched outputs, no lifecycle moment enforcing it; `git annex fsck` owns object corruption), no `lc export`, and the honest residues stated (a forged run-record subject defeats the history check; a manifest lc itself mis-recorded is the one case only a rehash would catch; the rerun entry point does not regenerate the crate). - One trap the suite found live: forging a result **in place** dirties its byte-identical sibling through the shared thin annex object — the recorded thin hazard demonstrating itself. Tests unlink first; CLAUDE.md records it. ## Test plan - 556 passed (31 new), ruff and mypy clean, `LC_CRATE_TESTS_REQUIRED=1` locally. - New: `test_crate.py` (pure, 14 tests, no git — determinism, run grouping, the Provenance edges, ContainerImage, Person, license spellings), `test_crate_smoke.py` (real materialize → real validator, two guard tests), `last_writer` against real repos incl. the enclosing-repository case, the foreign-write fact end-to-end incl. a bytes-free clone, crate convergence (created once / idempotent re-run commits nothing / license added later / license removed keeps the file / spec-dropped output excluded and named). - Manual e2e: `lc init` → license → materialize (crate lands in its own trailing commit) → re-materialize (no commit) → validator (0 REQUIRED) → forge + commit → `lc status` names the foreign commit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Q9ikHqeMA9tTX1Jrx5DdNL --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The eval workflow and its prompt were written for the pre-rebuild CLI —
the Snakemake shim, `lc run <output_id> --universe`,
`ok`/`stale`/`missing` states, the Containerfile + `requirements.txt`
container model. With layer 8 landed, this brings the eval back in line
with what `lc` actually is.
## `evals/prompt.md` (rewritten)
- `lc materialize` is the build verb; `lc run <command>` is the probe.
States are `current`/`behind`/`stale`, `lc status` always exits 0, and
`lc materialize --check` is the gate.
- Recipes import from the project's own locked environment — packages
arrive via `uv add` in the project root (what the denial messages
recommend); `uv pip` reaches nothing a recipe sees.
- A run refuses a dirty tree, so the agent is told to commit its edits
with plain `git add`/`git commit` (the annex filter is transparent —
nobody runs a git-annex command).
- `results/` is the engine's to write; a hand-placed file is now
detected as a foreign write and remade — the old "fails verification"
warning got teeth.
- The seed spec is stated to be **deliberately incomplete**: the recipe
commands carry no placeholders and the dependency contracts are partial,
so completing the spec (`{output}`, `{inputs.<id>}`, `{decisions.<id>}`,
and the matching `inputs:`/`decisions:` lists) is part of the task. This
makes the existing seed's shape an intentional test of spec literacy
rather than a leftover.
- The recipe template grammar section is kept — verified it still
matches astra's own grammar (`astra guide`, astra-tools 0.2.16).
## `.github/workflows/eval.yml`
- **Check step**: the old jq expression (`.universes[].outputs[]`,
`"ok"`, `"alias"`) matches nothing any verb emits now. Replaced with the
designed gate — `lc materialize --check --json | jq -e '.up_to_date and
((.current | length) > 0)'` — which exits nonzero while anything is
planned or failed, plus the non-emptiness guard the old check had.
`behind` deliberately does not fail the gate.
- **Setup step**: no scientific deps are pre-seeded — the project starts
empty and declaring what the recipes import (`uv add`, spelled out in
the prompt) is part of what the eval measures; the repo-venv `uv pip
install` is dropped (it never reached recipes). Setup commits the seed —
`lc init` never commits, and the first `lc materialize` would otherwise
be refused on a dirty tree before the agent did anything.
- **Trigger**: the dormant PR plumbing (comment step, concurrency group)
gets its trigger back — `pull_request: types: [labeled]` gated on a
`run-eval` label, alongside `workflow_dispatch`.
- **Artifacts**: `.snakemake/**` exclusion → `.lightcone/**`; keeping
`.git` via `include-hidden-files` is now a feature — the run records and
annexed results are the provenance.
- **Install**: lc and astra are installed as uv tools — `uv tool
install` of the checkout (the commit under test, stamped into the
version by hatch-vcs) links `lc` and the four git-annex executables
through the mirrored entry points, exercising the exact mechanism a user
install relies on; astra-tools is its own tool install, pinned to the
branch dependency. No venv is activated, so the runner needs no system
git-annex and `VIRTUAL_ENV` is never set.
## Seed + trace prompt
- `evals/tasks/snae/astra.yaml` drops `container: Containerfile` — the
directive is ignored by recorded decision, and no Containerfile exists
to point at.
- `trace-analysis-prompt.md` names the sandbox instead of the container
environment.
## Verified locally
- The seed validates against astra-tools 0.2.16 after the edit.
- The full setup + check plumbing runs end-to-end against today's `lc`:
`lc init` → overlay → `astra universe generate -n baseline` → `uv add` →
seed commit → `lc status` (three outputs, direct mode, landlock) → the
`--check` gate correctly failing with all three outputs planned on the
unbuilt project.
The agent step itself (Claude Code + astra plugin) is unchanged and
needs a real dispatch run to shake out — suggest a `workflow_dispatch`
on this branch once merged, or label this PR `run-eval`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01DJzmp2MUhwiNHR94cB91dx
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
First pass of the post-rebuild documentation rewrite — the entry points:
`README.md`, `docs/index.md`,
`docs/user/{index,install,getting-started}.md`. Style deliberately
follows the previous versions (tabs, admonitions, numbered steps, the
"what just happened" recap); the content is brought to the rebuilt CLI.
## What changed
- **README**: quick start shows the real loop (`init` → spec + scripts →
`uv add` → commit → `materialize`); the Capabilities section replaces
`lc verify` / `lc export wrroc` / Snakemake with what exists —
multiverse materialization, provenance by construction (manifests + run
records, annex-carried bytes), locked sandboxed execution, derived
containerized mode + SLURM, and the license-derived RO-Crate view.
- **docs/index.md**: light touch — drops "skills" from the alpha notice,
updates the lightcone-cli card's description.
- **user/index.md**: quick start updated (commit before materialize,
with a sentence on why the commit isn't ceremony); covered-pages list
wording refreshed.
- **user/install.md**: rewritten. Prerequisites are uv + git; git-annex
ships as a wheel with the install (with the platform floor stated
honestly, and WSL for Windows); a new "tell git who you are" step
(materialize checks identity up front); container runtime section
reflects detection (podman-hpc → podman → docker) and the absence of any
config file — `~/.lightcone/config.yaml` no longer exists. Upgrade
section notes an engine upgrade never invalidates results.
- **user/getting-started.md**: new worked example — a line fit over a
synthetic dataset with one methodological decision (keep vs clip
outliers), swept across `baseline` and `robust` universes, ending with
the license declaration and the auto-committed `ro-crate-metadata.json`.
It walks through the scaffold, declared inputs and the annex, the
dependency contract and placeholders, `uv add`, the clean-tree rule,
real `lc materialize` / `lc status` output, incremental rebuilds, and
the fresh-clone story.
## Verification
- Every command block in the walkthrough was executed against the
current `lc` before landing; the quoted console output is captured from
that run (slope numbers included).
- The fresh-clone claim was tested: a bare clone refuses, `lc init` +
`lc materialize` reports up to date with no annex content fetched — the
guide says exactly that.
- `zensical build` passes; the two remaining link warnings are in frozen
pages (`api/snakefile.md`, `hpc/targets.md`) that later passes replace.
Next passes: (2) core concepts + cluster/troubleshooting/glossary, (3)
CLI + JSON reference, (4) developer corner, (5) nav + workflow refresh.
Two cosmetic engine warts observed while verifying, deliberately not
touched here: Rich markup swallows the literal `[project]` in the
license hint (renders as "no .license in pyproject.toml"), and a stray
Dask teardown `TimeoutError` line can appear on stderr after a run.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01DJzmp2MUhwiNHR94cB91dx
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…186) Second pass of the documentation rewrite — the rest of the user guide: a new **Core Concepts** page plus rewrites of **Running on a Cluster**, **Troubleshooting**, and the **Glossary**. Style follows the frozen predecessors. ## What's in it - **`docs/user/concepts.md`** (new, added to nav + user index): the mental model in one page — a project is spec + lock; an output's three recorded facts and why they age differently; the `current`/`behind`/`stale` table with the contradiction-vs-circumstance line; the commit discipline (clean tree in, clean tree out, git log as build log, foreign-write detection); direct vs containerized mode; `lc status` vs `lc materialize --check`; the license ⇒ crate rule. - **`docs/user/cluster.md`**: the venue model replaces the old Snakemake/Dask-Gateway/site-registry content. The allocation *is* the resource declaration (no `--jobs`, no venue config); login-node prep vs in-allocation materialize with interactive/batch tabs; the login guard's refusal quoted verbatim; containers on HPC (podman-hpc detection + migrate, build-on-login-node with the arch gate, the multi-node shared-store refusal); an honest "early days" admonition inviting site reports. - **`docs/user/troubleshooting.md`**: organized around the CLI's actual messages, each captured live against a real project before being quoted — dirty tree (and its commit/discard path split, verified against the source), not-a-project, the sandbox trailer with the three denial remedies (`uv add`, declare the input, write to `{output}`/mkdtemp), behind-after-`uv add` as a non-error, stale-after-spec-edit (including the deliberate "code edits don't cascade" position), unfetched annex content, the NERSC login guard, git identity, and the three containerized refusals. - **`docs/user/glossary.md`**: ASTRA-side entries kept (ASTRA, spec, recipe, decision, universe, sub-analysis, insights/findings); execution-layer half replaced — materialize, manifest, `definition_version` / `env_version` / `data_version` / `input_versions`, the three states, the two modes, image + runtime, sandbox, git-annex, run record, RO-Crate. Snakemake, `code_version`, `ok`/`missing`/`alias`, and the `lc verify` failure kinds are gone with the architecture that defined them. ## Verification - Every quoted refusal/denial was produced by actually triggering it: dirty tree, not-a-project, `NERSC_HOST` login guard, a sandboxed read denial, `ModuleNotFoundError` under the sandbox, and the `behind`-everywhere state after a real `uv add` (plus the `--check` verdict line with its `--refresh` hint). - The dirty-refusal "discard these (lc writes results/)" branch was verified against the source. - `zensical build` passes; the only warnings remain the two frozen pages (`api/snakefile.md`, `hpc/targets.md`) that pass 4 replaces. Next: (3) CLI + JSON reference, (4) developer corner, (5) nav + workflow refresh. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DJzmp2MUhwiNHR94cB91dx Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The rebuild is functionally complete through layer 8; this PR closes the recorded residues that do not need Perlmutter access, one commit per decision. Each choice below was settled explicitly before implementation. ## What lands - **Ambient `UV_*` scrub** (closes #179): `project.child_env` drops every `UV_*` variable outside a closed plumbing allowlist (cache dir, timeouts, TLS, air-gap mode, index credentials, uv's recursion guard), so an exported `UV_NO_BINARY` or `UV_PYTHON` can no longer steer a sync while `env_version` reports nothing moved. The run verbs warn with the names of any non-empty variable dropped, from the same predicate. - **Machine-level uv.toml advisory** (#176, option 2): the scan checks uv's two documented user/system config paths for audited install-settings keys — key presence, not file presence, because list settings concatenate across levels — and reports a hit beside `sdist_built`. Never hashed. - **Network uncontrolled on every mechanism**: the OCI wrap drops `--network none`, its one restriction, so all three mechanisms attest `network: allowed` symmetrically and no consumer can read a promise into "containerized". `denied` stays in the type for a mechanism that genuinely emits a denial flag. Retires the Perlmutter spike item about `--network none` hanging on compute nodes. - **Recipe write scope narrowed to its own output directory** (reverses the recorded "not narrowed" decision): closes the cross-write residue — a concurrent task landing bytes in a sibling's directory before the sibling hashed produced a manifest that was self-consistent and wrong, undetectable by any checksum, so prevention is the only fix. A probe keeps `results/` whole (no output id) — the one probe/recipe asymmetry, and it excludes exactly the accident being prevented. The new enforcement test is mutation-checked through `Unavailable()`. - **`uv_version` manifest attestation**: the engine-closure decision's concrete loss, recorded — probed once per run by the driver and handed down (the HEAD discipline), beside `lc_version`. Attestation, never identity. - **Per-file sha256 in the crate, from annex keys**: every output file is a `File` under its dataset's `hasPart` with `sha256`/`contentSize` parsed from its SHA256E key — verifiable by `sha256sum` after a `git archive` deposit, available bytes-free because keys are repository state (`--include=*` is load-bearing). Non-SHA-256 keys yield size and no digest; git-carried files hash their bytes. Also fixes a latent honesty bug: out-of-tree inputs published lc's *framed* digest under the workflow-run `sha256` term; they now publish none. Validator floor unchanged at five. - **`crate:` line in `lc status`**: the rerun-lag residue made visible by comparing the document's own `datePublished` against the newest manifest `finished_at` the walk already read — no git, no rocrate import on status's path. - **End-of-run edit warning**: an edit made while the graph ran (the `git_dirty` window) is now named in the report; still no manifest field, by decision. - **`[project.scripts]` absence pinned**: the recorded review item — a metadata test asserts the worker and the shim never become console scripts. ## Re-examined and deliberately left (recorded in CLAUDE.md) - The forged-run-record-subject residue: the threat model excludes adversaries, and a copied subject is already deliberate. - The validator RECOMMENDED floor stays pinned at five — no publication-metadata config surface. ## Housekeeping Closed stale pre-rebuild issues #166, #128, #153 with pointers; commented triage on #152 and the advisory landing on #176. ## Verification Full suite: 582 passed (sandbox enforcement real on Linux; crate smoke with the official rocrate-validator, REQUIRED clean and the floor unchanged). ruff and mypy clean. Cross-write denial mutation-checked through `Unavailable()` (rc 0, bytes replaced, unsandboxed). Closes #179. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Third pass — `docs/cli/` rebuilt around the five verbs that exist, in the frozen pages' structure (Synopsis / semantics / options table / JSON / examples). ## What's in it - **Overview** (`cli/index.md`): the three global behaviors (current directory is the project — no walk-up, no global config; nothing waits on a human; refusals carry their remedy) and the exit-code contract, including `lc run`'s proxying and the `--check` gate semantics. - **`lc init`**: the four convergence classes (created / repaired / **blocked** / warned — with blocked counting against convergence and warnings never affecting exit), the real scaffold tree, what it deliberately doesn't create (`src/`, dependencies), and the deliberately bare flag surface. - **`lc materialize`** (new page): target grammar (`fit` vs `robust/fit`, matching-nothing is an error), the remake rules (stale vs behind vs current under `--refresh`), the run's contract (clean in/clean out, fetches declared inputs, commits as it goes, reports every independent failure, converges the crate), check mode's dirty-tree exemption, the absent-flags rationale (`--jobs`, `--force`), and the JSON report with `ok`/`up_to_date` called out as the branching keys. - **`lc status`**: the header facts, the commit column as the verb's reason to exist, the three states, the report-vs-gate split, and the JSON shape including `foreign_write`. - **`lc run`**: the probe — same environment and sandbox as a recipe, environment converge first, exit-code proxying (`128+N`), denial notes, works on any uv project, no sandbox opt-out. - **`lc build`**: the system-layer-only image, the closed declaration key set, the committed `docker-archive` as the store (id-pinned, no registry), the arch refusal, the clean-tree requirement, and the JSON result (`action: built | present`). - `verify.md` and `export.md` deleted with their verbs; nav updated (materialize added, order follows the workflow). ## Verification - All `--help` texts, the init/materialize/status JSON shapes, and the console blocks were captured from the current CLI against a real project. - The build JSON keys and `action` values were read from `commands.py`/`container.py`, and the tag format corrected against `image.py` (`lc-env-<16 hex>` — my first draft had the pre-rebuild `lc-<project>-<hash>` spelling). - `zensical build`: no new warnings; the two remaining are in frozen pages PR 4 replaces. Next: (4) developer corner, (5) nav + workflow refresh + final reconciliation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DJzmp2MUhwiNHR94cB91dx Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Fourth pass — the maintainer half of the site: `maintainer.md`, `architecture.md`, an **Engine Internals** section replacing the old Python API pages, and the contributing trio. ## What's in it - **`architecture.md`**: the CLI/engine/ASTRA split; the run pipeline as one annotated diagram (guards → converge → plan → fetch → venue → workers → driver loop → crate); identity (two hashes, three states, the one-classify-two-callers discipline); storage (DataLad model, run records, thin commits); the exec boundary (policy → argv rewrite → attestation); the container hatch; venues; the crate. This is CLAUDE.md's invariants translated for humans who didn't live the rebuild. - **`maintainer.md`**: the welcome page in its old shape — what the corner covers, the three-command dev loop, and the house rules (no dead code/foreshadowing, no escape hatches, literal behavior, one implementation per rule, honest reporting). - **Engine Internals** (`docs/api/`, 12 pages): an overview map plus one hand-written tour per module — project, dataset, identity, plan, assets, worker, materialize, venue, sandbox, image & container, crate. Each page: responsibility, key-symbols table, "what must stay true", and where its tests live. Hand-written rather than autodoc, deliberately: the engine is not a public API (projects don't depend on lightcone-cli), so contract beats signatures. The 17 old pages documented modules that no longer exist; all deleted. - **Contributing**: `setup.md` (uv-only loop, the gated-suite table, docs/wheel builds, pre-PR checklist), `testing.md` (the `tools`/`real_tools` seam, a where-does-this-question-belong table, the enforcement suite's four properties, the mutation-check rule and its two traps), and a new `extending.md` (the change-map table plus the everywhere-rules and conventions). - **Deleted**: `docs/hpc/` (site-registry model), `contributing/backends.md`, `contributing/hpc-sites.md`. Nav updated (Python API → Engine Internals; Extending added). ## Verification - `zensical build` now reports **zero issues** — the two warnings that survived passes 1–3 lived in pages this one deletes. - Module pages were anchored on the modules' own docstrings and CLAUDE.md's invariants; symbol names spot-checked against the source (e.g. `runtime_for_run`'s `(Runtime, "built"|"present")` return, `datalad_run_subject`, `_SITES`). - No remaining references to any deleted page anywhere in `docs/`. Next and last: (5) final polish — `check-docs.yml` refresh and CLAUDE.md reconciliation (the docs-frozen rules, the design-doc pointer). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DJzmp2MUhwiNHR94cB91dx Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Final pass of the documentation rewrite — no doc pages this time, just bringing the two things that *govern* the docs in line with their new reality. ## CLAUDE.md - **The freeze rule is replaced** by its successor: docs are live (PRs #185–#188), two tracks, and a change lands with its docs — a verb change updates its `docs/cli/` page, an engine change its `docs/api/` module page, user-visible behavior the user guide. The docs inherit the codebase's own discipline: document only what exists, quote refusals from real runs, verify command blocks by executing them. - **The design-doc pointer** now records the decision taken during this rewrite: the design records stay in the sibling checkout and are **dropped when the rebuild completes** — never imported into `docs/` (reverses the paragraph's earlier plan). - **The workflow inventory** catches up: eval runs on dispatch or the `run-eval` label (re-trigger by re-adding it), `check-docs.yml` reviews merged PRs, `docs-deploy.yml` tracks releases; the docs build command is stated. - The extending table's "add the next layer" row loses "Docs are deliberately deferred". Deliberately **not** done: slimming CLAUDE.md's invariant sections now that the developer corner overlaps them. CLAUDE.md is still the working memory for the remaining rebuild (Perlmutter spike, deferred layers), and gutting it mid-rebuild trades a known-good reference for dedup. Worth revisiting when the rebuild closes and the design docs drop. ## check-docs.yml The post-merge doc-drift check was written before the freeze and knew nothing about the structure. Its prompt now carries: - a **map from change kind to doc home** (verb → `docs/cli/`, module → `docs/api/` + architecture, user-visible → user guide + README, workflow/conventions → contributing); - the two failure modes that matter most for these docs: **quoted console output drifting** from what the CLI prints, and **documenting anything the code no longer delivers** (the no-foreshadowing rule applied to docs); - an updated skip list (CLAUDE.md and `evals/` are maintained separately) replacing the stale `skills/` rule. ## Verification - `zensical build`: zero issues. - check-docs.yml validates as YAML. - Grep for `frozen`/stale workflow descriptions in CLAUDE.md comes back clean. This closes the 5-PR docs rewrite. The site deploys with the next release (or a manual `docs-deploy` dispatch). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DJzmp2MUhwiNHR94cB91dx Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ywhere PR #184 dropped the OCI wrap's --network none so all three mechanisms attest network: allowed symmetrically; the container internals page still said recipes exec network-denied. Found by the drift check the new check-docs prompt describes — a quoted flag the code no longer delivers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJzmp2MUhwiNHR94cB91dx
Contributor
✅ Eval
lc statusConfusion & pain points (Claude analysis)Confusion & pain points
Full trace: |
…tion Brings PR #127 (mike-based docs versioning) across the rebuild: - zensical.toml keeps [project.extra.version] provider = "mike" - pyproject docs group keeps the squidfunk mike fork dependency - docs-deploy.yml is the mike-based workflow (deploy <version> + latest to gh-pages on release; manual redeploy via workflow_dispatch) - justfile stays deleted (removed intentionally in the rebuild); its mike recipes live in the workflow, CLAUDE.md now points at the raw mike commands for local use Verified: uv sync --group docs installs mike 2.2.0+zensical-0.1.0 and zensical build succeeds with the version provider enabled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014PC18whir9otmXpQbaEgir
Follow-up to the PR #190 eval trace, which surfaced three harness-attributable frictions (the other findings were agent-side noise). ## The `lc run` convention question, settled Checked the ecosystem before touching anything: `uv run "python -V"` fails with ``Failed to spawn: `python -V` `` (verified live), `docker run img "echo hi"` fails with `exec: "echo hi": executable file not found` — argv with no shell parsing is the convention across `docker run`, `uv run`, `kubectl exec`, `pipx run`, `poetry run`, `srun`. **`lc run` already matches it**, so the behavior stands unchanged and the syntax gets one line in the two places it was missing: - `evals/prompt.md`: "Argv style, like `docker run` or `uv run`: `lc run python scripts/fit.py --output /tmp/x`, never a single quoted shell string; for shell syntax use `lc run bash -c '...'`." - `docs/cli/run.md`: same sentence, in the synopsis. In the trace, the missing line cost one round trip through a genuinely cryptic failure (`/usr/bin/env: '<whole string>': No such file or directory` + the sandbox trailer insinuating a denial). ## Harness environment - **`UV_PYTHON` noise**: `setup-uv`'s `python-version` input exports `UV_PYTHON`, and the #184 install-settings scrub then (correctly) warned `ignored ambient UV_PYTHON — …` on *every* `lc run`/`lc materialize` — a dozen-plus unactionable lines the agent ended up `grep -v`'ing to read its own output. The input is dropped; determinism is kept by pinning `--python 3.12` on both tool installs. Same category as the `VIRTUAL_ENV` cleanup earlier. - **Truthful dev versions**: the shallow checkout had no tags, so hatch-vcs stamped `0.1.dev1+g<sha>` into the installed engine — and into every manifest's `lc_version`. `fetch-depth: 0` fixes it. Validation: label this PR `run-eval` to see the updated prompt and clean environment in action. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DJzmp2MUhwiNHR94cB91dx --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This merges the clean rebuild of lightcone-cli onto main — the full re-architecture from the Snakemake shim to the model where the locked environment is the execution environment. The pre-rebuild codebase was deliberately stripped and functionality re-added one layer at a time, each layer landing with its own tests, dependencies, and (since the rewrite) docs. 21 PRs, all individually reviewed and merged into
clean_rebuild; this PR is the promotion.What lightcone-cli is now
A project is
pyproject.toml+uv.lock+astra.yaml— uv is the only environment substrate, and mode is derived, never configured: direct by default (recipes in the project's.venvunder Landlock/Seatbelt), containerized when[tool.lightcone.image]is declared (recipes in a content-addressed image archived in the repository itself). Five verbs:init,materialize,status,run,build.definition_version(what an output is) andenv_version(what it ran under) are separate on purpose — a spec contradiction isstaleand remade; an environment move isbehind, reported and left alone.--refreshwidens by exactly that one state.git add/git commit. Every output is committed with a manifest and a run recorddatalad reruncan replay; runs start and end on a clean tree; foreign writes are detected by history and classify stale.Policy→ pure argv rewrite → honest attestation) shared by recipes and thelc runprobe; a recipe writes its own output directory and nothing else; denials carry copy-pasteable remedies. Network is uncontrolled on every mechanism, symmetrically attested.salloc/sbatchspellings; podman-hpc makes multi-node containerized runs real.[project].licenseturns on the RO-Crate view —ro-crate-metadata.jsonconverged by materialize, validated against Provenance Run Crate 0.5 in CI. Deposit isgit archive.The layers, as merged
lc run(landed first — the smallest honest consumer of the boundary)lc materializeuv tool installof the branch under test, empty-environment task, RO-Crate gate — three green runsVerification
clean_rebuild, including the three gated suites CI requires: sandbox enforcement (Landlock on Linux, Seatbelt on macOS), container smoke (real build → archive → bytes-free-clonedatalad rerun), and crate smoke (the official validator, REQUIRED clean).uv add, materializes through the real engine, and publishes a crate — $0.70–$1.22 per run.zensical buildrenders the rewritten docs with zero issues; the site deploys on the next release.Known deferred work (tracked, not blocking)
🤖 Generated with Claude Code
https://claude.ai/code/session_01DJzmp2MUhwiNHR94cB91dx