A dissolved verb run outside any project writes a runs/ store into cwd #218
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Surfaced by the milestone "Verb dissolution" (#210) close fieldtest (2026-07-07).
Auto-registration (generated campaign + process documents) is not gated on project presence, so a one-off
aura generalize --real …(or any dissolved verb) run from a non-project directory silently creates a content-addressedruns/store in that directory. The fieldtest's Scenario 1 created aruns/store with 34 families + 5 campaigns + 4 processes from a handful of commands.The cycle-0110 fieldtest already flagged "refusals leave no store litter", but a successful no-project run still does.
Resolution (one of): gate store creation on a project (
Aura.tomlpresent), or document that verbs record to./runswhen run outside a project.Triage 2026-07-09 (full open-backlog review, tree at
68317ec) — confirmed as a live defect, and an inconsistency rather than a doc gap:Env::runs_root()falls back to the relativePathBuf::from("runs")when no project is discovered (crates/aura-cli/src/project.rs:130-138; the doc comment names this as today's behaviour).run_campaignrefuses without a project ("campaign run needs a project…", crates/aura-cli/src/campaign_run.rs:322-333, pinned at crates/aura-cli/tests/research_docs.rs:958).run_*_sugarfunctions callenv.registry()+register_generated*(store writes) with no provenance check (crates/aura-cli/src/verb_sugar.rs:295/337/453/581, writes at :163-166/:236-239), then reach the campaign runner throughrun_campaign_by_id, which has no gate either (campaign_run.rs:379/396).So verbs that are pure sugar over the one campaign path skip the very project gate that path enforces. Fix shape: the same provenance gate ahead of
env.registry()in the four sugar entry points, plus a test pinning that a dissolved verb run outside a project refuses with the existing "needs a project" message and leaves noruns/store behind. Label adjusted tobugaccordingly.Design + approach (specify entry, /boss)
The triage located the fix as a project gate in the four
run_*_sugarentrypoints. Implementing that surfaced a load-bearing refinement and one large
collateral consequence; both are resolved here before spec production.
Refinement — gate placement. The first store touch on the dissolved-verb
path is not in
run_*_sugarbut earlier, invalidate_and_register_axes(
put_blueprint, main.rs:2529). That function validates the--axisnamesfirst (main.rs:2515-2522, returning
UnknownAxis→ exit 2) and only then writesthe blueprint.
Registry::openis lazy (registry lib.rs:59-61 — it stores apath, no I/O until a write) and
blueprint_axis_probeis store-free, so amalformed invocation already fails clean with no store litter. The gate
therefore belongs INSIDE
validate_and_register_axes, after axis validation andbefore the first store write — a single chokepoint shared by all four verbs —
not at the dispatcher top.
Fork — usage error vs. project gate ordering. For a malformed dissolved
verb run outside a project (e.g.
mc --real --axis bogus.name), two refusalscompete: exit 2 (unknown axis, a usage error) and exit 1 (no project). Decision:
the usage error wins. Basis: derived — a malformed command is malformed in any
environment, so a usage error is project-independent and already produces no
litter; placing the gate at the dispatcher top instead preempts the
axis-validation refusal, which (a) gives a worse diagnostic ("no project" for a
command that is itself broken) and (b) flips the exit-2 usage-refusal tests
(e.g.
mc_dissolved_refuses_an_unknown_axis_name,walkforward_campaign_refuses_a_raw_form_axis_name) that already pin "a refusedaxis name must not start a real run". Gate-at-first-store-touch preserves the
ordering; gate-at-dispatcher-top does not.
Error channel. The gate refuses via a new
AxisRegisterError::NoProjectvariant, which
exit_axis_register_errormaps to exit 1 (mirroring its existingRegistry→ exit 1 arm). The message carries the verb name and the substring"needs a project", mirroring
run_campaign's wording(campaign_run.rs:322-333).
Collateral — test migration. A well-formed dissolved verb run to completion
in a bare temp cwd was the pre-#218 behaviour a set of
cli_run.rstests pinnedas correct — the execution/grade tests (
sweep_real_…,walkforward_real_e2e_…,mc_r_bootstrap_real_e2e_…,generalize_real_e2e_…,plus the
*_dissolves_*family). Those migrate onto a project fixture (thebuilt_project()/project_lock()pattern) so they still pin their grades — awell-formed verb now correctly requires a project. The exit-2 usage-refusal
tests stay unchanged under the corrected gate placement. The exact migration set
is measured against the corrected placement, not the ~30 tests the
dispatcher-top attempt flipped.
The first implementation line (gate at the dispatcher top, via a RED-first
mini-fix) dead-ended on this collateral and was wound back to the green base;
the work re-enters as a spec-driven cycle. The outside-project refusal is pinned
per verb; the RED assertion the mini-fix produced is folded into that spec's
tests.
Spec auto-signed (/boss, grounding-check PASS)
The spec for the dissolved-verb project gate was auto-signed on a
grounding-check PASS with no human in the loop. An independent fresh-context
agent ratified every load-bearing assumption against currently-green tests: the
axis-validation-before-store-touch ordering in
validate_and_register_axes,run_campaign's existing provenance gate, the 24 execution tests that today runa verb to completion outside a project, the exit-2 usage-refusal tests, and the
built-project fixture pattern.
Scope confirmed empirically: a throwaway probe of the corrected gate placement
(inside
validate_and_register_axes, after axis validation) flips exactly 24cli_run.rsexecution tests — all of which pin the pre-#218 behaviour of a verbrunning outside a project — while the 6 exit-2 usage-refusal tests stay green.
Those 24 migrate onto the built-project fixture (grades re-pinned in-project,
topology_hashheld constant as the no-topology-drift guard); the gate landsafter the migration so the suite stays green throughout.
Design settled and grounded. This comment records the signature for
after-the-fact veto.
Cycle closed (/boss, drift-clean)
Landed as two commits: the e2e-test migration into the built project fixture
(
2162bd6) and the gate itself (f6d8048). The four dissolved verbs now refuseoutside a project (exit 1,
<verb> needs a project, no store) at the sharedvalidate_and_register_axeschokepoint — placed after axis validation, so amalformed
--axisstill exits 2 regardless of project. Four per-verb pins assertthe refusal; the six exit-2 usage-refusal tests stay green. Workspace suite green
(62 groups / 0 failed), clippy
-D warningsclean.Cycle-close audit: drift-clean against the ledger — gate parity with
campaign_runrestored, no new invariant, no doc drift. One debt item surfacedand filed forward as #223: the
built_project/project_lockfixture helpers arenow duplicated across three test binaries that share one
runs/store; theshared
tests/commonextraction was deliberately deferred to keep this bug fix'sblast radius inside
cli_run.rs.