gates propose: one atomic transaction - #374
Merged
Merged
Conversation
Follow-up to #373: the CLI's count-decide-record sequence is replaced by one ProposeAsync call. The transaction lives kernel-side under the store lock precisely so no caller can reassemble the budget race by invoking the pieces — this command was the last such caller. Black-box regression: the full 44-scenario sweep passes unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
IanFrelinger added a commit
that referenced
this pull request
Aug 23, 2026
* feat(cli): the init verb — application half of the scaffold slice (#366) Wires the tested ProjectScaffold (merged in #365) to `ashlar init <name>`. The command is deliberately thin: it chooses where files go and refuses to overwrite; every content and validity decision lives kernel-side in ProjectScaffold, which round-trips its own output through the loaders and refuses a non-sealed default by code. Split from the kernel half per the layer-boundary rule (master is kernel-first; this targets the application/studio-cli integration branch, mirroring application/mcp-a2a). Also adds a RootCommandRegistrationTests assertion that `init` stays registered — the front door of the product loop must never silently vanish from the root the way `trust` did in #162, which is exactly the failure mode that test file exists to guard. Exercised in the container: `ashlar init demo-app` writes both documents with the policy-review guidance; rerun refuses with exit 1. Registration tests 3/3. Co-authored-by: PlzTouchGrass <130718671+PlzTouchGrass@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * feat(cli): ashlar verify — the wall, rendered for real (#368) The verb behind the motion study. Thin over ProjectVerifier (#367): reads the two documents, renders each course with the fixed vocabulary, maps outcomes to SPEC-009 exit codes (0 verified · 65 verification failed · 1 not-a-project), honours NO_COLOR and redirected output. The verdict word is VERIFIED, not CERTIFIED — certified means signed, and nothing signs yet, so the gold line says exactly that: "unsigned — signing arrives with the ledger". When real keys land, the provenance course and signature appear here; until then this command prints the truth. Exercised in the container: ashlar init invoice-triage && ashlar verify ✓ course 1 · contract ✓ course 2 · composition ✓ course 3 · envelope ✓ VERIFIED 3 courses · unsigned (flip policy to proposing with budget 0) × FAILED course 'envelope': ... seal it or fund it exit 65 Registration guard added alongside init's. Registration tests 4/4. Co-authored-by: PlzTouchGrass <130718671+PlzTouchGrass@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Portability gate: the loop on Linux, Windows, and macOS (#370) * ci: portability gate — the product loop on Linux, Windows, and macOS "Fully portable" is a product requirement, and until now nothing enforced it: every green check runs on ubuntu. This gate runs the REAL loop on all three OSes — ashlar init, ashlar verify (exit 0, verdict present and honest about being unsigned), the broken-envelope path (exit 65 exactly, per SPEC-009, with the "seal it or fund it" reason), and the overwrite refusal (exit 1). Path-filtered to the CLI, the manifest contract, and itself, so the 3-OS matrix only spends runners when the loop's surface changes. Portability that is not gated regresses; this is the cheap insurance that it cannot regress silently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(ci): capture verify's exit code -e-safely in the portability gate All three OS legs failed at the same line — and the log is the good news: init and verify ran and printed VERIFIED on every runner, so the loop IS portable. The failure was this script: GitHub invokes bash with -e, and `out=$(cmd); rc=$?` aborts at the assignment when cmd deliberately exits 65, before the assertion that expects 65 can run. Captures now use `&& rc=0 || rc=$?`, which -e cannot interrupt. The gate failing on its own first run, on the exact step that tests the failure path, is the gate working — just one layer higher than intended. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: PlzTouchGrass <130718671+PlzTouchGrass@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * ashlar gates + the 44-scenario sweep (#372) * feat(cli): ashlar gates — the human half of admission The 2 AM approval, in the terminal. Thin over the kernel (#371): every rule — who may transition what, refusals require reasons, immutable history — lives in GateStore and is pinned by kernel tests; this class reads .ashlar/gates and renders in the fixed vocabulary. Gold appears only when a stone is seated. ashlar gates list what is held ashlar gates --show <id> courses, provenance, diff, state ashlar gates --admit <id> --as me seat the stone (actor recorded) ashlar gates --refuse <id> --reason "..." refusal teaches: recorded, fed back Plus a HIDDEN `gates propose --file p.json` — the runtime's entry, used by tests and demos until the SelfExtendRunner wires in. Hidden because users never propose; applications do. It loads the policy, counts admissions in the budget window, runs AdmissionGate.Decide, records the outcome, exit 65 on rejection. Exercised end to end in the container — the full thesis loop, real for the first time: init -> verify -> propose (HELD, "a person seats the stone") -> gates lists it -> --admit seats it ("admitted by ian.f · recorded") -> --show renders the record -> a tool proposal rejects with "never the application's", exit 65. Registration guard added (5/5). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(e2e): 44-scenario behavioural sweep for the loop, wired into the 3-OS gate scripts/e2e-loop.sh exercises the shipped CLI against every behavioural claim the loop makes — no mocks, every invocation a fresh process (so durability is inherent to the assertions, not a separate test). Coverage: init (creation, refusals leave files untouched, name validation writes nothing, paths with spaces), verify (all courses, every failure path with its exit code, no fabricated signatures, no raw ANSI when redirected), and gates (hold/admit/refuse/show across processes, sealed rejection, envelope-beats-mode-and-budget, failed and missing courses, budget degrade-to-held, append-once, illegal ids fail closed, malformed JSON). The first run scored 28/44 and earned its keep on the spot: all 16 failures traced to ONE real CLI bug the hand-run demo missed — --path was declared on `gates` but System.CommandLine does not inherit parent options into subcommands, so `gates propose --path` failed to parse. The manual demo used the cwd default, which is exactly why manual testing never caught it. Fixed by making --path a global option of the gates command. Second run: 44/44. The sweep now runs as a second step of the portability gate, so every claim is enforced on Linux, Windows, and macOS — "fully works" as a gate, not a status report. (Harness itself kept portable: awk over grep -P, which macOS lacks.) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: PlzTouchGrass <130718671+PlzTouchGrass@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * refactor(cli): gates propose uses the store's atomic transaction (#374) Follow-up to #373: the CLI's count-decide-record sequence is replaced by one ProposeAsync call. The transaction lives kernel-side under the store lock precisely so no caller can reassemble the budget race by invoking the pieces — this command was the last such caller. Black-box regression: the full 44-scenario sweep passes unchanged. Co-authored-by: PlzTouchGrass <130718671+PlzTouchGrass@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * ashlar run: the loop's last verb (#379) * feat(cli): ashlar run — the loop's last verb, verified first, manifest-driven The product rule this verb enforces: YOU CANNOT RUN WHAT DOES NOT VERIFY. The courses execute first; a failing wall refuses the run at exit 65 naming the course, with the fix pointer. Only a verified project reaches the orchestrator. v0 semantics, stated honestly in the class doc: the request runs through the existing orchestration machinery with provider/model taken from the manifest's first modelled agent — the scaffold ships mock, so a fresh project runs offline with zero setup (ASHLAR_ALLOW_MOCK mirrored from the CI gates when the manifest opts into mock). Mapping each manifest agent onto its own orchestration role is the M1 integration; this slice makes the manifest the thing that decides HOW a run executes. Provider comes from the manifest, not from flags. Orchestrator is factory-injected (the ChatCommand pattern) since ServiceProvider is private to Program. Observed end to end in the container — including our own earlier honesty fix firing in the wild: the mock run's fallback agent logged "GenericAgent performed no work" exactly as the production-grade pass designed. Sweep grows to 47 scenarios (run-refuses-unverified at 65, run-mock-completes, run-not-a-project): 47/47. Registration guard added (6/6). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: restore the run verb's registration, guard, and sweep scenarios The first commit of this PR carried only RunCommand.cs: a background CI watcher did a checkout + reset --hard on the shared working tree while these three edits sat uncommitted, and silently discarded them. The verb compiled but was never registered — exactly the built-but-never-added failure mode the guard test exists to catch, which is also why the guard is in this commit. Process fix adopted alongside: background watchers now only merge PRs; branch syncs that touch the working tree happen in the foreground, where uncommitted work is visible before anything resets it. Registration guard 6/6; sweep at 47 scenarios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: PlzTouchGrass <130718671+PlzTouchGrass@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: PlzTouchGrass <130718671+PlzTouchGrass@users.noreply.github.com> Co-authored-by: Claude Opus 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 freeto 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.
Follow-up to #373: the CLI count-decide-record sequence becomes one kernel-side ProposeAsync call under the store lock — the last caller that could reassemble the budget race is gone. Full 44-scenario sweep green.
🤖 Generated with Claude Code