Skip to content

feat(basecamp): opt into the inspector build via setup --inspector - #266

Closed
fryorcraken wants to merge 6 commits into
masterfrom
feat/basecamp-inspector-attr
Closed

feat(basecamp): opt into the inspector build via setup --inspector#266
fryorcraken wants to merge 6 commits into
masterfrom
feat/basecamp-inspector-attr

Conversation

@fryorcraken

@fryorcraken fryorcraken commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Closes #265.

What this does

bin-bundle-dir-inspector — the Basecamp output with the QML inspector compiled in — could not be selected through scaffold, leaving a sitometres UI end-to-end suite as the last thing reaching for raw nix build in an otherwise fully lgs-driven project.

The issue proposed appending the attr to BASECAMP_PORTABLE_ATTRS. That fixes the build selection but leaves the opt-in as a flake attr name the user has to know exists and spell correctly — for an output upstream deliberately marks as not a release artifact. This surfaces it as an intent instead:

$ lgs basecamp setup --inspector
inspector build selected (.#bin-bundle-dir-inspector) — test-only, not a release artifact
cloning basecamp at aa237766...
  • --inspector selects the build and persists the choice, so later runs stay on it without repeating the flag.
  • --no-inspector restores the default. It only undoes our attr — a hand-set bin-appimage survives, and scaffold says so rather than appearing to act.
  • Neither flag leaves the configured attr alone, so a plain setup re-run never silently drops the opt-in.
  • Both edit only the current host's entry. setup builds for the machine it runs on, so a Linux developer's toggle must not discard the project's aarch64-darwin mapping.

What this unblocks

The consumer is a sitometres UI end-to-end suite, which drives Basecamp headlessly through the QML inspector (via logos-qt-mcp). The inspector is a compile-time feature, deliberately off in every shipping output, so no release artefact can be driven this way.

Downstream context: fryorcraken/logos-radicle-module#4 converted both CI workflows to lgs, with its UI-test job pointing sitometres' --app-dir at .scaffold/basecamp/portable/ and running 18/18 steps green. The single raw nix build it could not convert is the inspector bundle.

The classification was the actual bug

Confirmed against the pinned flake (aa23776, tag 0.2.3): bin-bundle-dir-inspector is dirBundler appDistributedWithInspector, built from the same portable = true derivation as bin-bundle-dir and differing only by enableInspector. It is the portable stack by construction — the issue's reasoning holds.

Classified as dev it picked LogosBasecampDev for the XDG subpath and the cli lgpm variant, so Basecamp declined to load every module with one variant ... not supported on this platform line and opened to an empty UI.

Three adjacent instances of that same quiet failure, all reachable from the new flag and fixed here:

  1. Stale [repos.lgpm].attr. Only defaulted when empty, so toggling the stack on an already-set-up project left cli against a portable build. The two are one choice and now move together; an attr scaffold doesn't manage is preserved but warned about.
  2. Choice lost on a failed build. Persisted only after the long basecamp build, so an interrupt reverted it. An explicit flag now writes both attrs before the build.
  3. --no-inspector was a silent no-op on a project with a per-platform attr map — it compared the scalar, not what effective_attr resolves. Now compares the effective attr, and reports when it declines.

Also included

From the issue's follow-up comment: --print-output on basecamp build / build-portable. Reuses the flag install already exposes rather than adding --print-build-logs as a second name for the same thing. As the follow-up notes, there was no log-carrying module build left in CI: a failing build reported the derivation failure without the compiler output explaining why.

Docs also record .scaffold/state/basecamp.state and its three keys. setup has always written it and nothing documented it — which left --inspector telling a harness author to build something while leaving them to derive its path, and that path is exactly what they must not derive (the correct entry point under the nix output varies by basecamp generation and stack).

That contract now lives in docs/basecamp-module-requirements.md — the doc basecamp docs prints — rather than only in docs/commands.md. Dogfooding this PR downstream showed why: a consumer looking for "where does setup leave the binary" goes to basecamp docs, finds the module contract, and reconstructs a cache path by hand when it isn't there. The section covers the three things a consumer cannot infer — read it rather than reconstruct it, it is a read-only nix out-link whose real ELF is a dot-prefixed sibling on portable builds, and it moves whenever the stack moves (so --inspector invalidates a cached path).

Breaking change for library embedders

BasecampCommand::Setup goes from a unit variant to a struct variant so it can carry the tri-state. BasecampCommand is a public enum on a published crate, so any embedder constructing it stops compiling. The CLI is unaffected, and nothing in tests/ exercises the API surface, so CI does not catch this.

// before
project.basecamp(BasecampCommand::Setup)?;

// after — `None` is the previous behaviour: leave the configured stack alone
project.basecamp(BasecampCommand::Setup { inspector: None })?;

Recorded in ADR.md and in the rustdoc on the variant itself. No version bump in this PR — this repo bumps in standalone chore: commits, so one is expected to follow separately. #[non_exhaustive] is deliberately not added: it would itself be a breaking change and is a separate decision.

Split out of this PR

The issue's other aside — setup preserving scaffold.toml comments — is a change to the config writer that every scaffold command goes through, so it is on its own branch (fix/preserve-scaffold-toml-comments) with its own PR. It is unrelated to selecting a basecamp build, and reviewing them together would let a problem in either block the other.

Testing

  • 796 tests green: 596 lib + 192 CLI + 3 API + 5 doc, 0 failures.
  • cargo fmt --check clean; cargo clippy --all-targets warning count unchanged from the base commit (62 pre-existing, 0 new — an earlier revision of this description said 64, which was wrong). None of the 62 is in a file this PR touches.
  • Unit tests cover the attr selection round trip, per-host map editing (including that a sibling platform's entry survives both directions), the lgpm realignment both ways, and the inspector attr's stack classification — asserted via the XDG subpath and variant key, not just a boolean.
  • Verified end to end locally: --inspector persists attr = "bin-bundle-dir-inspector" and lgpm.attr = "cli-portable" even when the build is interrupted.

The earlier LOGOS_SCAFFOLD_E2E_NIX=1 gate is gone (review feedback). ensure_nix_present is only a which("nix"), so a stub nix on PATH satisfies it and an unresolvable [repos.basecamp] source fails the clone in milliseconds — which is well inside the pre-build window the test is actually about. basecamp_setup_inspector_persists_both_attrs_before_the_build now runs unconditionally in CI in 0.01s with no network and no nix store, alongside a sibling covering the per-platform-map case.

Docs updated per CONTRIBUTING: docs/commands.md for the flags, docs/basecamp-module-requirements.md (what basecamp docs prints) for the .scaffold/state/basecamp.state contract, and DOGFOODING.md for B1 / B5 plus the rerun-guidance rows.

🤖 Generated with Claude Code

@fryorcraken
fryorcraken requested a review from a team September 4, 2026 01:08
fryorcraken added a commit to fryorcraken/logos-radicle-module that referenced this pull request Sep 4, 2026
The last `nix build` for a Basecamp in CI is gone. `lgs basecamp setup
--inspector` (logos-co/scaffold#266) selects `.#bin-bundle-dir-inspector`
and — the half that actually mattered — classifies it as the portable
stack, moving `[repos.lgpm].attr` to `cli-portable` with it. Setting
`[repos.basecamp].attr` by hand was expressible all along and would have
built the right binary onto profiles it cannot load: lgpm left on `cli`
accepts only `-dev` variants, so Basecamp declines every module with one
warning line and opens to an empty UI.

The rev needs no flag — scaffold reads `[repos.basecamp].pin`, which is
already the rev this job wants. But the Nix store cache is still keyed on
the workflow's own `BASECAMP_REV`, so the two can now drift: a new step
asserts they are equal rather than letting a bumped pin silently turn
every run cold.

The sitometres probe workaround stays. That was the open question, and
the answer is no: scaffold's `basecamp_bin` is inside a nix out-link like
`result-bundle` was, read-only, with the ELF still at
`.LogosBasecamp.elf` where the probe looks for `.LogosBasecamp`.
Confirmed by pointing `--basecamp` straight at it and getting the same
"no Basecamp with the QML inspector compiled in". `lgs` changed where the
bundle comes from, not what is in it — this one is sitometres' to fix.

Also from #266, both restoring what the conversion to `lgs` gave up:

- `--print-output` on both workflows' module builds. A failing build now
  explains itself in the job log instead of pointing at a file on a
  runner about to be destroyed.
- `setup` no longer strips `scaffold.toml` comments, so the
  `runtime_dir` / `sun_path` notes survive a run. Verified: a
  `setup --inspector` changed only the two `attr` lines.

#266 is unmerged, so `LGS_VERSION` becomes `LGS_REV` — a pinned commit,
not a branch, so the tool driving every build cannot move under CI and
the cache key stays meaningful. Both workflows must carry the same value.
Revert both to a crates.io version when #266 ships in a release; the flag
names do not change.

Verified locally before pushing: `setup --inspector` in a worktree, then
browse.yaml 18/18 green against the resulting bundle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fryorcraken

Copy link
Copy Markdown
Collaborator Author

Dogfooded this in the downstream consumer the PR description names, fryorcraken/logos-radicle-module#5. Both flags work as described, and the last raw nix build for a Basecamp is gone from that repo's CI.

Evidence

Locally: lgs basecamp setup --inspector in a fresh worktree, then browse.yaml 18/18 green against the resulting bundle.

In CI, all four sitometres specs (browse, branches, source, sync) pass against a real Basecamp built by setup --inspector, alongside the four fast-layer jobs. That run also exercises build-portable --print-output.

The classification is the part I checked hardest, since it is the half that would fail silently:

  • lgs basecamp paths alice reports module_root under Logos/LogosBasecamp, not …Dev.
  • [repos.lgpm].attr moved to cli-portable in the same run.

Confirming the lockstep matters: with lgpm left on cli, Basecamp declines every module with one warning line and opens to an empty UI. That is precisely the failure shape this repo has been bitten by before, and it is invisible until a spec times out on its first step with nothing useful in the runner's output.

Comment preservation also verified against a real case — this repo's scaffold.toml carries load-bearing notes about a sun_path overflow that made every module segfault. A setup --inspector run changed only the two attr lines and left the comments intact.

Three notes from the integration

None are blockers; the first is the only one I would consider acting on.

1. setup does more than a CI consumer needs. The e2e job wants the inspector bundle and nothing else, but setup also builds lgpm and seeds the alice/bob profiles. Cheap next to a Basecamp build, and I would rather have that than hand-roll a flake ref — but if a --no-profiles-shaped flag ever appears, this is a consumer for it. Not worth adding speculatively.

2. The pin is now load-bearing in a place that can drift. With setup doing the build, the rev comes from [repos.basecamp].pin — but this workflow's Nix store cache is keyed on its own BASECAMP_REV env var. Bump one and not the other and every run silently goes cold. That is the consumer's problem, not scaffold's, and I have added a step asserting the two are equal. Flagging it because any repo doing this conversion inherits the same trap.

3. basecamp_bin in .scaffold/state/basecamp.state is the useful output, and I did not find it documented. basecamp docs covers the module contract thoroughly but does not mention where setup leaves the binary or that the path is recorded there. A consumer that needs the binary — which is anything driving Basecamp headlessly — either reads that file or reconstructs ~/.cache/logos-scaffold/basecamp/<pin>/app-result/bin/… by hand. Worth a line in docs.

What still blocks full lgs coverage, and it is not scaffold

The bundle still cannot be handed to sitometres directly. basecamp_bin sits inside a nix out-link — read-only, with the real ELF at .LogosBasecamp.elf — and sitometres 0.1.0's hasInspector() probes [binPath, .<base>], never .<base>.elf. So a copy-and-symlink step still sits between lgs and the spec run.

I checked whether this PR removed the need for it and it does not: pointing --basecamp at scaffold's own binary produces the identical "no Basecamp with the QML inspector compiled in". Scaffold changed where the bundle comes from, not what is inside it, and there is nothing scaffold can do about it. Tracked upstream as paradoxcomputer/sitometres#1 — a one-line fix to that candidate list.

Worth stating plainly since the PR description points at this consumer: with #266, scaffold is no longer what stands between lgs and a fully-lgs e2e pipeline.

@weboko weboko left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at a806193. Verified on Linux (x86_64-linux) with Nix 2.35.2 and a real nix on PATH.

What I checked out as correct. The classification argument holds against the pinned flake. In logos-basecamp@aa23776, flake.nix:230 is binBundleDirInspector = withMainProgram (dirBundler appDistributedWithInspector), and appDistributedWithInspector (flake.nix:180-186) is the same ./nix/app.nix import as appDistributed with portable = true; enableInspector = true. nix/app.nix:201 shows portable is what drives -DLOGOS_PORTABLE_BUILD, and it is also what selects the unwrapped $out/bin/LogosBasecamp layout — so adding bin-bundle-dir-inspector to BASECAMP_PORTABLE_ATTRS (and with it Logos/LogosBasecamp rather than Logos/LogosBasecampDev, and cli-portable) is right, not a guess. nix eval .#bin-bundle-dir-inspector.drvPath resolves cleanly at that pin.

The flag itself behaves as documented end to end: setup --inspector writes attr = "bin-bundle-dir-inspector" and [repos.lgpm].attr = "cli-portable" before the build starts, a later --no-inspector moves both back to app/cli, --no-inspector on a project that never opted in declines with a note instead of silently doing nothing, and --inspector --no-inspector resolves last-flag-wins through clap's overrides_with. 797 tests pass (600 lib / 189 CLI / 3 API / 5 doc), cargo fmt --check is clean, and cargo clippy --all-targets reports zero warnings in any file this PR touches.

The findings below are all in the save_project_config half of the change.


1. save_project_config now panics on a scaffold.toml that parse_config accepts (blocking)

Switching the writer from "render into an empty document" to "merge over the parsed original" means write_config_into now inherits whatever shape the user's file has, and the .expect("… table") calls in it stop being structurally unreachable. Previously serialize_config always started from DocumentMut::new(), so every doc.entry(k).or_insert(Item::Table(..)) genuinely inserted a Item::Table and the expects could not fire. They can now.

The trigger is a root-level inline table for any section whose parser tolerates a non-table value by falling back to defaults. parse_config reads these through Item::as_table, which returns None for an inline table, so the file parses fine — and then the writer reaches for as_table_mut() on the inline value and aborts.

Repro (x86_64-linux, PR head):

wallet = { home_dir = ".scaffold/wallet" }

[scaffold]
version = "0.2.0"
# ... otherwise minimal, valid v0.2.0 config
$ lgs basecamp setup --inspector
inspector build selected (.#bin-bundle-dir-inspector) — test-only, not a release artifact

thread 'main' panicked at src/config.rs:807:27:
wallet table

Confirmed for four sections, each aborting at its own expect:

inline key at document root panic site
wallet = { … } src/config.rs:807 expect("wallet table")
framework = { … } src/config.rs:815 expect("framework table")
localnet = { … } src/config.rs:827 expect("localnet table")
circuits = { … } src/config.rs:839 expect("circuits table")

src/config.rs:821 (expect("idl table")) is the same shape via [framework] + idl = { … }, and ensure_subtable's expect("parent is a table") becomes reachable the same way for modules = { … } once the config actually carries a module (basecamp modules rather than setup).

Two things make this worth fixing before merge rather than filing as a follow-up. It is an abort with a backtrace, not a scaffold-level error — exactly the "raw stack trace with no scaffold-side hint" that DOGFOODING.md B1 calls a UX regression. And it lands on hand-edited files specifically, which is the population this change exists to be kind to; the file that gets a comment written into it is the file most likely to have been hand-shaped in other ways too.

The unparseable-input fallback doesn't cover it, since the file parses. Either coerce a table-like item into a Table before writing, or treat "modelled key exists but isn't a Table" as the same case as unparseable input and fall back to a fresh render — but please make it a returned error or an explicit fallback rather than an expect, since these are now reachable from file content.

2. [basecamp.profiles] is the one conditional emitter the sweep missed (src/config.rs:932)

env and env_append both got the else { basecamp_table.remove(...) } branch; profiles did not. Same defect the PR describes: clearing the profiles while [basecamp] still exists leaves the old tables stranded in the file.

Confirmed with a test in config::tests:

let original = minimal_v0_2_0()
    + "\n[basecamp]\nport_base = 41000\n\n[basecamp.profiles.alice]\nruntime_dir = \"/run/user/1000\"\n";
let mut cfg = parse_config(&original).unwrap();
cfg.basecamp.as_mut().unwrap().profiles.clear();
let updated = update_config(&original, &cfg).unwrap();
// fails: `[basecamp.profiles.alice]` is still in `updated`;
// serialize_config(&cfg) emits only `[basecamp]\nport_base = 41000`.

update_config_clears_keys_reset_to_defaults doesn't catch it because it sets cfg.basecamp = None, which short-circuits to doc.remove("basecamp") at line 979 and never exercises "section kept, profiles emptied". Worth extending that test with the mixed case — it is the vacuous-coverage shape the PR write-up already identified once.

While in there: write_repo_ref is only called under if let Some(repo) = &cfg.basecamp_repo / cfg.lgpm_repo with no else removing a stale [repos.basecamp] / [repos.lgpm]. Nothing sets either back to None today, so it is latent rather than a live bug, but it is the last gap in the "every conditional emitter has a removal branch" invariant the PR states.

3. An unparseable or unreadable scaffold.toml is silently overwritten

update_config does existing.parse().unwrap_or_default(), and save_project_config does fs::read_to_string(&path).unwrap_or_default(). The fallback policy is defensible, but it is silent and there is no backup, so a file that was one stray bracket away from valid is replaced by a from-scratch render — every comment and unmodelled section gone, no message. That is the exact loss the rest of this PR is built to prevent, and the user gets no signal it happened.

unwrap_or_default() on the read also collapses "no file yet" and "file exists but I couldn't read it" (permissions, I/O error) into the same path; the second case silently truncates a file that was there.

A one-line eprintln! naming the fallback would be enough for the parse case. For the read, distinguishing NotFound from other errors and propagating the rest would be better. init's migration already writes a scaffold.toml.bak; matching that here would be the belt-and-braces option.

4. DOGFOODING.md isn't updated, and CONTRIBUTING requires it

CONTRIBUTING.md:44 — "If the change affects user-facing behavior, update README.md and DOGFOODING.md in the same PR." docs/commands.md is updated (thank you — the --inspector paragraph is genuinely good), but nothing in the runbook is.

Concretely missing:

  • B1 drives basecamp setup with no flags. Nothing exercises --inspector / --no-inspector, the [repos.lgpm].attr realignment, or the persist-before-build ordering — and B1's "Expected Success Signals" already enumerate per-platform attr behavior, so the inspector attr belongs alongside it.
  • B5 covers basecamp build / build-portable without mentioning --print-output.
  • Nothing anywhere asserts the new comment-preservation contract. This one matters most: it silently changes the behavior of every command that rewrites scaffold.toml, not just the ones this PR touches, and the failure mode it introduced mid-review (keys reset to defaults being stranded) is invisible unless a scenario looks for it. B1 already says to capture a scaffold.toml excerpt as evidence — a check that a comment placed before setup is still there afterward, and that a value reset to its default is actually gone, would be cheap and would have caught findings 1–3.

The "Minimum Rerun Guidance" table should probably also gain a row for changes to the config writer, since the blast radius is every config-writing command rather than just basecamp.

5. Minor: basecamp_setup_inspector_persists_both_attrs_before_the_build does real network + Nix work in cargo test

The doc comment frames this as local-only coverage that no-ops in CI, and that's accurate for CI. But on any developer machine that does have Nix — the machines most likely to be working on this code — a plain cargo test now shells out to basecamp setup --inspector, which clones the pinned basecamp repo into the shared cache root and starts a Nix build of a Qt-heavy closure. It's the only test in tests/cli.rs that reaches the network or the Nix store, and it uses the real default cache root rather than a temp one.

The assertion is worth keeping. Gating it behind an explicit opt-in env var, and pointing LOGOS_SCAFFOLD_CACHE_ROOT at the test's own tempdir, would make it safe to leave in the default run.


Findings 1 and 2 are code defects with reproductions; 3 and 4 are behavior/documentation gaps that follow from the same change. I'd hold approval on 1 in particular — an abort on a scaffold.toml that parses is a worse outcome than the comment loss the change fixes.

I could not complete a full basecamp setup here: the container's proxy answers 403 for Nix's github: flake inputs, and a direct-egress run gets past evaluation but then builds the Qt closure from source, which is the environment limitation DOGFOODING.md B1 documents as the usual B-series blocker. Everything above the Nix build — attr selection, lgpm realignment, persist ordering, config round-tripping — was verified against the real binary.


Generated by Claude Code

@weboko

weboko commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to my review above: the Nix build did finish after all, so the caveat I ended on ("could not complete a full basecamp setup") no longer applies. Bypassing the proxy for GitHub specifically got Nix past the 403 on github: flake inputs, and the closure then built from source.

basecamp setup --inspector completed green on x86_64-linux:

inspector build selected (.#bin-bundle-dir-inspector) — test-only, not a release artifact
  ✓ building basecamp (.#bin-bundle-dir-inspector) (24m24s)
  ✓ building lgpm (github:…/logos-package-manager/202af6fa#cli-portable) (16.2s)
seeded profiles: alice, bob
setup complete

Everything the PR claims about the artefact holds against the real output:

  • The inspector is genuinely compiled in. $out/bin/.LogosBasecamp.elf carries InspectorServer, QML_INSPECTOR_PORT, and [QmlInspector] Inspector server listening on port, and logos-qt-mcp (1fea5094) is in the recorded build inputs. nix/app.nix:202 confirms the path: -DENABLE_QML_INSPECTOR=ON plus -DLOGOS_QT_MCP_ROOT. This is the build a sitometres harness needs.
  • The portable classification is right in practice, not just in the flake source. Profiles seeded under xdg-{data,cache,config}/Logos/LogosBasecamp — not LogosBasecampDev — and $out/bin/ has the unwrapped portable layout. lgpm built at cli-portable, matching.
  • basecamp doctor → 5 PASS, 0 WARN, 0 FAIL, "Ready".
  • The persistence claim holds where it matters most: a subsequent plain basecamp setup with no flag stayed on bin-bundle-dir-inspector / cli-portable and completed in 20s off cache with no rebuild. That's the behavior the tri-state was designed for, working.
  • Both comments I planted in scaffold.toml survived the write, with [repos.basecamp] / [repos.lgpm] inserted around them.

So the feature does what it says end to end. That doesn't change findings 1–5 — the panic in particular is on a different input shape than the one exercised here — but the headline functionality is verified rather than assumed, and I wanted the record to say so.


Generated by Claude Code

fryorcraken added a commit to fryorcraken/logos-radicle-module that referenced this pull request Sep 4, 2026
* Build the inspector Basecamp through lgs, not raw nix

The last `nix build` for a Basecamp in CI is gone. `lgs basecamp setup
--inspector` (logos-co/scaffold#266) selects `.#bin-bundle-dir-inspector`
and — the half that actually mattered — classifies it as the portable
stack, moving `[repos.lgpm].attr` to `cli-portable` with it. Setting
`[repos.basecamp].attr` by hand was expressible all along and would have
built the right binary onto profiles it cannot load: lgpm left on `cli`
accepts only `-dev` variants, so Basecamp declines every module with one
warning line and opens to an empty UI.

The rev needs no flag — scaffold reads `[repos.basecamp].pin`, which is
already the rev this job wants. But the Nix store cache is still keyed on
the workflow's own `BASECAMP_REV`, so the two can now drift: a new step
asserts they are equal rather than letting a bumped pin silently turn
every run cold.

The sitometres probe workaround stays. That was the open question, and
the answer is no: scaffold's `basecamp_bin` is inside a nix out-link like
`result-bundle` was, read-only, with the ELF still at
`.LogosBasecamp.elf` where the probe looks for `.LogosBasecamp`.
Confirmed by pointing `--basecamp` straight at it and getting the same
"no Basecamp with the QML inspector compiled in". `lgs` changed where the
bundle comes from, not what is in it — this one is sitometres' to fix.

Also from #266, both restoring what the conversion to `lgs` gave up:

- `--print-output` on both workflows' module builds. A failing build now
  explains itself in the job log instead of pointing at a file on a
  runner about to be destroyed.
- `setup` no longer strips `scaffold.toml` comments, so the
  `runtime_dir` / `sun_path` notes survive a run. Verified: a
  `setup --inspector` changed only the two `attr` lines.

#266 is unmerged, so `LGS_VERSION` becomes `LGS_REV` — a pinned commit,
not a branch, so the tool driving every build cannot move under CI and
the cache key stays meaningful. Both workflows must carry the same value.
Revert both to a crates.io version when #266 ships in a release; the flag
names do not change.

Verified locally before pushing: `setup --inspector` in a worktree, then
browse.yaml 18/18 green against the resulting bundle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Move the GitHub-authored actions off the retired Node 20 runtime

Every job logged the same deprecation warning: checkout, cache and
setup-node target Node 20, which GitHub has retired, so the runner was
already force-running them on Node 24. Bumping to the Node-24 majors
(checkout v7, cache v6, setup-node v7, upload-artifact v7,
download-artifact v8) matches what was happening anyway rather than
changing behaviour — it just stops the warning and removes the cliff
when the forcing turns into a hard failure.

`node-version` is a separate axis from the action runtime: that is the
Node sitometres and the spec-schema validator actually run on. Node 20 is
EOL, so it moved to 24 with them. sitometres declares `engines.node:
">=20"` — an open lower bound, not a ceiling — so 24 satisfies it and
there is nothing to raise upstream.

The two third-party actions are unaffected and stay pinned. Checked
rather than assumed: `cachix/install-nix-action@v27` is
`using: composite` (no Node runtime at all), and
`nix-community/cache-nix-action@v7` is already `using: node24`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Stop a third-party apt mirror from failing the UI job

Run 33826324704's `sync` job died before any real work, while its three
siblings passed the identical step minutes apart:

  E: Failed to fetch https://packages.microsoft.com/ubuntu/24.04/prod/… 403
  E: The repository '… noble InRelease' is no longer signed.

The runner image preconfigures that repo; this job does not use it. All
six packages installed here come from Ubuntu's own archive, so a
Microsoft mirror having a bad day should not be able to fail a UI test
run. Remove its list before updating, and let `update` warn instead of
exiting non-zero — `install` remains the real gate and still fails loudly
if a package is genuinely unavailable.

Also repoints the sitometres probe-workaround comment at the upstream
issue tracking its removal, paradoxcomputer/sitometres#1. The one-line
fix is `hasInspector`'s candidate list gaining `.<base>.elf`; when it
ships, that whole step goes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Disable extra apt sources by pattern, not by filename

The first cut of this named /etc/apt/sources.list.d/microsoft-prod.list
directly. That was a guess: I never confirmed the filename on a current
noble runner image, and if the image has moved that source to the deb822
`.sources` format the `rm -f` silently does nothing — leaving a step that
looks hardened and is not, passing only because `update` had been made
non-fatal in the same change.

Move every source that is not Ubuntu's own out of the way instead, by
pattern, covering both `.list` and `.sources`. `find -exec` rather than a
glob so that matching nothing is not an error, and `mv -v` into a
disabled-sources dir rather than `rm` so the log records what was actually
disabled instead of leaving it to be inferred.

Also worth recording: this is not a blip that more retries would fix. The
runner image sets `APT::Acquire::Retries "10"`, so apt had already retried
ten times before surfacing the 403.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Derive BASECAMP_REV from scaffold.toml instead of duplicating it

Before this, two places named the same Basecamp revision: the job's
`env: BASECAMP_REV`, and `[repos.basecamp].pin` in scaffold.toml. That was
harmless while the workflow ran `nix build` with the env var, because one
variable fed both the build and the store cache key. It stopped being
harmless when `lgs basecamp setup` took over the build: setup takes no
--rev and reads the pin, so the workflow's literal became a second source
of truth for one fact.

The divergence would have been invisible. Bump the pin and forget the env
var, and the cache keys on the old rev — restoring a store full of the
previous Basecamp, which the new build cannot use, so it recompiles from
scratch on every run. Nothing errors, the specs still pass, the job just
quietly costs ~9 minutes instead of ~3. CI slowness gets blamed on runners.

So read the pin from scaffold.toml and export it. `tomllib` from the
stdlib (3.11+, runner ships 3.12) rather than a TOML CLI: this is one
scalar, and adding a `pip install` to fetch a TOML reader — in the step
right after the one hardening against package-source flakiness — is a poor
trade. It parses TOML as TOML rather than regexing it, and asserts a full
40-char hex sha, since a truncated pin still yields a usable cache key
while silently not being the rev anyone meant.

Ordering is load-bearing: the resolve step must precede the cache step,
which is why it is separate rather than folded into setup. `env.X` in a
`with:` expression resolves against the accumulated step environment, so
the cache key does see the $GITHUB_ENV write.

The post-setup equality check stays, with its rationale narrowed. It can
no longer catch a hand-editing slip — there is nothing left to mis-edit —
but it still pins the assumption that scaffold builds the pin it was
given, which is worth asserting while `setup --inspector` is unreleased.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Say why the pin assertion rejects uppercase hex

Verified the assertion locally against every shape that would otherwise
yield a plausible-looking cache key — truncated, empty, a branch name,
40 non-hex chars, and uppercase — and confirmed it accepts the real pin.
Also confirmed the derivation resolves to exactly the rev the workflow
previously hardcoded, so the switch to reading scaffold.toml leaves the
cache key unchanged rather than forcing one cold run.

The uppercase case is the non-obvious one and the one someone will
otherwise "relax": the pin goes into the cache key verbatim, so `AA23…`
and `aa23…` key two separate caches for the same rev and quietly halve
the hit rate. Rejecting is better than normalising, since scaffold builds
whatever the file says and only the file should decide the rev.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Read the Basecamp pin with tomlq, per review

Replaces the inline Python. My objection to a TOML CLI was that it needs
installing; the review's answer is that this job already runs apt, so add
it there — which is right, and cheaper than the reasoning I used to avoid
it.

The apt step moves up to just before the resolve step as a result. It now
serves two unrelated needs in one transaction: the graphics libraries the
Basecamp bundle resolves at load time, and `tomlq` for the pin. Ordering
is fenced on both sides — after apt (which provides tomlq), before the
cache step (which keys on what resolve exports).

Worth knowing: the runner preinstalls `yq 4.53.6`, but that is the Go yq
and ships no `tomlq`. The apt `yq` is the Python one that does. Two tools
sharing a name, so the install is not redundant, and `tomlq --version`
runs immediately after so a future image change that swaps them fails
here rather than at the resolve step.

Verified locally against the real scaffold.toml: resolves the same rev the
workflow previously hardcoded, so the cache key is unchanged. The
validation still rejects truncated, empty, branch-name, non-hex and
uppercase pins — and, new to this approach, the literal string "null",
which is what `tomlq -r` prints for a missing key where Python would have
raised KeyError. Confirmed that case rather than assumed it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Make the two Basecamp assertions fail legibly, not cryptically

Three review agents ran read-only over this branch. Two of them independently
landed on the same step — the one that checks the built Basecamp — and for the
same reason: the step whose entire job is to explain a failure produced the
least legible failure in the file.

`. .scaffold/state/basecamp.state` had two problems. Sourcing executes the
file, and `basecamp_bin` is a path, so a value carrying $(...) would run. And
scaffold writes each key CONDITIONALLY (`if !state.pin.is_empty()` in its
state.rs, with a unit test asserting the minimal file omits the other two), so
a missing key is a real shape — one that surfaced as a bare `pin: unbound
variable` from `set -u`, with no ::error:: and no hint, at exactly the moment
someone bumps LGS_REV and the state format has moved under them. Now parsed
with sed and asserted non-empty, naming LGS_REV in the message.

`strings … | grep -q` conflated two different failures. With no pipefail the
exit status is grep's alone, so a bundle whose layout moved — `strings` writing
to stderr and printing nothing — reported "no inspector" for what was really
"wrong path". That matters here because the premise of this PR is that the
bundle moved once already, from result-bundle/ into scaffold's cache. Split
into an explicit -f test and the grep, each with its own message.

Same shape in the .lgx variant loop: without nullglob an empty directory leaves
the literal pattern as "$f", and the run fails blaming a missing linux-amd64
variant rather than saying build-portable produced nothing. Now nullglob plus a
count assertion — two modules, so a drop to one is caught here rather than as a
spec timeout later.

Verified rather than assumed: each case was run against a sandbox fixture, and
each fails with its own message and exit 1 while the happy path still passes.

Also from the reviews, comment-only:

- The resolve step's ordering note said it must run "before the apt step that
  installs tomlq" when the real (and correct) order is after. Rewritten as the
  three constraints that actually hold — after apt, before the cache step, and
  before `setup --inspector`, which rewrites scaffold.toml's attr keys in
  place. Safe today because setup never touches `pin`, but that is now written
  down rather than left to be rediscovered.
- Both workflows note that LGS_REV lives on an unmerged PR branch, so a
  cargo-install failure across every job means re-pin, not debug cargo. Not
  hypothetical: #266 was force-pushed while this change was being written.

And two CLAUDE.md fixes:

- It told a reader to `cargo install logos-scaffold --version 0.3.1` and then
  claimed the e2e sequence "was run against released v0.3.1". Step 1 of that
  sequence is `setup --inspector`, which does not exist in 0.3.1 — so following
  the file got you an unrecognised-flag error. The pinned build is now the
  install instruction, with the release kept as background.
- "It used to carry one" had lost its antecedent to an earlier edit and read as
  though the yq package carried a rev.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Re-pin LGS_REV to scaffold#266's current head

#266 was force-pushed mid-session: 16b4b514 → 8f41e37e. The old pin,
7ba5f981, was two heads behind and is the head of no branch — reachable
today only because the PR keeps it alive, which is precisely the hazard the
comment added in f92cc18 warns about. Moving to the current head shortens
that exposure rather than relying on GitHub not collecting it.

All four of #266's checks are green at 8f41e37e (two `validate`, two
Template E2E `render-and-build`), which is the bar for taking it.

Verified before pinning CI to it, rather than assuming a same-branch commit
is equivalent:

  * `cargo install --git … --rev 8f41e37e` builds clean (two pre-existing
    `unexpected cfg` warnings, no errors);
  * `basecamp setup --help` still carries `--inspector` — and documents the
    same persist-and-`--no-inspector` behaviour the workflow comment relies
    on;
  * `basecamp build-portable --help` still carries `--print-output`.

So this is a rev change and nothing else: no flag renamed, no semantics
moved, and no workflow step needed editing.

All three sites move together — both workflows and CLAUDE.md's local install
command. The two workflows must not diverge (a job pair on different scaffold
builds is the desync `LGS_REV` exists to prevent), and CLAUDE.md's command is
what a teammate would run to reproduce CI locally.

Both cache keys change with the rev, so the next run reinstalls lgs once.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Pin the sitometres probe fix, and delete the workaround it existed for

The copy-and-symlink step is gone. `--basecamp` now points straight at
scaffold's own `basecamp_bin` inside the read-only out-link: no `cp -RL`, no
`chmod -R u+w`, no second filename manufactured beside the wrapper, and
nothing left coupling this workflow to lgs's bundle layout.

Published sitometres (0.1.0) could not run this bundle at all. `hasInspector()`
probed `bin/LogosBasecamp` (a ~5 KB sh wrapper) and `bin/.LogosBasecamp`
(absent), never the `bin/.LogosBasecamp.elf` that nix's dirBundler ships, and
refused a build that plainly has the inspector. Filed as
paradoxcomputer/sitometres#1; fixed by #3, which probes three spellings
(`.<base>`, `.<base>.elf`, `.<base>-wrapped`) and resolves symlinks first.

#3 is unmerged, so SITOMETRES is a git commit, not an npm version — same shape
and same reasoning as LGS_REV, including pinning a COMMIT rather than the
branch. A moving ref would silently change the tool gating every spec, which is
exactly what scaffold#266's mid-session force-push demonstrated. `npx` on a git
ref runs `prepare`, so the TypeScript compiles at install time.

Verified locally rather than assumed, and the result was not what the old
comment predicted:

  * the pinned build launches Basecamp from scaffold's path directly —
    inspector attached on its port, spec reached step 1;
  * the OLD workaround now FAILS against it. `cp -RL` dereferences the `.elf`
    into a real file, and the symlink beside it no longer satisfies a probe
    that resolves before looking: "no Basecamp with the QML inspector compiled
    in", before a single step runs.

So deleting the step is not merely safe with this pin — it is required by it.
The workflow comment says so, and says not to reintroduce it.

Also removes the "built packages carry the variant" assertion, per review. It
was a diagnostic, not a gate: it could not fail a correct build, and
`build-portable`'s whole purpose is that variant. The failure it guarded — the
wrong variant surfacing as one Basecamp warning, an empty UI and a step-one
timeout — is already written up above `Build both modules (portable)`, which is
where anyone hitting it will be reading. Not reintroduced under another name.

Two paths became variables while touching these lines: APP_DIR (job env) and
BASECAMP_BIN (already exported from the state file), so the portable directory
is named once rather than in three steps.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`bin-bundle-dir-inspector` — the Basecamp output with the QML inspector
compiled in — could not be selected through scaffold, leaving a sitometres
UI end-to-end suite as the last thing reaching for raw `nix build` in an
otherwise fully `lgs`-driven project (#265).

Surface it as an intent rather than an attr name. `basecamp setup
--inspector` selects the build and says it is test-only; `--no-inspector`
restores the default; neither flag leaves the configured attr alone, so a
plain re-run never silently drops the opt-in. Hand-writing the flake attr in
scaffold.toml would have worked too, but it makes users spell an upstream
output name that upstream marks explicitly as not a release artifact.

The classification itself is what was wrong: upstream builds the inspector
output from the same `portable = true` derivation through the same directory
bundler as `bin-bundle-dir`, differing only by a compile-time flag, so it is
the portable stack by construction. Classified as dev it picked
`LogosBasecampDev` for the XDG subpath and the `cli` lgpm variant, and
Basecamp then declined to load every module with a single `variant ... not
supported on this platform` line and opened to an empty UI.

Three adjacent instances of that same quiet failure, all reachable from the
new flag:

- `[repos.lgpm].attr` was only defaulted when empty, so toggling the stack
  on an already-set-up project left a stale `cli` against a portable build.
  The two are one choice and now move together; an attr scaffold does not
  manage is preserved but warned about.
- The choice was persisted only after the (long) basecamp build, so an
  interrupt or an unrelated failure reverted it. An explicit flag now writes
  both attrs before the build.
- `--no-inspector` compared the scalar `attr`, so on a project with a
  per-platform map it did nothing and said nothing. It now compares what
  `effective_attr` actually resolves, and reports when it declines to act.

The flags edit only the current host's entry — `setup` builds for the
machine it runs on, so a Linux developer's toggle must not discard the
project's `aarch64-darwin` mapping.

Also from the issue's follow-up: `basecamp build` / `build-portable` accept
`--print-output`, reusing the flag `install` already exposes rather than
adding a second name for it. Without it there is no log-carrying module
build left in CI — a failing build reports the derivation failure without
the compiler output saying why.

Docs record `.scaffold/state/basecamp.state` and its three keys, which
`setup` has always written and nothing documented: a harness needs
`basecamp_bin` rather than deriving a path whose correct entry point varies
by basecamp generation and stack.

Preserving `scaffold.toml` comments — the issue's other aside — is a change
to the writer every command goes through, and is split out to
`fix/preserve-scaffold-toml-comments`.

Closes #265

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fryorcraken and others added 2 commits September 4, 2026 14:53
`--no-inspector` keyed the `[repos.lgpm].attr` realignment on
`inspector.is_some()` — on "a flag was passed" rather than on "the stack
actually moved". So on a project scaffold had never put on the inspector
build, `--no-inspector` printed its "nothing to undo" note, left
`[repos.basecamp].attr` alone as documented, and then rewrote lgpm
anyway: a hand-set `cli-portable` against a dev basecamp silently became
`cli`, persisted before the build. The flag appeared to do nothing while
acting on the half of the pair the user cannot see, which is the exact
silent mismatch the feature exists to close.

`apply_inspector_selection` now returns whether this host's effective
attr moved, and that gates both the realignment and the pre-build
persist — so a run that changes nothing also writes nothing, rather than
rewriting `scaffold.toml` to say what it already said. `--inspector` on a
project already on the inspector build is likewise a no-op.

Also from review:

- `--inspector` / `--no-inspector` are now `conflicts_with`, matching
  `run`'s `--reset` / `--no-reset`. Last-wins was the wrong default for a
  flag that persists to `scaffold.toml`: a fat-fingered pair would have
  rewritten the project's stack with no diagnostic. The tri-state
  collapse moves into a named `inspector_selection` with its own test —
  the neither-flag `None` is load-bearing, and a regression to
  `Some(false)` would drop every project's opt-in on the next plain
  `setup`.
- `basecamp_setup_inspector_persists_both_attrs_before_the_build` is
  gated behind `LOGOS_SCAFFOLD_E2E_NIX=1` and points
  `LOGOS_SCAFFOLD_CACHE_ROOT` at its own tempdir. On a developer machine
  with nix it was shelling out to a real `setup` on every `cargo test` —
  a clone of the pinned basecamp repo and the start of a Qt closure
  build. It was the only test in the suite touching the network, and the
  CLI target's wall time drops from 25s to 5s without it.
- DOGFOODING `B1` gains the inspector round trip (both directions, the
  no-flag re-run, the portable `module_root` check, and the rejected flag
  pair); `B5` gains `--print-output`; the rerun guidance gains a row for
  the stack selector, whose failures are quiet by construction.

Findings 1-3 of the review land on `src/config.rs`, which this branch no
longer touches — that work is PR #267.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dogfooding this PR in a downstream consumer surfaced the gap the previous
commit thought it had closed. `docs/commands.md` documents
`.scaffold/state/basecamp.state` and its three keys — but a consumer
asking "where does `setup` leave the binary" goes to `basecamp docs`,
which prints `docs/basecamp-module-requirements.md`, and that doc covers
the module contract thoroughly while never mentioning the state file at
all. The reported outcome is exactly the one the docs exist to prevent:
hand-reconstructing `~/.cache/logos-scaffold/basecamp/<pin>/app-result/
bin/…`.

Move the contract to the doc that gets printed, and say the three things
a consumer cannot infer from the path:

- Read `basecamp_bin`; do not reconstruct it. Which file under the nix
  output is the entry point varies by generation and stack — a `/bin/sh`
  launcher on the dev `#app` build, the unwrapped binary on a portable
  one, inside `Contents/MacOS/` on `bin-macos-app`. Picking wrong starts
  an app with no Qt plugin path, which reads as a Basecamp bug.
- It is a read-only out-link, and on portable builds the real ELF is a
  dot-prefixed sibling (`.LogosBasecamp.elf`). A harness needing a
  writable tree has to copy, not modify in place.
- It moves when the stack moves. `--inspector` rebuilds at a different
  attr, so a path cached across a `setup` silently execs the previous
  build — the failure the flag was added to make impossible.

Also folds in the pin-drift trap from the same dogfooding report. A
consumer keyed on its own copy of the basecamp rev is pinning it twice
and nothing reconciles the two; that is the consumer's bug to fix, but
it is inherited by every repo doing this conversion, so it costs one
paragraph here to warn about rather than one silent cold cache each.

`docs/commands.md` keeps its summary and now points at the fuller
treatment. `DOGFOODING.md` B1 reads the state file after both the plain
`setup` and `--inspector` — two excerpts, so "basecamp_bin moves with
the stack" is evidenced rather than asserted — and the rerun guidance
gains a row saying plainly that this file is a public interface despite
living under `.scaffold/`: a key rename breaks downstream CI with no
scaffold-side error at all.

No behavior change; the doc is `include_str!`-embedded, so `basecamp
docs` picks it up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fryorcraken

Copy link
Copy Markdown
Collaborator Author

@weboko — thank you for this. The verification half in particular (checking flake.nix:230 and nix/app.nix:201 rather than taking the classification argument on trust, then coming back with the completed 24-minute build) is the review that actually tests the claim, and the follow-up confirming $out/bin/.LogosBasecamp.elf carries InspectorServer is the evidence this PR needed and did not have.

One structural note before the findings: you reviewed at a806193, when this branch still carried the scaffold.toml comment-preservation work. That has since been split to #267 (fix/preserve-scaffold-toml-comments). git diff master...HEAD -- src/config.rs is now empty here, so findings 1–3 — all of which you correctly scoped to "the save_project_config half of the change" — describe code that lives in the other PR. I have not implemented them here; I have verified their status against #267's current head instead. Findings 4 and 5 are this PR's and are fixed.


1. save_project_config panics on an inline table — correct, and still open in #267

I want to be unambiguous about this one, because it was previously disputed. You are right. #267's earlier analysis claimed or_insert appends a separate [wallet] table when the document already holds an inline wallet = { … }, making the output a cosmetic duplicate stanza rather than a panic. That is wrong. I checked it against the crate's own toml_edit 0.22 rather than reasoning about it:

input:  wallet = { home_dir = ".scaffold/wallet" }
        [scaffold]
        version = "0.2.0"

doc.get("wallet").as_table()               -> None      (parse falls back to default, file parses)
doc.entry("wallet").or_insert(Item::Table) -> the existing inline item
  .as_table_mut()                          -> None      <- `.expect("wallet table")` aborts here

Entry::or_insert returns the occupied value; it does not append alongside it. So the abort is real, and your mechanism — Item::as_table returning None for an inline table on the read side is exactly what lets the file through the parser to reach the writer — is precisely right.

Status in #267 at 7f32892: unfixed, verbatim. All five expects you tabulated are unchanged (config.rs:870, 878, 884, 890, 902), ensure_subtable's expect("parent is a table") is unchanged at 1472, parse_config still reads wallet through Item::as_table, and there is no inline-table coercion anywhere in the file. This should block #267, not this PR.

It is not reachable on this branch: serialize_config here still starts from DocumentMut::new(), so as you say the expects remain structurally unreachable until the merge-over-original writer lands. Your framing was right — this is a defect the comment-preservation change introduces.

2. [basecamp.profiles] removal branch + write_repo_ref gaps — fixed in #267

Both halves. #267 added a bc.profiles.is_empty() branch that walks an existing [basecamp.profiles] table, clears the keys scaffold owns, drops entries that end up empty, and removes the parent when nothing survives — with a update_config_drops_a_cleared_basecamp_profile test for exactly the "section kept, profiles emptied" case you noted update_config_clears_keys_reset_to_defaults short-circuits past. The write_repo_ref gap is closed too: basecamp and lgpm now go through a match repo { Some(..) => write, None => repos.remove(name) }, with update_config_drops_a_dropped_lgpm_repo pinning it. The comment there ("no caller sets these to None today, but [basecamp] had no such caller either right up until it did") is your latent-vs-live point taken.

Worth saying that #267 went further than the finding on the same theme, and for a reason worth flagging: several removals cannot be driven by the model at all, because the corresponding parsers are not total. A fully-default profile is dropped at parse, and an empty env_append list is skipped — so "absent from the model" does not mean "absent from the file", and a blanket remove there would delete user data rather than merely stale it. The removal branches are written as clear-what-scaffold-owns-and-keep-the-rest for that reason.

3. Silent overwrite of an unparseable / unreadable scaffold.tomlhalf fixed in #267

  • Unreadable (your second paragraph, the one that silently truncates a file that is on disk): fixed. save_project_config now matches on ErrorKind::NotFound for the legitimate from-scratch case and propagates every other read error with context, explicitly refusing to render fresh over a file whose comments it could not see.
  • Unparseable: still open. update_config is still existing.parse().unwrap_or_default() with no diagnostic. Your minimum ask — a one-line eprintln! naming the fallback — is not there, and neither is the scaffold.toml.bak that init's migration already writes. I would suggest raising this on fix(config): preserve scaffold.toml comments on every rewrite #267 directly; the argument that the user gets no signal that the thing this PR exists to prevent just happened to them stands.

4. DOGFOODING.mdfixed here

B1 gains the inspector round trip: --inspector, a no-flag re-run that must stay on it, the portable module_root check (LogosBasecamp, not …Dev), --no-inspector, and the rejected flag pair — each with a grep beside it as the evidence, since the attrs are written before the build and so are checkable even where the build is out of reach. B5 gains --print-output on both structs, with a note to verify against a failing build. The Minimum Rerun Guidance gains a row for the stack selector.

Your third bullet — that nothing asserts the comment-preservation contract — I have deliberately not actioned here, because the contract it names does not exist on this branch any more. It belongs on #267, and it is the sharpest thing in your review: a runbook check that a planted comment survives a setup and that a value reset to its default is actually gone would have caught 1, 2 and 3 between them. I would rather see it land there than see a scenario here describing behavior this PR does not have.

5. Nix/network work in a plain cargo testfixed here

Gated behind LOGOS_SCAFFOLD_E2E_NIX=1, and when it does run LOGOS_SCAFFOLD_CACHE_ROOT points at the test's own tempdir so the clone never reaches the developer's shared ~/.cache/logos-scaffold. The doc comment says why on both counts. You were right that "no-ops in CI" was the wrong bar — the machines that would have paid for this are the ones working on this code.


Also in this push

@fryorcraken's dogfooding found that the .scaffold/state/basecamp.state documentation was in the wrong file: docs/commands.md had it, but a consumer asking "where does setup leave the binary" goes to basecamp docs, which prints docs/basecamp-module-requirements.md — where it was absent. The contract now lives there, covering the three things that cannot be inferred from a path: read basecamp_bin rather than reconstructing it, it is a read-only out-link whose real ELF is a dot-prefixed sibling on portable builds, and it moves whenever the stack does (so --inspector invalidates a cached path). B1 reads the file before and after --inspector so that last claim is evidenced.

Also corrected the PR description's clippy count: 62, not 64. --all-targets reports 58 lib plus 62 lib-test of which 58 are duplicates. All pre-existing; none in a file this PR touches, which matches what you measured.

Findings 1 and 3-unparseable are the open ones, both on #267.


Generated by Claude Code

@danisharora099 danisharora099 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at f207de3.

The in-memory logic in apply_inspector_selection and align_lgpm_attr is careful and the unit tests around it are good. The problems are at the seams: where that logic meets the config writer, the build path, and the docs. Two of them undercut the PR's own goal, so I am holding on those.

Blocking

  1. --inspector is silently not persisted when the project has a per-platform attr map without an entry for this host. The config writer drops the scalar whenever a map exists. Inline comment on basecamp.rs:354.
  2. --print-output on build / build-portable does nothing. The module build path never reads the flag. Inline comment on basecamp.rs:141.

Should fix here, all about the same state file

  1. Docs and runbook say basecamp_bin moves when the stack moves. It does not; the out-link is keyed by pin only. Inline on DOGFOODING.md:1073 and basecamp-module-requirements.md:39.
  2. The --inspector then --no-inspector round trip replaces a hand-set portable attr with the default. Inline on basecamp.rs:376.
  3. Persisting before the build can leave config and basecamp.state on different stacks, and doctor cannot tell. Inline on basecamp.rs:248.

Minor

  • docs/basecamp-module-requirements.md:249: the LOGOS_DATA_DIR row lists three portable attrs. The inspector attr now qualifies too.
  • BasecampCommand::Setup changed from a unit variant to a struct variant on the public API with no version bump. Inline on api/mod.rs:648.
  • The E2E test could run in CI with a stub nix on PATH. Inline on tests/cli.rs:3405.
  • The last line of the new DOGFOODING block has no || true. Inline on DOGFOODING.md:1079.

The existing verification on this thread ran on a project with a plain scalar attr, so it does not exercise point 1, and nobody checked that --print-output actually streamed anything. Happy to re-review once 1 and 2 are in.

Comment thread src/commands/basecamp.rs Outdated
.attr_platform
.insert(system.to_string(), BASECAMP_ATTR_INSPECTOR.to_string());
} else {
basecamp_repo.attr = BASECAMP_ATTR_INSPECTOR.to_string();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: this write is lost whenever the project already has a per-platform map.

write_repo_ref (src/config.rs:1053) emits only the inline map when attr_platform is non-empty and drops the scalar. So on a project like:

[repos.basecamp]
attr = { aarch64-darwin = "bin-macos-app" }

a Linux developer running setup --inspector gets lgpm.attr = "cli-portable" on disk but never bin-bundle-dir-inspector. The next plain setup defaults the scalar back to app and builds a dev basecamp against portable lgpm, which is the exact empty-UI mismatch this flag exists to prevent. A later --no-inspector reports "nothing to undo".

The unit tests only cover a map that already contains the current host, so this path is untested.

Suggested fix: when the map is non-empty, always write this host's key into the map and never touch the scalar. That also turns the --no-inspector branch below into a plain remove(system) with no scalar-repair heuristic.

Comment thread src/commands/basecamp.rs
variants,
module,
print_output,
} => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: --print-output is a no-op on this path.

The only nix spawn reachable from cmd_basecamp_build is run_build_portable_nix (around line 2087), which calls cmd.output() and never consults print_output_enabled(). Output is captured and discarded on success, and only trimmed stderr surfaces on failure, exactly as before the flag existed. The help text is also wrong on the other half: build never writes a .scaffold/logs file.

basecamp_build_accepts_print_output_flag only checks --help, so nothing catches this.

Either route that spawn through run_logged the way setup and install do, or drop the flag from this PR.

Comment thread DOGFOODING.md Outdated
# since the attrs are written before the build starts.
"$SCAFFOLD_BIN" basecamp setup --inspector
grep -n 'attr' scaffold.toml
cat .scaffold/state/basecamp.state # basecamp_bin must have moved

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion is false on Linux, and a dogfooder following it will file a bug that is not one.

build_basecamp_app always writes the out-link to <cache>/basecamp/<pin>/app-result regardless of attr, and both the 0.2.x dev launcher and the portable bundle resolve to bin/LogosBasecamp. So basecamp_bin is byte-identical before and after --inspector.

The real hazard is the inverse of what the docs describe: the shared symlink gets re-pointed, so a consumer holding the old path silently execs the new build, and the displaced stack loses its GC root so the next toggle back rebuilds. Keying the out-link by attr (app-result-<attr>) would make this line true and let both stacks coexist.

Comment thread docs/basecamp-module-requirements.md Outdated

- **Read it; do not reconstruct it.** The path is under the cache root (`~/.cache/logos-scaffold/basecamp/<pin>/app-result/…` by default, and wherever `LOGOS_SCAFFOLD_CACHE_ROOT` or `[scaffold].cache_root` points otherwise), but *which file* under that output is the correct entry point differs by basecamp generation and by stack. On a dev `#app` build it is a `/bin/sh` launcher that exports `QT_PLUGIN_PATH` / `QML2_IMPORT_PATH` before exec'ing the real binary; on a portable build (`bin-bundle-dir`, `bin-bundle-dir-inspector`, `bin-appimage`) it is the unwrapped binary, because the bundle supplies those paths itself; on macOS `bin-macos-app` it is inside `LogosBasecamp.app/Contents/MacOS/`. Picking the wrong name starts an app that cannot find its Qt platform plugin or QML imports — a failure that looks like a basecamp bug, not a path bug. `setup` resolves this for you and writes the answer.
- **It is a nix out-link, so it is read-only**, and on a portable build the real ELF sits beside it as a dot-prefixed sibling (`.LogosBasecamp.elf`). A harness that needs a writable copy, or one that probes for a specific file layout, has to account for that — copy the tree out rather than expecting to modify it in place.
- **It moves when the stack moves.** `setup --inspector` rebuilds at a different flake attr, so `basecamp_bin` points somewhere else afterwards. Re-read the file after any `setup`; caching the path across a stack change silently execs the previous build.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the DOGFOODING comment: the path does not change between stacks today, because the out-link is keyed by pin only and both stacks resolve to bin/LogosBasecamp. Either key the out-link by attr so this becomes true, or reword this bullet to say the symlink is re-pointed in place.

Comment thread src/commands/basecamp.rs Outdated
// nothing at all (the map was the only source for this host).
basecamp_repo.attr_platform.remove(system);
let resolved = basecamp_repo.effective_attr(system);
if resolved.is_empty() || resolved == BASECAMP_ATTR_INSPECTOR {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The round trip loses a hand-set portable attr, silently.

Start from attr = "bin-appimage", lgpm.attr = "cli-portable". --inspector overwrites the scalar. --no-inspector then lands here, sets app, and align_lgpm_attr drags lgpm to cli. The user is now on the dev stack with no note, because the "left alone" note above only fires when the effective attr was not the inspector. A per-host map entry gets the same treatment: deleted rather than restored. inspector_flags_touch_only_the_current_host_entry asserts this outcome as correct, and docs/commands.md says hand-set values survive.

Restoring the previous value would mean remembering it somewhere, so I am not asking for that here. But the docs should not claim the opposite, and a note when --no-inspector lands on a different stack than the user started from would help.

Comment thread src/commands/basecamp.rs
// hand-set attr has nothing to persist, and rewriting `scaffold.toml` to
// say exactly what it already said is churn the user did not ask for. A
// plain `setup` still writes once, at the end.
if stack_changed {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment above says nothing downstream depends on the build having finished. That is not quite right: launch, paths, and doctor derive the XDG subpath and variant key from config, but take the binary from basecamp.state, which does not record the attr.

After an interrupted setup --inspector, config says portable while the binary on disk is still the dev launcher. launch seeds portable profiles and execs the dev app, and doctor reports PASS because the path exists.

Recording the built attr in basecamp.state and having doctor compare it to effective_attr would close this, and also answers the "which build is this?" question from the basecamp_bin comments.

Smaller: on a toggle this early save plus the unconditional save at the end write scaffold.toml twice with identical bytes. Guarding the trailing save with !stack_changed would keep it to one.

Comment thread docs/commands.md Outdated
- `run` combines build (which chains `setup`), IDL build, localnet start, wallet topup, and deploy into a single command — the inner loop for day-to-day development. It works with no configuration. If a `[run]` section with `post_deploy` is present in `scaffold.toml`, each hook is executed after deploy via `sh -c` (cwd = project root) with `SEQUENCER_URL`, `NSSA_WALLET_HOME_DIR`, `LEE_WALLET_HOME_DIR`, `SCAFFOLD_PROJECT_ROOT`, `SCAFFOLD_IDL_DIR`, `SCAFFOLD_TOPUP_SKIPPED`, and `SCAFFOLD_DEPLOY_SKIPPED` env vars; when the project has exactly one deployable program, `SCAFFOLD_PROGRAM_ID` and `SCAFFOLD_GUEST_BIN` are also set. If a localnet is already running it is reused; otherwise it is started, and deploy is skipped when the guest binaries + IDL + config and the sequencer instance are unchanged. `--profile NAME` selects a named pipeline from `[run.profiles.<name>]`; `--reset` wipes sequencer state + wallet and re-seeds before the run (`--no-reset` overrides a config-set default); `--post-deploy <cmd>` (repeatable) overrides the configured hooks and `--no-post-deploy` skips them entirely; `--watch` re-runs the pipeline on file changes. `run` covers the deploy loop only — it does not run `wallet -- check-health` or any `basecamp` command.
- `spel -- ...` forwards raw spel CLI arguments to the project-vendored `spel` binary so any spel subcommand (`inspect`, `pda`, `generate-idl`, …) runs against the project's pinned version without a global install.
- `basecamp setup` pins basecamp + `lgpm` (read from `[repos.basecamp]` / `[repos.lgpm]` — both `build = "nix-flake"`), builds both (logged to `.scaffold/logs/<timestamp>-setup-*.log`), and seeds per-profile XDG directories for `alice` and `bob` under `.scaffold/basecamp/profiles/`. The two pins move as a set: scaffold's default `lgpm` rev is the one the pinned basecamp release locks, because that same package-manager library is what the app uses to read the modules `lgpm` installed. Existing projects keep whatever they pinned in `scaffold.toml` — bumping means editing both pins and re-running `setup`. Runtime config (`port_base`, `port_stride`) is in `[basecamp]`.
- `basecamp setup` pins basecamp + `lgpm` (read from `[repos.basecamp]` / `[repos.lgpm]` — both `build = "nix-flake"`), builds both (logged to `.scaffold/logs/<timestamp>-setup-*.log`), and seeds per-profile XDG directories for `alice` and `bob` under `.scaffold/basecamp/profiles/`. The two pins move as a set: scaffold's default `lgpm` rev is the one the pinned basecamp release locks, because that same package-manager library is what the app uses to read the modules `lgpm` installed. Existing projects keep whatever they pinned in `scaffold.toml` — bumping means editing both pins and re-running `setup`. Runtime config (`port_base`, `port_stride`) is in `[basecamp]`. Setup also records what it built as `key=value` lines in `.scaffold/state/basecamp.state` — `pin`, `basecamp_bin`, `lgpm_bin` — which is the supported way to find the binary from a script. Use `basecamp_bin` verbatim rather than reconstructing a path: which file under the nix output is the right entry point differs by basecamp generation and by stack — on a dev `#app` build it is a `/bin/sh` launcher that exports `QT_PLUGIN_PATH` / `QML2_IMPORT_PATH`, and picking the raw binary beside it yields an app that cannot find its Qt platform plugin or QML imports. `setup` resolves that for you. `lgs doctor` prints the same path for humans. [basecamp-module-requirements.md](./basecamp-module-requirements.md#what-setup-leaves-behind--scaffoldstatebasecampstate) — which `basecamp docs` prints offline — carries the full contract for script consumers, including the read-only out-link layout and the fact that the path moves when the stack does.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two corrections in this paragraph:

  • lgs doctor does not print the binary path. Only lgs basecamp doctor reads basecamp.state.
  • The --inspector bullet says the flag persists "overriding a per-platform attr map, since the inspector output is built for every supported system", and a few sentences later says both flags "edit only the current host's entry". The code does the second. The first reads as if the whole map is replaced.

Comment thread src/api/mod.rs
pub enum BasecampCommand {
/// Build/install the pinned basecamp binary and seed profiles.
Setup,
Setup {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes BasecampCommand::Setup from a unit variant to a struct variant on the public API, so any embedder writing BasecampCommand::Setup stops compiling. Fine if intended, but it deserves a version bump or at least a note, since nothing in tests/ exercises the API surface and CI will not notice.

Comment thread tests/cli.rs
/// developer's shared `~/.cache/logos-scaffold` — B1's "do not pollute the
/// user's home" rule applies to the test suite too.
#[test]
fn basecamp_setup_inspector_persists_both_attrs_before_the_build() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could run unconditionally in CI. ensure_nix_present is only a which("nix"), and the file already stubs binaries on PATH via write_wallet_stub. A stub nix plus a [repos.basecamp] source that fails to clone immediately would prove the write ordering in milliseconds, and the two contains assertions stay as they are. Then the opt-in gate and the nix caveat in the PR description both go away.

Comment thread DOGFOODING.md Outdated
"$SCAFFOLD_BIN" basecamp paths alice | grep -i module_root
"$SCAFFOLD_BIN" basecamp setup --no-inspector
grep -n 'attr' scaffold.toml
"$SCAFFOLD_BIN" basecamp setup --inspector --no-inspector # must be rejected

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is meant to exit non-zero, but unlike the other expected-failure lines in this scenario it has no || true. Under a set -e capture script the block aborts here. Same for the setup --inspector line above on a host where the clone fails: the comment says to run the greps regardless, but the greps come after a command that may exit non-zero.

…ized

Review of #266 found two defects that made the feature silently not work,
plus a docs claim this branch introduced that was false.

`--inspector` wrote the scalar `attr` whenever the per-platform map lacked
the current host. But `write_repo_ref` emits the inline map *instead of* the
scalar whenever the map is non-empty, so on a project carrying only a
colleague's `aarch64-darwin` entry the choice reached disk as nothing at
all: `[repos.lgpm].attr` moved to `cli-portable`, the next plain `setup`
defaulted the scalar back to `app`, and the result was a dev basecamp built
against portable lgpm — precisely the empty-UI mismatch the flag exists to
prevent. A later `--no-inspector` then reported "nothing to undo". The
decision now keys on whether the project uses a map at all rather than on
whether that map happens to mention this host, which also reduces the undo
path to a plain `remove(system)` and drops the scalar-repair heuristic.

`--print-output` was a no-op on `build` / `build-portable`:
`run_build_portable_nix` called `cmd.output()` and never consulted
`print_output_enabled()`, so nix's log was captured and discarded exactly as
before the flag existed. Only stderr is now forwarded when the flag is set —
stdout carries the `--print-out-paths` store paths this function parses, so
routing the whole spawn through `run_logged` (which redirects both) would
break the build instead of fixing the flag. The help text claimed a
`.scaffold/logs` file that `build` never writes; corrected on both verbs.

The `basecamp_bin` docs asserted the path moves when the stack moves, which
was false: the out-link was keyed by pin alone, so both stacks resolved to
the same `app-result/bin/LogosBasecamp`. Rather than only rewording, the
out-link is now keyed by attr (`app-result-<attr>`), which makes the claim
true and is the better behaviour anyway — a shared link is re-pointed by
every toggle, so a consumer holding the old path silently execs the new
build and the displaced stack loses its GC root, forcing a rebuild on the
way back. Both stacks now coexist under one pin.

Also: `--no-inspector` restores the default stack rather than the attr
configured beforehand, which scaffold does not remember; it now says so
instead of moving the user's stack in silence, and `docs/commands.md` no
longer claims hand-set values survive. The trailing `save_project_config` is
skipped on a toggle, since the pre-build save already wrote those bytes.

Tests: the nix-gated e2e test now runs unconditionally in CI. A stub `nix`
satisfies `ensure_nix_present` (only a `which`) and an unresolvable source
fails the clone in milliseconds, which is well inside the pre-build window
under test — no network, no nix store, 0.01s. Added a sibling covering the
map-without-current-host case, verified to fail against the previous code
with the reported symptom, and a behavioral `--print-output` test that
distinguishes streamed output from the failure-path echo, so deleting the
implementation fails it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fryorcraken

Copy link
Copy Markdown
Collaborator Author

Thanks — this was a genuinely good catch list. Both blocking findings were real, and one of them meant the feature silently did nothing on a common project shape. Pushed as 6b71700.

Blocking 1 — the attr write is lost when a per-platform map exists — fixed

Confirmed exactly as described. write_repo_ref (config.rs:1053) emits the inline map instead of the scalar whenever attr_platform is non-empty, so on a project whose map lists only a colleague's platform, --inspector persisted nothing while still moving [repos.lgpm].attr to cli-portable. The next plain setup defaulted the scalar back to app and built a dev basecamp against portable lgpm — the exact mismatch the flag exists to prevent.

Took your suggested fix. The decision now keys on whether the project uses a map at all, not on whether that map happens to mention this host:

  • --inspector: map non-empty → write this host's key; otherwise → write the scalar.
  • --no-inspector: the mirror. With a map it is a plain remove(system), and the scalar-repair heuristic is gone, as you predicted.

One wrinkle worth flagging: --no-inspector on a map can uncover an empty scalar, which would build a bare nix build .#. setup defaults the scalar before calling in, so no real run reaches it, but the undo now lands on a buildable attr on its own rather than relying on its caller. Your review actually surfaced this — an existing unit test constructed that shape and started failing.

Tests: inspector_flag_writes_into_a_map_that_lacks_this_host covers both directions at the unit level, and basecamp_setup_inspector_persists_into_a_platform_map_lacking_this_host covers it end to end through the binary, since the defect lives in the seam between the selector and the serializer where neither side's unit tests can see it. Verified it fails against the previous code with the reported symptom.

src/config.rs is untouched — the fix sits entirely at the call site, so there is no overlap with #267.

Blocking 2 — --print-output is a no-op on build / build-portablefixed

Confirmed. run_build_portable_nix called cmd.output() and never consulted print_output_enabled().

Chose to fix rather than drop the flag, but not by routing through run_logged — that redirects both streams, and this function parses store paths from --print-out-paths on stdout, so handing it the whole spawn would leave store_paths empty and fail every build with "returned no output paths". Only stderr is forwarded, which is where nix writes its build log. That gets the flag's actual purpose (a failed CI build explaining itself) without breaking the command.

Help text corrected on both verbs — you were right that build never writes a .scaffold/logs file.

Test: basecamp_build_print_output_actually_streams_nix_output asserts both directions. It has to distinguish the marker arriving on its own line (streamed) from the same marker quoted inside scaffold's nix build … failed (…) summary, which echoes captured stderr by design — a naive contains would pass with the flag deleted. Verified that stubbing the implementation out fails it.

The docs regression I introduced — fixed, and you were right

Plainly: I asserted basecamp_bin moves between stacks without checking, and it did not. build_basecamp_app wrote <cache>/basecamp/<pin>/app-result regardless of attr, and both the 0.2.x dev launcher and the portable bundle resolve to bin/LogosBasecamp, so the path was byte-identical before and after --inspector. A dogfooder following that line would have filed a bug that wasn't one.

Went with keying the out-link by attr (app-result-<attr>) rather than only rewording. Blast radius is one source line and one docs path example — nothing else in src/, tests/, or the docs referenced app-result. It makes the claim true and is the better behaviour regardless, for the reason you gave: a shared link is re-pointed by every toggle, so a consumer holding the old path silently execs the new build and the displaced stack loses its GC root, forcing a rebuild on the way back. Both stacks now coexist under one pin. Both docs updated to describe this accurately.

basecamp.rs:376 — the lossy --no-inspector round trip — partially actioned

Verified. Taking you at your word that restoring the previous value is out of scope, so:

  • docs/commands.md no longer claims hand-set values survive. It now says --no-inspector restores the default stack, not what was configured beforehand, and that scaffold does not remember a displaced value.
  • --no-inspector now prints a note whenever it lands on something other than the default, so the move is not silent.
  • inspector_flags_touch_only_the_current_host_entry reassessed. It still asserts the same outcome, because that is what the code does — but the comment now states plainly that the round trip is lossy by construction and that this is a known limitation rather than the desired end state, instead of implying the outcome is correct.

basecamp.rs:248 — attr not recorded in basecamp.statedeferred, with the smaller half actioned

The scenario is real: after an interrupted setup --inspector, config says portable while the binary on disk is the dev launcher, and doctor passes because the path exists. My "nothing downstream depends on the build having finished" comment was too broad.

Deferring the basecamp.state attr field deliberately. It changes a documented consumer contract that this PR just finished writing down, and doctor gaining a config-vs-built comparison is a new check with its own semantics. Both belong in a follow-up that can treat the state-file schema as its subject rather than smuggling it in here. Worth noting the attr-keyed out-link above narrows the window considerably: the recorded basecamp_bin now contains the attr it was built at, so the two stacks no longer share a path, and a stale record points at the older build rather than being silently overwritten by the newer one.

The smaller point is fixed: the trailing save_project_config is now guarded with !stack_changed, so a toggle writes scaffold.toml once.

docs/commands.md:180 — two factual corrections — both fixed

Both confirmed. lgs doctor does not read basecamp.state; only lgs basecamp doctor does (it prints found <path>), so the text now says that. And the "overriding a per-platform attr map" phrasing contradicted the later sentence — the code edits only the current host's entry. Rewritten to describe where the write actually lands and why (the map is what gets serialized), which also documents the Blocking 1 fix.

src/api/mod.rs:648 — breaking public-API change — maintainer call, not actioned

Confirmed breaking: BasecampCommand is a public enum without #[non_exhaustive], so BasecampCommand::Setup stops compiling for embedders, and nothing in tests/ exercises the API surface.

Not bumping the version here, because I don't think it's mine to decide. CONTRIBUTING.md prescribes no versioning policy, there's no CHANGELOG, and the history shows version bumps landing as standalone chore: commits rather than inside feature PRs (fbde925, 6b5f125). The crate is published, so this is a real semver event on a 0.x line. Flagging for a maintainer: either bump to 0.4.0 before release, or add #[non_exhaustive] to the enum so future variants stop being breaking. Happy to do either in this PR if you say which.

tests/cli.rs:3405 — run it unconditionally in CI — worked, gate removed

Your suggestion was right and it was straightforward. ensure_nix_present is only a which("nix"), so a stub nix on PATH satisfies it, and an unresolvable [repos.basecamp] source fails the clone immediately — well inside the pre-build window the test is actually about. Both contains assertions unchanged.

The LOGOS_SCAFFOLD_E2E_NIX=1 gate is gone, the test runs in CI in 0.01s with no network and no nix store, and I've added the map-variant sibling on the same harness. I'll drop the nix caveat from the PR description.

DOGFOODING.md:1079 — missing || truefixed

Confirmed. Added || true to the --inspector --no-inspector line and to the three setup invocations above it, so a set -e capture script reaches the greps that are the actual evidence — which was the stated intent of the block.


cargo fmt --check, cargo test (796 passing), and cargo clippy --all-targets all clean locally; clippy is still at the 62-warning baseline with none added.

`BasecampCommand::Setup` became a struct variant in this PR so it could
carry the `--inspector` tri-state. `BasecampCommand` is public on a
published crate with no `#[non_exhaustive]`, so an embedder writing
`BasecampCommand::Setup` stops compiling — and nothing in `tests/`
exercises the API surface, so CI does not catch it. Raised in review; it
was left implicit until now.

Recorded in ADR.md, which is where this repo already documents
consumer-facing surfaces and their migrations (see "Hook Env Contract is a
Documented Public Surface" and the 0.2.0 schema-migration entries). There
is no CHANGELOG and no dedicated API doc, and CONTRIBUTING.md says to add
to ADR.md rather than rewrite it, so this follows the existing convention
instead of introducing a new file. The entry gives the one-line migration:
`BasecampCommand::Setup { inspector: None }` is the previous behaviour.

Also noted in the rustdoc on the variant itself, so it is visible from the
generated API docs without finding this PR.

No version bump here: this repo bumps in standalone `chore:` commits, so
one is expected to follow separately. `#[non_exhaustive]` deliberately not
added — it would itself be a breaking change and is a separate decision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fryorcraken

Copy link
Copy Markdown
Collaborator Author

@danisharora099 — resolution on the public-API point you raised, pushed as 27ffa8b.

The breaking change is now documented rather than implicit, in three places:

  • ADR.md — a new entry with the one-line migration. That is where this repo already records consumer-facing surfaces and their migrations ("Hook Env Contract is a Documented Public Surface", the 0.2.0 schema-migration entries), and CONTRIBUTING.md says to add to it rather than rewrite it. There is no CHANGELOG and no dedicated API doc, so this follows the existing convention instead of introducing a new file.
  • Rustdoc on the variant itself, so it shows up in the generated docs without anyone having to find this PR.
  • The PR description.

The migration, for the record:

// before
project.basecamp(BasecampCommand::Setup)?;

// after — `None` is the previous behaviour
project.basecamp(BasecampCommand::Setup { inspector: None })?;

No version bump in this PR, deliberately. The repo's convention is standalone chore: commits for bumps (fbde925, 6b5f125) rather than folding them into feature PRs, so one is expected to follow separately before release. Flagging that as the remaining action rather than something silently dropped.

#[non_exhaustive] also deliberately not added — it would itself be a breaking change, and it is a separate design decision that shouldn't ride along here.

Docs-only commit; CI green on all four checks.

@danisharora099 danisharora099 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 27ffa8b. Both blockers are fixed and covered by tests that fail against the previous code, and the out-link change makes the docs true rather than just rewording them. Thanks for taking the E2E test suggestion further than I asked, running it in CI unconditionally is the right outcome.

Verified locally: cargo fmt --check clean, lib tests green, the three new CLI tests pass in about a second. One CLI test failed on my first full run (deploy_program_path_json_includes_program_id, a ureq panic) but passes on rerun and on master, so that is a pre-existing flake unrelated to this PR.

Two non-blocking notes inline. Deferring the basecamp.state attr field to a follow-up is fine by me. The version bump is a maintainer call, and the ADR entry gives them what they need to make it.

Comment thread src/commands/basecamp.rs
// different stack than the user started from is exactly the sort
// of quiet change this feature exists to surface.
let restored = basecamp_repo.effective_attr(system);
if restored != BASECAMP_ATTR {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking. This condition fires in the opposite case from the one it was added for. The lossy round trip lands on the default after displacing a hand-set value, and there restored == BASECAMP_ATTR, so the note stays silent. In the scalar branch above attr is always set to the default, so the note never fires there. In the map branch it fires only when the scalar is non-default, which cannot be serialized alongside a map. In practice it is close to unreachable.

Printing the note unconditionally on a successful undo would be simpler and would actually cover the case it describes. Fine as a follow-up.

Comment thread src/commands/basecamp.rs
// would rebuild from scratch. Per-attr links let both stacks coexist under
// one pin and make `basecamp_bin` genuinely move when the stack does,
// which is what `basecamp.state`'s consumers are told to rely on.
let link = out_dir.join(format!("app-result-{attr}"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking. lgpm-result in build_lgpm just below is still one shared link across cli and cli-portable, so it has the same re-pointing behaviour this change fixes for basecamp: a toggle replaces the other stack's lgpm and drops its GC root. Much smaller cost since lgpm builds in seconds, but keying it the same way (lgpm-result-<attr>) would keep the two consistent and make lgpm_bin in basecamp.state distinct per stack too. One-liner for a follow-up.

The env table's `LOGOS_DATA_DIR` row enumerated the portable
`[repos.basecamp].attr` values as `bin-macos-app`, `bin-appimage`,
`bin-bundle-dir` — the set as it stood before this branch added the
inspector attr. `BASECAMP_PORTABLE_ATTRS` now carries a fourth entry
(`BASECAMP_ATTR_INSPECTOR` = `bin-bundle-dir-inspector`), and the
`LOGOS_DATA_DIR` export is gated on `is_portable_basecamp`, which reads
exactly that constant. So on a macOS host after `setup --inspector`,
`launch` does export `LOGOS_DATA_DIR` and the table said it would not.

The same file's prose at line 37 already listed
`bin-bundle-dir-inspector` among the portable builds, so the doc
contradicted itself. This is also the text `basecamp docs` prints from
its embedded copy, so the stale list shipped offline to consumers.

Docs-only; the code was already correct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fryorcraken

Copy link
Copy Markdown
Collaborator Author

One finding from @danisharora099's review body was missed in my earlier responses — flagging it rather than leaving it silently dropped.

docs/basecamp-module-requirements.md:249 — the LOGOS_DATA_DIR row of the env table still listed the portable [repos.basecamp].attr values as bin-macos-app, bin-appimage, bin-bundle-dir. This branch added a fourth entry to BASECAMP_PORTABLE_ATTRS (BASECAMP_ATTR_INSPECTOR = bin-bundle-dir-inspector, src/constants.rs:212-217), and the LOGOS_DATA_DIR export is gated on is_portable_basecamp, which reads that constant — so on macOS after setup --inspector the variable is exported, contrary to the table. The prose at line 37 had already been updated, leaving the file inconsistent with itself, and since basecamp docs prints an embedded copy the stale list shipped offline too.

Fixed in 231889c (docs-only, one line; the code was already correct).

@fryorcraken

Copy link
Copy Markdown
Collaborator Author

Further dogfooding on this, and I've hit something the review discussion hasn't covered: --inspector breaks basecamp install and basecamp launch for as long as it is set. Not subtly — it is a hard error.

I don't think this is a bug in the implementation. I think it's the flag's shape.

What happened

Working in a module repo, I ran setup --inspector to get a bundle for a sitometres spec. Later in the same session I wanted to look at the module in a normal Basecamp window:

$ lgs basecamp install
...
error: the `.lgx` has no variant for this stack — a dev basecamp loads
`<host>-dev` variants and a portable one loads bare `<host>` variants.
Rebuild with the matching flake output (`#lgx` for dev, `#lgx-portable`
for portable) or align `[repos.basecamp].attr`.

Correct behaviour, given the state. --inspector had moved the profiles to the portable stack, so the dev #lgx artefacts no longer match. The fix is setup --no-inspector, then install, then launch — and to go back to running specs, setup --inspector again.

So the two things a module developer does most often — run the app, run the e2e specs — are now mutually exclusive modes, and switching between them means re-running setup each time. setup also reseeds the alice/bob profiles on the way through, which is its own small cost.

Why I think it's the shape, not the implementation

The PR's strongest argument is that [repos.basecamp].attr and [repos.lgpm].attr must move together, because a half-switched stack fails silently — Basecamp declines every module with one warning line and opens empty. That's right, and the lockstep is a genuine improvement.

But it's also the tell. If selecting this build has to drag lgpm's attr with it, and changes the XDG subpath the profiles are seeded under, and invalidates the recorded basecamp_bin, then --inspector isn't selecting a build. It's switching the whole stack into a different mode, and every one of those consequences follows from that.

A flag on setup reads like "same command, one option different". This is closer to "a different setup". The current design has already needed:

  • lgpm's attr moved in lockstep (this PR)
  • the out-link keyed by attr (app-result-<attr>) so the two stacks stop sharing a path (from the review)
  • a persisted tri-state so a later plain setup doesn't silently drop the choice
  • docs warning that --inspector invalidates a cached basecamp_bin

Each fix is right, and each one is scaffold absorbing another consequence of two modes wearing one command's clothes.

What I'd suggest

@fryorcraken's framing, and I agree with it: make it a separate verb rather than a flag —

$ lgs basecamp setup             # the dev stack: install, launch, doctor
$ lgs basecamp setup-inspector   # the portable+inspector stack: specs

That would change the shape of the problem in three ways:

  1. The mode is visible at the call site. A CI workflow or a README line says which stack it wants. Today setup means "whatever this project was last toggled to", which is why the tri-state had to exist at all.
  2. The persistence question mostly dissolves. Much of the review — the platform-map write, the lossy --no-inspector round trip, an interrupted setup leaving config and disk disagreeing — is about correctly remembering a mode across invocations. A verb that states its mode each time has far less to remember.
  3. install / launch / doctor can say something useful. Rather than the variant-mismatch error above, they could say "this project is set up for the inspector stack; run lgs basecamp setup for a dev stack" — which is the sentence I actually needed.

If the two stacks could coexist on disk (the attr-keyed out-link from this PR is most of the way there), the verbs could even be idempotent enough to alternate without a full re-setup. That's a bigger change and I'm not asking for it here.

Not asking to block

This PR delivered what it set out to: the last raw nix build for a Basecamp is gone from my repo's CI, and it works. I'd rather see it merge than hold it for a rename.

But I'd flag setup-inspector-as-a-verb as worth deciding before the flag ships in a release and the name is load-bearing. Renaming a flag after consumers pin to it is the expensive version of this conversation.

One smaller thing that would help regardless of the outcome: install and launch could name the fix in the error. They already know both the configured attr and the artefact's variant, so "run lgs basecamp setup to switch to the dev stack" is derivable, and it turns a two-command detour into a one-line one.

fryorcraken added a commit to fryorcraken/logos-radicle-module that referenced this pull request Sep 9, 2026
The end-to-end layer built `#bin-bundle-dir-inspector` through `lgs basecamp
setup --inspector`, on the belief that it was the only Basecamp output with
the QML inspector compiled in. That belief was wrong, and it is what kept both
workflows pinned to a commit on the unmerged logos-co/scaffold#266.

Reading logos-basecamp's flake.nix at the pinned rev: `appDistributed` (which
feeds #bin-bundle-dir, appimage and macos) passes `enableInspector = false`,
while the dev `app` inherits logosQtMcp and sets no such flag. The inspector is
off in the SHIPPING outputs — which is what the flake's own comment says — not
off in the dev build. Upstream's inspector-driven integration-test and
shutdown-test checks both use `appPkg = app`.

Verified with the same probe sitometres uses, searching for the literal
"[QmlInspector] Inspector server listening on port":

  #app                       bin/.LogosBasecamp      present
  #bin-bundle-dir-inspector  bin/.LogosBasecamp.elf  present
  #bin-bundle-dir (shipping) bin/.LogosBasecamp.elf  ABSENT

The shipping row is the control: it proves the probe discriminates rather than
matching whatever it is pointed at. ui-tests.yml now asserts the #app row on
every run, so a future Basecamp flipping enableInspector off for the dev build
fails loudly instead of silently removing what this layer depends on.

Switching to the dev app removes three things rather than trading them:

  * `basecamp setup`, which also built lgpm and seeded alice/bob profiles the
    job never used, and which rewrites scaffold.toml stripping every comment —
    the reason the pin had to be read before it ran. That ordering constraint
    is gone. Running specs no longer conflicts with `lgs basecamp launch`
    either, since neither `attr` key is touched.
  * `--variant linux-amd64`. sitometres' hostVariant() defaults to
    linux-amd64-dev, exactly what `.#lgx` produces; the override existed only
    because the bundle demands portable variants.
  * the dev-vs-portable classification problem that is the whole subject of
    scaffold#265 — it only ever arose from choosing the bundle.

With `--inspector` unneeded, both workflows return to released scaffold. The
other flag they pinned for, `--print-output` on `basecamp build`, is sugar for
LOGOS_SCAFFOLD_PRINT_OUTPUT=1, and print_output_enabled() is read inside
run_logged on every build path regardless of subcommand — so a job-level env
var is the whole feature on the release.

sitometres returns to a published version too. Its git pin existed for the
probe bug (paradoxcomputer/sitometres#1), which only ever affected the bundle:
the released probe looks for bin/.LogosBasecamp, and the dev app ships exactly
that. browse.yaml was run green on 0.1.0 and 0.1.2 against the dev app before
making the switch; 0.1.2 is what both the workflow and run-local-e2e.sh now
pin.

That pin was not free. #266 moved three times while this repo tracked it, and
one move reverted a feature already documented here as fixed
(save_project_config's comment preservation), silently reintroducing the
scaffold.toml comment stripping. Both retracted upstream asks would have been
avoided by reading the dependency's source first, which CLAUDE.md now says.

Verified before committing: browse.yaml 18/18 green against the dev app on
both sitometres versions; run-local-e2e.sh 38/39, the one failure being the
step local.yaml documents as seeded-profile-specific and expected to fail
against a developer's own node.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fryorcraken

Copy link
Copy Markdown
Collaborator Author

This is actually likely to be unnecessary, will close once confirmed.

fryorcraken added a commit to fryorcraken/logos-radicle-module that referenced this pull request Sep 9, 2026
The end-to-end layer built `#bin-bundle-dir-inspector` through `lgs basecamp
setup --inspector`, on the belief that it was the only Basecamp output with
the QML inspector compiled in. That belief was wrong, and it is what kept both
workflows pinned to a commit on the unmerged logos-co/scaffold#266.

Reading logos-basecamp's flake.nix at the pinned rev: `appDistributed` (which
feeds #bin-bundle-dir, appimage and macos) passes `enableInspector = false`,
while the dev `app` inherits logosQtMcp and sets no such flag. The inspector is
off in the SHIPPING outputs — which is what the flake's own comment says — not
off in the dev build. Upstream's inspector-driven integration-test and
shutdown-test checks both use `appPkg = app`.

Verified with the same probe sitometres uses, searching for the literal
"[QmlInspector] Inspector server listening on port":

  #app                       bin/.LogosBasecamp      present
  #bin-bundle-dir-inspector  bin/.LogosBasecamp.elf  present
  #bin-bundle-dir (shipping) bin/.LogosBasecamp.elf  ABSENT

The shipping row is the control: it proves the probe discriminates rather than
matching whatever it is pointed at. ui-tests.yml now asserts the #app row on
every run, so a future Basecamp flipping enableInspector off for the dev build
fails loudly instead of silently removing what this layer depends on.

Switching to the dev app removes three things rather than trading them:

  * `basecamp setup`, which also built lgpm and seeded alice/bob profiles the
    job never used, and which rewrites scaffold.toml stripping every comment —
    the reason the pin had to be read before it ran. That ordering constraint
    is gone. Running specs no longer conflicts with `lgs basecamp launch`
    either, since neither `attr` key is touched.
  * `--variant linux-amd64`. sitometres' hostVariant() defaults to
    linux-amd64-dev, exactly what `.#lgx` produces; the override existed only
    because the bundle demands portable variants.
  * the dev-vs-portable classification problem that is the whole subject of
    scaffold#265 — it only ever arose from choosing the bundle.

With `--inspector` unneeded, both workflows return to released scaffold. The
other flag they pinned for, `--print-output` on `basecamp build`, is sugar for
LOGOS_SCAFFOLD_PRINT_OUTPUT=1, and print_output_enabled() is read inside
run_logged on every build path regardless of subcommand — so a job-level env
var is the whole feature on the release.

sitometres returns to a published version too. Its git pin existed for the
probe bug (paradoxcomputer/sitometres#1), which only ever affected the bundle:
the released probe looks for bin/.LogosBasecamp, and the dev app ships exactly
that. browse.yaml was run green on 0.1.0 and 0.1.2 against the dev app before
making the switch; 0.1.2 is what both the workflow and run-local-e2e.sh now
pin.

That pin was not free. #266 moved three times while this repo tracked it, and
one move reverted a feature already documented here as fixed
(save_project_config's comment preservation), silently reintroducing the
scaffold.toml comment stripping. Both retracted upstream asks would have been
avoided by reading the dependency's source first, which CLAUDE.md now says.

Verified before committing: browse.yaml 18/18 green against the dev app on
both sitometres versions; run-local-e2e.sh 38/39, the one failure being the
step local.yaml documents as seeded-profile-specific and expected to fail
against a developer's own node.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fryorcraken added a commit to fryorcraken/logos-radicle-module that referenced this pull request Sep 9, 2026
* Run the e2e layer on the dev #app, and CI on released scaffold

The end-to-end layer built `#bin-bundle-dir-inspector` through `lgs basecamp
setup --inspector`, on the belief that it was the only Basecamp output with
the QML inspector compiled in. That belief was wrong, and it is what kept both
workflows pinned to a commit on the unmerged logos-co/scaffold#266.

Reading logos-basecamp's flake.nix at the pinned rev: `appDistributed` (which
feeds #bin-bundle-dir, appimage and macos) passes `enableInspector = false`,
while the dev `app` inherits logosQtMcp and sets no such flag. The inspector is
off in the SHIPPING outputs — which is what the flake's own comment says — not
off in the dev build. Upstream's inspector-driven integration-test and
shutdown-test checks both use `appPkg = app`.

Verified with the same probe sitometres uses, searching for the literal
"[QmlInspector] Inspector server listening on port":

  #app                       bin/.LogosBasecamp      present
  #bin-bundle-dir-inspector  bin/.LogosBasecamp.elf  present
  #bin-bundle-dir (shipping) bin/.LogosBasecamp.elf  ABSENT

The shipping row is the control: it proves the probe discriminates rather than
matching whatever it is pointed at. ui-tests.yml now asserts the #app row on
every run, so a future Basecamp flipping enableInspector off for the dev build
fails loudly instead of silently removing what this layer depends on.

Switching to the dev app removes three things rather than trading them:

  * `basecamp setup`, which also built lgpm and seeded alice/bob profiles the
    job never used, and which rewrites scaffold.toml stripping every comment —
    the reason the pin had to be read before it ran. That ordering constraint
    is gone. Running specs no longer conflicts with `lgs basecamp launch`
    either, since neither `attr` key is touched.
  * `--variant linux-amd64`. sitometres' hostVariant() defaults to
    linux-amd64-dev, exactly what `.#lgx` produces; the override existed only
    because the bundle demands portable variants.
  * the dev-vs-portable classification problem that is the whole subject of
    scaffold#265 — it only ever arose from choosing the bundle.

With `--inspector` unneeded, both workflows return to released scaffold. The
other flag they pinned for, `--print-output` on `basecamp build`, is sugar for
LOGOS_SCAFFOLD_PRINT_OUTPUT=1, and print_output_enabled() is read inside
run_logged on every build path regardless of subcommand — so a job-level env
var is the whole feature on the release.

sitometres returns to a published version too. Its git pin existed for the
probe bug (paradoxcomputer/sitometres#1), which only ever affected the bundle:
the released probe looks for bin/.LogosBasecamp, and the dev app ships exactly
that. browse.yaml was run green on 0.1.0 and 0.1.2 against the dev app before
making the switch; 0.1.2 is what both the workflow and run-local-e2e.sh now
pin.

That pin was not free. #266 moved three times while this repo tracked it, and
one move reverted a feature already documented here as fixed
(save_project_config's comment preservation), silently reintroducing the
scaffold.toml comment stripping. Both retracted upstream asks would have been
avoided by reading the dependency's source first, which CLAUDE.md now says.

Verified before committing: browse.yaml 18/18 green against the dev app on
both sitometres versions; run-local-e2e.sh 38/39, the one failure being the
step local.yaml documents as seeded-profile-specific and expected to fail
against a developer's own node.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Fix run-write-e2e.sh, and share its setup with run-local-e2e.sh

Review found that `run-write-e2e.sh` was left on the old world by the previous
commit, which is the exact failure `run-local-e2e.sh`'s own comments warn
about, reproduced in the file next door. It still pinned the sitometres fork
commit, read `.scaffold/state/basecamp.state`, pointed at
`.scaffold/basecamp/portable`, and passed `--variant linux-amd64` — and it told
the user to run `lgs basecamp setup --inspector`, a flag that does not exist on
the released scaffold this branch now pins. Following its own error message got
an unrecognised-flag error. `write.yaml` still points readers at it, and
neither wrapper runs in CI, so the twelve green checks were no evidence
whatever about either.

Rather than apply the same four edits twice, the shared half moves to
`e2e-env.sh`: the sitometres pin, the Basecamp path with its build hint, the
app dir, and both preflight checks. The two wrappers now differ only in the
profile they point the run at, which is the only thing that ever distinguished
them. Keeping them in step by hand has now failed twice; this makes it
structural.

Verified by running both, which is the point:

  run-write-e2e.sh   36/36 passed  (was unrunnable before this commit)
  run-local-e2e.sh   38/39 passed  (unchanged; the one failure is the step
                                    local.yaml documents as seeded-profile-
                                    specific and expected to fail against a
                                    developer's own node)

Also from review, all stale references the previous commit missed:

  * scaffold.toml's runtime_dir warning justified itself with "#266's current
    head, which is what both CI workflows pin via LGS_REV" — a vanished env
    var. The warning is still true, so it keeps the released-version framing
    instead, plus the note that no CI job runs `setup` any more.
  * CLAUDE.md's verb table advertised `build-portable` as "(e2e, AppImage)".
    The e2e layer no longer uses portable at all, so a reader following the
    table landed on exactly the dev/portable mismatch the same file spends
    three paragraphs warning about. Split into the two rows it should have
    been.
  * ui-tests.yml's BASECAMP_REV and store-cache comments both still explained
    themselves in terms of what `setup` builds.
  * .gitignore's dist/ note named the portable directory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fryorcraken

Copy link
Copy Markdown
Collaborator Author

Closing this, together with #265 which it implements — the motivating issue turned out to rest on a false premise of mine. Detail in #265's closing comment; the short version is that bin-bundle-dir-inspector is not the only Basecamp output with the QML inspector compiled in. The dev #app has it too — enableInspector = false is set on appDistributed, which feeds the shipping outputs, not on app. Upstream's own inspector-driven checks use appPkg = app.

The consuming repo now runs its whole end-to-end suite against #app on released scaffold, with no --inspector and no attr changes at all. So the dev-vs-portable classification this PR exists to fix never needed fixing for that use case — it only arose from picking the bundle in the first place.

What is being discarded, and what I would keep

Two separable things landed here. I want to be explicit that only one of them is moot:

  • setup --inspector — moot, per the above. The flag, the BASECAMP_PORTABLE_ATTRS addition and the lockstep [repos.lgpm].attr move all go with it.

  • --print-output on basecamp build / build-portablenot moot, and genuinely useful. It is only unnecessary for us: print_output_enabled() is read inside run_logged, which every build path goes through regardless of subcommand, so LOGOS_SCAFFOLD_PRINT_OUTPUT=1 as a job-level env var already gives the whole behaviour on v0.3.1. Both our workflows now do that.

    The flag is still the better ergonomics — install already has it, and a user who has just watched a build fail with a one-line status and a log file on a runner about to be destroyed should not have to know an env var exists. If you want that half, it is worth a small standalone PR rather than resurrecting this branch; it would be a handful of lines against current master, without the inspector machinery or the ~230 lines of CLI tests that were testing the flag I no longer need. I am happy to send it if it is wanted — say the word. I am not opening it unprompted, since I no longer have a use for it and an unused feature is a maintenance cost you would be carrying, not me.

One thing worth recording

The revert of save_project_config's comment preservation out of this branch — sensible on its own terms, since ~500 lines of config-writer surgery had nothing to do with selecting a basecamp build — silently reintroduced scaffold.toml comment stripping in a repo that had already written it down as fixed. That is not a complaint about the revert; it is the hazard of a downstream consumer pinning cargo install --git --rev to an open PR branch, which is what we were doing and have now stopped. Worth knowing that pinning to a PR branch is a thing people do to your repo.

basecamp setup preserving comments remains a real open ask, independent of everything above.

Apologies for the churn, and thanks for the review time already spent on this.

@fryorcraken fryorcraken closed this Sep 9, 2026
@fryorcraken
fryorcraken deleted the feat/basecamp-inspector-attr branch September 9, 2026 10:44
@fryorcraken

Copy link
Copy Markdown
Collaborator Author

Link for anyone arriving here later: the downstream change that removed the need for this is fryorcraken/logos-radicle-module#38, now merged.

It switches that repo's end-to-end layer to the dev #app and returns both CI workflows to released logos-scaffold 0.3.1 — no --inspector, no --print-output flag, no --git --rev pin. LOGOS_SCAFFOLD_PRINT_OUTPUT=1 as a job-level env var covers the logging half on the release, as described above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

basecamp: allow selecting bin-bundle-dir-inspector via [repos.basecamp].attr

3 participants