Repository navigation
The layer journey as a scenario: self install, repo set in every combination, deploy and deploy thenobles, then the move — on every platform #855
Description
Feature: #463 — writ command surface: layers, segments, adoption. Asked for 2026-09-07 after the
real thing ran on Danoble-MBP-A: "We need an integration test as complete as the scenario we just ran.
It must run on our complete platform matrix: Darwin, Linux, and Windows, all architectures. Ubuntu
distro." Extended the same day: "The integration test(s) should cover this scenario: writ self install; writ repo set [base|team|personal] [url|path], all combinations in turn; writ deploy and
writ deploy <another> to be sure that writ deploy thenobles works."
The journey it replays
The real writ binary in a pristine sandbox, on every platform, from a binary that has never run to a
converged machine and through a layer's own history moving under the deployed links:
writ self install— the binary installs itself into the sandbox;repo listis empty, with no
placeholder read as a registration (writ self install creates empty layers/base and layers/team, which repo list reads as registrations and repo add refuses #840).writ repo set, all combinations in turn — base, team, personal, each by path and byfile://
URL, set over one another (replacement narrated, a writ-owned clone removed, a user path never),
unset, and the old verbs unknown (writ repo set and unset replace add and remove: a layer has one registration, and re-pointing it is set #791, writ repo remove leaves the clone it made in the writ-owned home #792, writ names a cloned repository by its layer; it should name it the way git clone does #793); a URL-registered clone brought forward when
its remote advances (writ has no command to refresh a repository it cloned; a layer's manifests stay at the clone's checkout until the user runs git by hand #812). Then every one of the seven non-empty layer subsets is registered and a
bare deploy proves the implicit set is exactlycommon*from the registered layers plus each
registered repository's own-named project (The implicit projects are common and one per configured layer repository; a repository-named project in any layer deploys unnamed #850) —personalalone, the state this machine lived
in for months, among them, andbasealone from a fresh self install as Break the bootstrap cycle: deploying base must not require base #477's proof.writ deploy, thenwrit deploy thenobles— the bare form converges the implicit set (writ deploy with no project refuses, though common is implicit and the config path handles zero projects #843,
The implicit projects are common and one per configured layer repository; a repository-named project in any layer deploys unnamed #850); four collisions narrated, personal over base (Report conflicts when two sources provide the same path #470); selectors put.Darwinon macOS only,
.Linux/.Debianon Ubuntu only,.Windowson Windows only,commoneverywhere; every consumer
answers--help, on Windows under Git for Windows' bash.thenoblesis added by name, is still
there after the next bare deploy (The implicit projects are common and one per configured layer repository; a repository-named project in any layer deploys unnamed #850's persistence), is removed bydecommissionwith the rest
re-converged, andcommonand a repository-named project are refused by name (writ decommission: --all removes everything writ deployed, a named removal re-converges the rest, and an implicit project is refused by name #851).- The move (noblefactor-ops#147, personal#172): personal advances to the commit that renames
noblefactortonoblefactor-opsand moves its consumers out ofcommon*, with no redeploy — the
links dangle, counted; the dry run says nothing (writ deploy --dry-run returns before the conflict pre-flight, so a dry run cannot say what stop will refuse #853);--conflict=replacerelinks everything to
the base's helper and leaves the old links under~/localas orphans (writ upgrade does its whole job: links added, orphans removed, stale links relinked, copies regenerated -- what the name promises #845); the store still counts
them after a hand cleanup (writ upgrade does its whole job: links added, orphans removed, stale links relinked, copies regenerated -- what the name promises #845); the default policy accepts writ's own links; a file outsideHome/
forces--allow-dirty(writ deploy judges dirtiness at the repository root: a modified Inventory file forces --allow-dirty though only Home/ is planned or served #852).
The rule that makes it land now
Every step is the ruled interface. Where today's binary lacks it, the step skips by the name of
the issue that ships it, keyed on a capability probe, and the run prints its skip-list. Nothing is
rewritten to today's verbs. The scenario is therefore the writ lane's executable acceptance test for
#840, #791, #792, #793, #843, #850 and #851, green on the day each lands. Part 3's rows about what
deploy leaves behind assert today's behaviour with the issue in the failure message, so a fix's landing
is visible as the scenario failing at that step.
Where it runs
The existing scenario job: darwin-arm64, linux-amd64 (ubuntu-latest, 24.04), linux-arm64
(ubuntu-24.04-arm), windows-amd64, windows-arm64, each leg independent. Decision needed: "all
architectures" includes darwin/amd64, which docs/plans/arm64-build-and-test-matrix.md ruled built but
never run — Intel macOS bills at 10×, the runner labels are a closed set (macos-15-intel,
macos-26-intel). Either that ruling stands and the matrix stays at five, or macos-15-intel joins it
for the scenario job.
Shape: the hybrid (ruled 2026-09-07)
A second scenario beside TestWritDeployScenario — its own file, cmd/writ/scenario_layer_journey_test.go,
so it does not collide with the writ lane's edits to the first — reusing the sandbox, runWrit,
initializeRepo and the assertion helpers. One test body, two ways to fill its layer table:
- Fixture mode, the PR gate. Four checked-in trees under
cmd/writ/testdata/layer-journey/
(base,team,personal-a,personal-b; the helper is the real file from noblefactor-ops at
dee8e84e5) become repositories named as the real ones —noblefactor-ops,devlore-cli,
personal— each with a bare clone in the sandbox, so every layer has a path and afile://URL and
commit B reaches a by-URL run through its remote. No token, no network, runs on forks; the five-leg
scenariojob runs it viamake test-scenario. - Real mode,
WRIT_SCENARIO_REAL=1, never on the PR gate. noblefactor-ops by URL atdee8e84e5,
devlore-cli as the running checkout, personal at27e53171and54de61d6— the real move, one commit
apart — by path locally and by URL in CI withPERSONAL_LAYER_TOKEN(fine-grained, contents read,
that one private repository). Asserts invariants, not inventories; personal's git-crypt files stay
ciphertext in a sandbox that holds no key. Runs fromscenario-real.yamlon a schedule and on
dispatch, falling back to the fixture personal when the secret is absent.
Special-purpose GitHub repositories were weighed and set aside: three more things to keep in step, for
the one thing the two public real repositories already give — a clone over the network. The
consumer-CI half of the real move (shellcheck following the helper from a pinned base checkout) is
personal's workflow and out of scope: it ran for real on personal#174.
Acceptance
- the scenario runs on all five current legs, gated behind
WRIT_SCENARIO_RUN=1like the first,
green where the rulings are shipped and skipping by issue where not; the skip-list is identical on
every leg - every assertion's failure message names the issue it references
- the Intel macOS decision is recorded, one way or the other, here and in the plan
-
docs/plans/writ-deploy-scenario.mdandplatform-test-matrix.mdname the second scenario;
writ self install creates empty layers/base and layers/team, which repo list reads as registrations and repo add refuses #840, writ repo set and unset replace add and remove: a layer has one registration, and re-pointing it is set #791, writ deploy with no project refuses, though common is implicit and the config path handles zero projects #843, The implicit projects are common and one per configured layer repository; a repository-named project in any layer deploys unnamed #850 and writ decommission: --all removes everything writ deployed, a named removal re-converges the rest, and an implicit project is refused by name #851 each carry a comment naming the step that is their acceptance test
Plan
docs/plans/feature/855-layer-move-scenario.md on feature/855-layer-move-scenario.
Related
#346 (the first scenario), #470 (the three-layer fixture it asked for), #840, #791, #792, #793, #843,
#850, #851 (the rulings Parts 0–2 test), #845, #852, #853, #831, #842 (the behaviours Part 3 names),
noblefactor-ops#147 and personal#172 (what Part 3 replays).
Activity
[-]A layer-move scenario: the base takes the foundation, personal renames its project, and every platform proves the deploy, the breakage, and the redeploy[/-][+]The layer journey as a scenario: self install, repo set in every combination, deploy and deploy thenobles, then the move — on every platform[/+]David-Noble-at-work commentedon Sep 8, 2026
Ruled 2026-09-07, both proposals accepted verbatim:
The plan is approved as amended (hybrid: fixtures gate, real mode scheduled). Phase 1 begins.
test(writ): the journey's selector table follows writ's distro rule; …
test(writ): the layer-journey scenario -- self install, repo set, dep…
David-Noble-at-work commentedon Sep 8, 2026
Phases 1–4a landed in #859 (2026-09-08). The second run was green on all six legs — darwin-arm64, darwin-amd64 (
macos-15-intel, its first outing), linux-amd64, linux-arm64, windows-amd64, windows-arm64 — with the same skip-list on each: #791 (carrying #792 and #793), #812, #840, #850, #851, and #860 on the Ubuntu legs. The first run taught two things, both folded in: writ names a distribution byIDalone (now #860, ruled), and a Windows checkout rewrites the fixtures' line endings (the fixture tree pins LF).Phase 4b — real mode,
scenario-real.yaml,PERSONAL_LAYER_TOKEN— is the next PR here. The writ lane's recorded order clears #812 and #860 first; each landing should turn its skip into a pass without this scenario changing.refactor(layout): New-DevloreKeyVaults lives in Home/noblefactor-ops.…