Skip to content

release-train: develop -> staging - #540

Merged
tracebloc-release-train[bot] merged 12 commits into
stagingfrom
release-train/to-staging
Aug 22, 2026
Merged

release-train: develop -> staging#540
tracebloc-release-train[bot] merged 12 commits into
stagingfrom
release-train/to-staging

Conversation

@LukasWodka

@LukasWodkaLukasWodka commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Automated promotion by the release train (RFC-0008 D14). Head is the train-managed release-train/to-staging branch (a mirror of develop), so it never collides with a human PR. Merged only when the fr-gate is green.


Note

High Risk
Touches auth/env resolution, bearer-token telemetry POSTs, host-dir wipe vs spool recreation, and training-envelope arithmetic used to size cluster resources. A bug here can send events to the wrong backend, resurrect ~/.tracebloc after offboard, or mis-size training runs.

Overview
Ships command-outcome telemetry for real: one inline POST (~1s), env-partitioned disk spool, OTLP/JSON mapping, and draining of installer spool files filtered by deployment.environment. Offboard (tracebloc delete) now latches the wiped config dir so exit-path telemetry cannot recreate ~/.tracebloc.

Session env is resolved once.sessionEnv trims/lower-cases the stored env; auth status, --check, cluster doctor, and telemetry labels all use that value. Token lookup uses the raw profile key (CurrentToken) so a remapped label cannot miss a working session.

Training envelope is vendored from pinned client-runtime (envelope_contract.json + fail-closed drift workflow). Overhead/floors come from that file; Decision A is unchanged (user ceiling is not shrunk). resources set restating the current size stamps RESOURCE_PROVENANCE=user without --yes.

CI: formatters go through make fmt-check / scripts/format.sh (tracked *.go only); installer job drops apt-get. Version 0.10.10.

Reviewed by Cursor Bugbot for commit e8af6f1. Bugbot is set up for automated code reviews on this repo. Configure here.

shujaatTraceblocand others added 3 commits August 20, 2026 12:57
…eep Decision A (backend#2220) (#538)
* refactor(resources): read the envelope constants from the contract, keep Decision A (backend#2220)
Third and last reader in the P0 consolidation. client-runtime#358 made the
training-envelope arithmetic single-definition and client#766 made both
installers readers of it; this does the same for set.go.
Deleted: the four literals (1-CPU/3-GiB overhead, 1-core/2-GiB floors). They now
come from internal/resources/envelope_contract.json, vendored from
client-runtime, whose arithmetic is node_sizing.envelope_from_allocatable.
cli#143 Decision A is AMENDED IN SCOPE, NOT DEVIATED FROM. The number the user
sets is still the per-run ceiling written to RESOURCE_* verbatim; DeriveTraining
is still the identity on it; Overhead() is still a fit-check safety margin that
is never subtracted. Every exported signature is unchanged, and the existing
suite passes untouched. What went away is the duplicate *definition* of four
numbers, which was never part of that decision. Amendment recorded on cli#143.
Worth naming what the duplication actually cost, because it was not only
tidiness: LargestReadyNode/nodeLarger ranks candidates (cpu, memory) while the
bash installer ranked them (memory, cpu). On a cluster of 8c/16Gi + 4c/32Gi
`resources set` and the installer anchored on DIFFERENT nodes and gave different
answers about one machine. Nobody chose that; it fell out of two independent
implementations. The contract records one order, and this repo's was the one
kept -- it is what the user is shown when the wizard clamps their prompt.
Go needs no generator, unlike the installers: go:embed takes the contract
verbatim at compile time, so the vendored artifact is byte-identical to upstream
and the cross-repo gate is a plain diff. The constants became small funcs rather
than package vars for the same reason Overhead() is a func -- a var would be
mutable from anywhere, and the point is that one place decides.
mustContract panics on a malformed contract, MustCompile-style: the file is
embedded at COMPILE time, so it can only be invalid via a hand-edit or a botched
re-vendor -- a broken build, not a runtime condition. A default here would be a
fifth policy. TestContractIsValid keeps the panic out of a release.
The drift gate FAILS CLOSED when it cannot read upstream, per cli#536 -- a check
that never executed must not report as passing. client-runtime is private, so it
mints a least-privilege App token (named repositories, contents:read) instead of
GITHUB_TOKEN. It also re-derives upstream's vectors from upstream's own generator:
a vendored contract can match upstream byte-for-byte while UPSTREAM's goldens have
gone stale against upstream's code, and mirroring that faithfully is still wrong.
Verified: go build + go vet clean, go test ./... all packages ok, gofmt and
goimports clean, go.mod/go.sum untouched. Mutation-tested: overhead 3GiB -> 4GiB
in the vendored contract reddens 5 tests here, two of them PRE-EXISTING
(TestOverhead_IsOneCoreThreeGiB, TestMaxRunCoresAndGiB) -- so the existing suite
already anchored these numbers independently, which is the ticket's DoD holding
in a consumer repo.
Refs: RFC-BACKEND-664 P0, client-runtime#358, client#766, cli#143, cli#536
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(resources): drop the unreachable ContractVersion export (deadcode gate)
The Lint job's deadcode check flagged it: exported, but unreachable from
./cmd/tracebloc. I had added it "for doctor output" and then never wired it into
a command, which is exactly the shape that gate exists to catch — an export with
no caller reads as API surface and gets maintained like it.
The tests are in-package, so they use mustContract().ContractVersion directly and
lose nothing. Surfacing the contract version in `doctor` may well be worth doing
(a binary and an edge disagreeing about the contract is a real field question),
but it belongs in the PR that touches the read path, not as a stub here.
Not touched: the pre-existing stale allowlist entry for
internal/doctor/doctor.go Status.String. It is reported on clean develop too and
is a warning, not a failure — deadcode-check.sh still exits 0. Pruning it is
someone's cleanup, not this PR's.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: shujaat hasan <shujaathasan@shujaats-MacBook-Pro.local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
`Installer (shell)` is a REQUIRED status check on develop, and its first action was
an `apt-get` with no retry and no time bound of its own. A slow package mirror
therefore consumed the whole 10-minute job budget before any shell was parsed, and
blocked every PR in the repo while doing it.
MEASURED, not theorised. cli#533 is a workflow-only diff that cannot touch installer
behaviour, and it failed FOUR consecutive times:
job 96126585157 Installer (shell) failure 10m16s
15:34 Set up job
15:34 Run actions/checkout
15:34 shellcheck + dash parse <- 10 minutes here, then killed
15:44 Post Run actions/checkout
Nothing after the `apt-get` line ever ran. And the annotation read `The job has
exceeded the maximum execution time of 10m0s` on a job called `Installer (shell)`,
so whoever sees it reasonably concludes the installer is hanging. Nothing points at
apt.
NOT REPO-WIDE, which is worth stating because the ticket first implied it: #530 and
#526 pass the same check. It reproduced on one head, four times.
THE FIX REMOVES THE DEPENDENCY RATHER THAN HARDENING IT. Both tools are already on
`ubuntu-latest`:
* shellcheck is preinstalled -- tracebloc/.github's own `quality / shellcheck`
job, a REQUIRED check in 16 repos, calls `shellcheck --version` with no install
at all. The org has depended on that fleet-wide for as long as that job existed.
* dash IS Ubuntu's /bin/sh, an essential package.
A retry-with-timeout around apt would have been the smaller diff and the worse fix:
a step that installs nothing cannot stall on a mirror, and no wrapper can say that.
`shellcheck --version | head -2` is kept as the first line, matching what the org's
own shellcheck job does -- so the version in use is in the log, and an absent binary
fails on line one with an obvious message instead of somewhere further down.
THIS PR'S OWN RUN IS THE PROOF, and that is deliberate: if either tool were missing
the step fails loudly here, before merge. Better than any claim in the comment.
Verified locally too: shellcheck --shell=sh --severity=error scripts/install.sh
clean, dash -n scripts/install.sh clean.
Closes#534.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…0) (#539)
* feat(resources): stamp `resources set` as a human choice (backend#2220)
Companion to client#768, and the half that makes the marker mean anything.
client#768 taught the installer to write env.RESOURCE_PROVENANCE
(installer | user | unknown). Without this side, an edge the installer marked
`installer` and the operator then re-sized by hand would KEEP saying
`installer` -- a deliberate human choice wearing the one label that invites a
future ladder to overwrite it. That is strictly worse than no marker at all,
so the two PRs are only correct together.
BuildEnvSpec now writes RESOURCE_PROVENANCE=user unconditionally. Unconditional
for the same reason NoGPUEnvValue is: the apply runs
`helm upgrade --reset-then-reuse-values`, which re-applies the release's stored
values on top of chart defaults, so an omitted key is silently RE-INHERITED
rather than cleared. There is no branch on which omitting it would be safe.
`tracebloc resources set` is by definition the human choice -- there is no
variant of running it that isn't.
Read side: Training.Provenance, normalised through NormalizeProvenance, which
maps anything unrecognised -- empty, junk, a value this binary predates, wrong
case -- to `unknown`, never to a guess. Returning "" would have made callers
branch on empty and invent their own default, which is a second policy and
exactly what this ticket removes. Callers MUST treat `unknown` as a human
choice: it means we do not know, and guessing `installer` risks overruling an
operator.
Surfaced in `resources --verbose` as "set by", not in the default view: it
answers a support question ("did someone set this, or did we?"), not one an
operator needs on every run, and a line of bookkeeping that never changes the
numbers should not grow the default output. `unknown` renders with its
explanation rather than the bare word, which invites the wrong conclusion -- it
does not mean something is broken.
The env map already flows into helm.UpgradeParams.Env, so no new plumbing. The
added apply-boundary test starts from an installer-marked edge specifically,
because the unit test proves the map is right while this proves the map arrives
-- and a marker that never lands looks identical to one that did, with the
consequence surfacing much later.
zz-all-strings.golden regenerated: one line, "set by", reviewed.
Verified: go build + go vet clean, go test ./... all packages green, gofmt and
goimports clean, deadcode exit 0. 6 new tests in internal/resources (including a
set-then-read round trip) + 1 apply-boundary test in internal/cli.
Refs: RFC-BACKEND-664 P0, client#768, client-runtime#358, cli#538
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(resources set): a same-size set must still stamp `user` (Bugbot + review, #539)
Bugbot's High, confirmed by @saadqbal in review, and the fix was indeed sitting
three lines below the bug.
BuildEnvSpec stamps RESOURCE_PROVENANCE=user unconditionally WITHIN ITSELF, but
the caller could exit before ever reaching it: `sameCeiling` returned "nothing to
change" whenever the desired ceiling matched the current one. So an
installer-sized edge whose operator ran `resources set --max`, or passed flags
restating the current ceiling, kept `installer` -- a deliberate human choice
wearing the one label that invites a future ladder to overwrite it. That is
verbatim the state BuildEnvSpec's own comment calls the most dangerous the marker
can be in, and my PR body claimed this PR and client#768 "are only correct
together" while this hole made that untrue: not wrong, just narrower.
Fixed with the phantom-GPU branch as the template -- same condition, same
reason, already written and already reviewed. An unchanged ceiling now falls
through to persist when the stored marker is not already `user`. Both conditions
can hold at once, so both report their reason rather than one masking the other.
`unknown` counts as stale deliberately. A pre-marker edge whose operator restates
the ceiling has just made that size explicit, so recording it is the honest
answer; the cost is one extra apply per edge, exactly once, because the second
run sees `user` and is a clean no-op again.
Three existing no-op tests now carry RESOURCE_PROVENANCE=user in their fixtures
(TestSet_NoOpSkipsApply, TestSet_NoOpEvenWhenCurrentNoLongerFits,
TestWizard_LeaveAsIs). Their invariant is unchanged and still asserted -- a clean
no-op makes no helm call -- it just needs the marker present to BE the clean
case. The uncovered case became TestSet_SameCeilingStampsProvenance, a table over
installer / pre-marker / junk, asserting the apply happens, that we do not claim
nothing changed while correcting the marker, and that the reason is stated.
zz-all-strings.golden regenerated: one line, reviewed.
Also rebased onto develop now that cli#538 has merged -- `git rebase --onto
origin/develop 05517ed` to drop the squashed parent's commits rather than
replay them into conflicts. GitHub had already retargeted the base to develop
when #538 landed, so the stacked-PR banner in the description was stale; removed.
Verified: go build + go vet clean, go test ./... all packages green, gofmt and
goimports clean, deadcode exit 0.
Refs: backend#2220, client#768
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(release): bump VERSION to 0.10.10
v0.10.9 is already released and this PR changes published files under
internal/*, so the release train needs a version above every released tag —
version-bump-gate fails otherwise (backend#1561).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: shujaat hasan <shujaathasan@shujaats-MacBook-Pro.local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

Comment threadinternal/cli/resources_set.go
…#2217) (#542)
* feat(telemetry): the CLI validated events and delivered none (backend#2217)
`pendingSink()` returned nil, so every command outcome was validated and
dropped. The ingest endpoint exists now (backend#1905) and speaks OTLP/HTTP JSON
(backend#2213), so this is the delivery half.
Option (c), as decided on the ticket: one inline POST with a ~1s budget for the
whole step, spool to disk on any failure, drain at most 20 pending on the next
invocation. No background daemon, no goroutine outliving the process, no retry
loop -- the next command IS the retry. The deciding argument against
fire-and-forget is that in a short-lived CLI async delivery is a lie: a goroutine
outliving main() is killed at exit, so the honest options are "always block" or
"always drop on failure", and dropping on failure discards exactly the
partition-time events worth having.
Three things worth a reviewer's attention:
1. `anyValue` switches on reflect.Kind, not on concrete type. The emitter's
`checkAttrValue` deliberately admits named scalar types (`type Reason string`,
time.Duration via telemetry.Duration) by kind; a `switch v := value.(type)`
at the seam would match the dynamic type, miss those, and silently drop the
values the layer above went out of its way to accept.
2. A 4xx other than 401/403/408/429 DISCARDS the batch. The endpoint answers 400
for a wholly unparseable batch; re-spooling that would wedge the spool
forever, re-sent by every future command and pushing good records out at the
cap. 401/403 retry because they are a credential state, not a payload
verdict -- the next login makes them deliverable.
3. The spool keeps drop-OLDEST, which is not a contradiction of D7's amended
drop-newest row. That row says drop-newest because `exporterhelper` sheds at
the entrance and offers nothing else -- a platform constraint on the edge
Collector. This spool is our own code and can do what D7 originally wanted,
so it does, matching the installer's `tail -n` trim.
`deliver` takes a resolved URL rather than an env. Found by its own test: with
`api.BaseURL(env)` computed inside, the test posted to PRODUCTION. Passing the
URL also means the record's label and its destination cannot disagree.
No `timeUnixNano`. Per the #2213 decision no client-side timing is sent, so the
receiver stamps arrival and "how late" is knowingly invisible; adding an event
clock is a contract change, not a drive-by here.
Verified: `make check` green (vet, full test suite, fmt, file-budget, style,
tool-pins) and `make lint` clean (errcheck, ineffassign, misspell, staticcheck).
Nine mutations run against the nine new assertions -- collapse the resourceLogs
entries, encode int64 as a number, swap reflect for a type switch, invert the
trim, retry a 400, send the legacy `Token` keyword, widen the spool to 0644,
drop on retry, drop when unauthenticated -- all nine killed their test, none
survived, none merely broke the build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(telemetry): the spool round-trip recoded integers as doubles (#542 review)
@saqlainsyed007 and Bugbot, both right, and it defeated this PR's own wire-mapping
guarantee on the path the design exists for.
`readSpool` used `json.Unmarshal` into `map[string]any`, so every JSON number came
back as float64 and `anyValue` routed it to `doubleValue`. The in-memory attempt
sent `exit_code` as a canonical `intValue` string; every DRAINED retry -- the
partition-time events the whole spool exists to preserve -- sent the same field as
`doubleValue`. Same event, two encodings, wrong one on the path that matters.
Reproduced before fixing rather than argued from the code:
in-memory : "tracebloc.cli.exit_code":{"intValue":"2"}
round-trip: "tracebloc.cli.exit_code":{"doubleValue":2}
Fix is the one suggested: decode with `dec.UseNumber()` so numbers arrive as
`json.Number`, and handle that in `anyValue` -- `intValue` when integral,
`doubleValue` otherwise. It also keeps the spool file human-readable, which was a
stated goal.
THE ORDERING IN `anyValue` IS LOAD-BEARING, and this is the part worth reviewing:
`json.Number` is a NAMED STRING TYPE, so the existing kind switch would have
matched `reflect.String` and emitted `stringValue` -- turning an exit code into a
string on the drained path, a different wrong answer from the float64 one. The
json.Number branch therefore sits before the switch, not inside it.
Why the existing test stayed green, since that is the reviewable lesson:
`TestIntegersAreEncodedAsStrings` operates on the in-memory shape and never goes
through writeSpool/readSpool. So it could not see this, which is the vacuity the
house bar rejects. Two tests now cover the drained path:
* `TestSpoolRoundTripPreservesIntegerEncoding` asserts the two encodings AGREE,
not merely that the drained one looks right -- a test checking only the drained
payload would be satisfied by both paths being wrong together. It also asserts
the in-memory reference explicitly, so a regression there cannot make the
comparison pass by both sides breaking.
* `TestSpoolRoundTripKeepsRealsAsDoubles`, because a fix for integers that
truncated every float would pass the first test.
One note on the first draft of that test: it asserted "no doubleValue anywhere in
the drained payload" and failed on CORRECT output, because the fixture carries
`sampling_rate: 0.5` which must stay a double. Narrowed to per-attribute
assertions -- an over-broad assertion reported as a code bug is its own defect.
Verified: `make check` green (vet, full suite, fmt, file-budget, style,
tool-pins), `make lint` clean. 13 mutations now, all killed: the original nine
plus dropping `UseNumber` (the reported defect), letting json.Number fall through
to the kind switch, routing integral numbers to doubleValue, and truncating reals
to integers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(telemetry): the spool leaked records across environments (#542 review)
Bugbot, and it reopened one level up exactly the mismatch `deliver` takes a
resolved URL to prevent.
A single host-wide `pending.jsonl` meant records queued while signed in to one
backend were drained by the NEXT invocation against another -- POSTed to its
endpoint, with its token, still carrying the first one's
`deployment.environment`. `tracebloc login --env dev` after a failed prod command
was enough. Reproduced before fixing: a `deployment.environment=prod` record
arrived at a dev endpoint with a dev token.
The spool is now per environment, `pending-<env>.jsonl`, and `deliver` takes the
resolved spool PATH rather than deriving one -- so label, spool and destination
are three views of a single resolution in `pendingSink` instead of three chances
to disagree. That is the third reason on that function's doc comment, and the
second and third each bit once.
The consequence is stated in the code rather than hidden: records for an
environment the operator never uses again are never delivered. That is the right
trade -- they are capped, there is usually no token for that environment anyway,
and delivering them to the WRONG backend is not a better outcome than not
delivering them.
`spoolEnvSlug` strips anything outside [a-z0-9-] and lowercases. Unreachable with
the closed dev/stg/prod set, and present because a path segment built from a
string is a traversal waiting for that set to open; an unexpected value becomes
one bucket rather than being dropped, since an undeliverable record is still
evidence.
The leak test asserts BOTH halves -- nothing prod-labelled reaches the dev
endpoint, AND the prod spool still holds its record afterwards -- because a fix
that simply discarded the other environment's queue would satisfy the first
assertion alone.
Verified: `make check` green, `make lint` clean, 16 mutations all killed (the
original nine, the four from the int-encoding fix, plus reverting to a host-wide
spool, and the slug ceasing to strip separators or normalise case).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@tracebloc-release-traintracebloc-release-trainBot added the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Aug 21, 2026
@tracebloc-release-traintracebloc-release-trainBot removed the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Aug 21, 2026
LukasWodkaand others added 2 commits August 21, 2026 08:37
…d#2217) (#545)
Completes backend#2217. The CLI half landed in #542; this is the installer half,
built as option (b) rather than (a).
WHY NOT (a), THE ROUTE THE TICKET ASSUMED. #2217 says "convert at the seam and
POST", which presumes the installer can authenticate. It cannot: the ingest
endpoint needs `Token`/`Bearer` under `IsAuthenticatedEdge`, and the installer
holds `TRACEBLOC_CLIENT_ID`/`TRACEBLOC_CLIENT_PASSWORD` -- a provisioning pair
with no exchange for a token -- and never reads the CLI's config (zero hits across
`scripts/`). Having it read `~/.tracebloc/config.json` would also deliver NOTHING
for the failures that matter most: `validate_config` and `early_data_dir_guard`
run before provisioning, so no token exists on disk when those events are written,
and those are exactly what the installer's `$TMPDIR` fallback was built to
preserve.
So the CLI carries them. It already owns the token and, since #542, the spool,
drain loop and OTLP mapping -- this adds one more file to read and leaves the
installer with no credential handling at all.
THE UNPREDICTABLE FALLBACK PATH TURNED OUT NOT TO NEED AN INDEX FILE. I had
expected to need one; `_telemetry_fallback_spool` uses
`mktemp .../tracebloc-telemetry-XXXXXX`, so the NAME is unpredictable but the
PATTERN is fixed. A glob over $TMPDIR / $HOME / /tmp finds them, the installer
needs no change, and there is no shared state to keep in step.
EVERY RECORD IS FILTERED BY ITS OWN ENVIRONMENT, and this is the part to review.
Our spool is partitioned by env in the FILENAME; the installer's is not, and its
records carry whatever CLIENT_ENV that run used. Forwarding blind would post a
prod-labelled install failure to whichever backend this invocation points at --
the same leak #542's second finding was about. A record ships only when its
`deployment.environment` matches this run's; the rest stay for a later invocation
against that env. A record with NO environment is never forwarded (the contract
omits rather than empties, so absent means unresolvable) but is also never
dropped: it is still evidence.
Their files are touched only on a path that CONSUMED them, and never on failure
or when unauthenticated. A file we took nothing from is not rewritten at all.
Verified: `make check` green, `make lint` clean, 16 mutations across the three
suites all killed. One of the new nine earned its keep: it proved
`TestInstallerRecordsSkipsAFileWithNothingForUs` VACUOUS -- it compared file
bytes, and a rewrite of unchanged records produces identical bytes, so it passed
under the mutation it existed to catch. It now asserts the real contract (the file
never enters `remainder`) and the mutation kills it.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…#544)
* chore(shell): drop the pipe into early-closing readers (backend#2264)
Seven pipelines fed a reader that closes before EOF (`| grep -q`, `| head -1`).
Under `set -euo pipefail` that shape can return 141: the reader closes, the
producer takes SIGPIPE, pipefail surfaces the signal as the pipeline status.
MEASURED, none of the seven is reachable. Every producer here is a bash
builtin `printf` emitting a single short token ("$min", "$max", "$line",
"$BACKEND_REF"), and a single-line producer cannot SIGPIPE a `grep -q`
anyway -- grep must read to end-of-line before it can report the match, so
-q drains the input regardless. This is a shape conversion so the shared
gate can be armed with the repo already green, NOT a bug fix.
scripts/coverage-floor.sh 2 sites
scripts/file-budget.sh 1 site
scripts/sync-backend-fixtures.sh 2 sites
scripts/sync-schema.sh 2 sites
Verified behaviourally, both directions, on the converted conditions:
ref validators REJECT (exit 2) ../../etc/passwd, a..b, -badstart,
'x;rm -rf /', 'a b'
ACCEPT main, abc123/def-1.2_3, a 40-char SHA
malformed guards catch 'pkg' (no colon), 'pkg:abc', 'pkg:12x', 'pkg:-5'
for BUDGETS and FLOORS alike; a well-formed entry still
reports "ok: internal/cli/data.go 248 <= 500"
pct extraction still reads a real number: "ok: ./internal/cli/ 85.0% >= 1%"
Each fixture was asserted to actually apply before its run, so an inert
mutation cannot be mistaken for coverage.
`make file-budget` and `make check-style` pass; shellcheck -S warning clean
across scripts/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: restore the pinned fixtures my own verification overwrote (backend#2264)
Self-inflicted, and worth naming precisely. To check that the converted ref
validators still ACCEPT valid refs, I ran
DATA_INGESTORS_REF=main bash scripts/sync-schema.sh
BACKEND_REF=develop bash scripts/sync-backend-fixtures.sh
These scripts are not predicates. On a valid ref they do the sync -- they
downloaded from unpinned refs and rewrote four tracked files, and `git add -A`
swept them into the conversion commit:
internal/schema/ingest.v1.json (+79 lines from main)
internal/api/testdata/edge_device_adopt.json ("status": 0 -> 2)
internal/api/testdata/edge_device_create.json
internal/api/testdata/edge_device_patch_cluster_id.json
That is a silent supply-chain change to PINNED content, which is exactly what
`.data-ingestors-ref` and `.backend-ref` exist to prevent, and it is what
reddened Schema drift, Backend fixtures drift, Test and Integration (kind).
The drift checks did their job.
Restored all four from origin/develop. The PR is back to the four script
changes it claims to be.
The lesson is the harness, not the scripts: a verification step that INVOKES a
mutating command is not a read-only check, and `git add -A` after running one
cannot tell the difference. The rejection cases (exit 2 before any network
call) were safe; the acceptance cases were not.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@tracebloc-release-traintracebloc-release-trainBot added the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Aug 21, 2026
… (cli#546) (#547)
Bugbot MEDIUM on the develop->staging promotion mirror (#540), against #539
(backend#2220).
#539 made any non-`user` RESOURCE_PROVENANCE stale, so an unchanged ceiling
stopped returning early and instead fell through to the apply in order to
re-stamp the marker as `user`. Correct intent, wrong landing site: the
fall-through lands in the confirmation gate, and off a terminal that gate does
not ask -- it returns exit 1. So
tracebloc resources set --cores 4 --memory 16 # the CURRENT ceiling
went from the exit-0 no-op the command's own --help documents ("0 applied (or
nothing to change)"), and that docs/cli-navigation.md draws as an edge going
straight to exit 0 bypassing CONF, to a hard failure.
Nearly every installed edge still reads `installer` or `unknown`, so the blast
radius was the installed base rather than an edge case, and the callers that
restate a size are scripts -- the bootstrap, the end-to-end journey -- none of
which pass --yes for what the docs promise is a no-op.
The fix is one clause on the gate, not a revert: the gate guards the CEILING,
and an unchanged ceiling has nothing to ask about ("Let each training run use
up to 4 CPU / 16 GiB?" when the answer is already that is a question with one
honest answer). The re-stamp #539 added still happens. The phantom-GPU
fall-through (#241) had the same shape and is fixed by the same clause: both
are bookkeeping writes, already announced by their own Infof lines, not budget
changes an operator needs to sanction.
Tests: TestSet_SameCeilingNeedsNoYes. Every same-ceiling case already in the
file passed `yes: true` -- the flag under dispute -- which is why nothing was
red. The new cases assert exit 0 AND that the apply still happens, because
either alone is satisfiable by the wrong fix; they read RESOURCE_PROVENANCE=user
off the values file helm was actually handed (the existing assertions go through
--dry-run, which skips the very gate at issue); they cover the phantom-GPU
sibling and a declining prompter on a terminal; and one sub-case bounds the fix
by proving a real CHANGE off a terminal still exits 1 and mutates nothing.
Five of the six sub-cases fail without the one-clause change.
docs/cli-navigation.md: the `no change` node now says it may still re-stamp
provenance or clear a phantom GPU, and never asks to confirm -- true of #539's
behaviour too, which shipped without updating the map.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@tracebloc-release-traintracebloc-release-trainBot removed the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Aug 21, 2026
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@tracebloc-release-traintracebloc-release-trainBot added the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Aug 21, 2026
Comment thread.github/workflows/build.yml
@tracebloc-release-traintracebloc-release-trainBot removed the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Aug 21, 2026
… ~/.tracebloc (backend#2314) (#548)
* fix(delete): stop the exit-path telemetry write re-creating the wiped ~/.tracebloc (backend#2314)
`tracebloc delete` printed "✔ Removed local tracebloc data and config." and then
put the directory back before the process exited, so the offboard's central
promise was not kept.
main.go emits the command-outcome event AFTER the command tree returns, and the
telemetry spool lives at <config.Dir()>/telemetry/pending-<env>.jsonl — inside
the tree the offboard just removed. Two separate defects combined:
* writeSpool called MkdirAll BEFORE its len(events) == 0 early return, so it
created the directory even when it had nothing to write and was about to
delete the spool file. This is why the tree came back on the DELIVERED path
too, not just offline.
* Nothing told the exit-path write that this invocation had deliberately
removed local state, so on the undelivered path it wrote a real event file
back into the wiped tree. That is the path the offboard always takes: the
wipe takes the token with it, so deliver() finds no credential and spools.
removeHostDataDir now returns the directory it removed and the offboard records
it, so writeSpool drops any write that lands inside it. The recorded value is
the PATH, not a boolean: a bare "telemetry is off" flag silences writes the
offboard never touched, and is permanently sticky inside a test binary — three
unrelated spool tests failed exactly that way while this was being written.
Delivery over the network is untouched: an online offboard still reports its
outcome. Only the on-disk fallback is suppressed, and a dropped telemetry record
is the cheaper loss against silently undoing a wipe the user asked for.
Regression coverage in telemetry_transport_test.go, verified load-bearing by
reverting each half independently. This is the only failing assertion in the
`Offboard teardown (k3d)` e2e, red on develop since c246912.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ci(e2e): the offboard suite depends on internal/cli/telemetry*.go too (backend#2314)
The paths probe gates `Offboard teardown (k3d)` on the black-box run's
dependency surface, and the telemetry transport was missing from it. That is the
same gap the filter's own comment records for internal/ui after #367: the
command-outcome event is emitted from main.go AFTER the offboard returns, and its
spool lives inside the ~/.tracebloc the offboard just deleted, so a telemetry
change re-created the wiped tree and broke the suite's config-dir assertion
without touching delete.go.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@tracebloc-release-traintracebloc-release-trainBot added the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Aug 21, 2026
Comment threadinternal/cli/telemetry.go
@tracebloc-release-traintracebloc-release-trainBot removed the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Aug 21, 2026
saadqbaland others added 2 commits August 21, 2026 18:58
…cli#549) (#550)
* fix(fmt): the formatter gates walked the working tree, not the repo (cli#549)
`make fmt-check` ran `gofmt -s -l .` and `goimports -l .`, and `.` is the whole
working TREE. Any untracked directory holding Go files — a nested git worktree,
a vendored copy, a build sandbox — was reported as drift while every tracked
file in the repo was correctly formatted:
==> goimports (import grouping) needed on:
<untracked-scratch-dir>/internal/cli/data.go
==> run `make fmt` to fix
CI never saw it, because a fresh checkout has no untracked Go files. So this was
a local-only FALSE failure in `make check`, the documented pre-push tier — and
the remedy it printed was the same bug in write mode: `make fmt` (`gofmt -s -w .`)
rewrote files the repo does not track, i.e. someone else's working copy.
Both invocations, check and write, now take the file list from
`git ls-files '*.go'` — exactly the set a PR can contain.
Notably this was the ONLY gate affected: Go's `./...` skips dot-prefixed
directories, so `vet`, `test` and `deadcode` never saw `.claude/` at all. `gofmt
.` does not skip them, which is why fmt-check alone cried wolf.
scripts/format.sh, rather than more backslash-continued shell in the Makefile,
because the edge cases want testing and comments:
* xargs over a NUL stream from a printf builtin, not a bare argv expansion.
Measured: 9000 tracked files is 2.0 MB of argv against a 1 MB ARG_MAX —
the naive form dies with "argument list too long", the batched form
reports all 9000.
* tracked-but-DELETED index entries are filtered out. `git ls-files` reports
the index, so a mid-edit deletion would otherwise hard-error the gate.
* FAILS CLOSED (exit 2) outside a git work tree, and on an empty file list.
The empty case is not cosmetic: bare `gofmt -l` with no path arguments
reads STDIN, so an unguarded empty list checks nothing and exits 0 — the
inert-verification class of backend#1729.
* stderr is deliberately not captured; `go run`'s download progress would
otherwise appear as phantom drift filenames on a cold cache.
build.yml's Lint job calls `make fmt-check` instead of keeping its own inline
copy, so the file set has one definition and cannot drift from local. That also
drops the restated `goimports@v0.48.0` pin, and GOIMPORTS_VERSION joins
check-tool-pins.sh's TOOLS so it stays dropped on the next bump.
Verified on this branch, with a `.claude/worktrees/` directory present holding
deliberately misformatted Go:
* `make check` — green (was red on the same tree before this change; the old
`gofmt -s -l .` flags the scratch file, shown in the ticket)
* tracked drift still caught, both gates: a non-simplified slice expression
in internal/slug/slug.go fails gofmt -s, a mis-grouped import fails
goimports
* `make fmt` leaves the scratch file byte-identical (sha unchanged)
* fail-closed paths exercised: no work tree, empty list, bad usage — all 2
* a deleted tracked file passes at 218/219 rather than erroring
* shellcheck (default severity, not just -error) clean; actionlint clean
Refs cli#549. Found while working on cli#548, kept out of it to keep that PR
scoped to the offboard telemetry bug (backend#2314).
* fix(fmt): check mode swallowed a formatter failure and called it clean (cli#549)
Bugbot High on scripts/format.sh:90 and @LukasWodka on #550, both correct, and
the defect was the same class this PR exists to fix — in the PR's own new code.
`run_formatter` ended with `exit 2` on a non-zero formatter. Write mode called it
directly, so that killed the script. Check mode captured it:
drift="$(run_formatter "gofmt -s" gofmt -s -l)"
A function that exits inside a command substitution ends only the SUBSHELL. With
`set -uo pipefail` and no `-e`, the parent read an empty `drift`, found no
drift, left `fail` at 0, printed
==> fmt-check: 219 tracked Go file(s) clean
and exited 0. So a cold `go run` cache, a network blip, a bad GOIMPORTS_VERSION
pin or any non-zero gofmt produced a FALSE GREEN in `make fmt-check` and in CI's
Lint step, with the true `FAILED (exit N)` line going to stderr where nothing
acted on it. Reproduced standalone before changing anything:
f() { echo boom >&2; exit 2; }
out="$(f)"; echo "PARENT STILL RUNNING; out=[${out}]"
-> boom / PARENT STILL RUNNING; out=[] / parent exit=0
`run_formatter` now writes the formatter's stdout to a temp file and RETURNS the
status; callers are `run_formatter ... || exit 2` in the parent shell. It never
exits and is never wrapped in `$( )`.
A comment cannot hold that shut, so scripts/tests/format-verify.sh does: eight
properties, formatters stubbed on PATH and via $GO, hermetic, no network, ~6 s.
It asserts the failure propagates in BOTH modes, that the untracked file never
reaches a formatter while the tracked one does, that tracked drift still exits
1, and the three fail-closed paths.
Proof it is not inert — the harness run against a copy of format.sh with the old
subshell shape restored:
FAIL: a failing formatter exited 0 (check mode swallowed it — the #550 defect)
format-verify: 1 FAILED, 7 passed
and against the fix: `format-verify: 8 properties hold`.
Wired into `make check`, `make ci`, and build.yml's Installer (shell) job — that
job, not Lint, because stubbed formatters need no Go toolchain. `make check` is
10.6 s warm with it, against the 60 s budget.
Fixing the harness cost one round of the same trap: `rc="$(run_case ...)"` lost
the CASE_OUT the function had set. Call sites are direct now, with a comment
naming why, since it is the identical mechanism.
Re-verified after the refactor: `make check` green with a `.claude/worktrees/`
scratch dir present holding misformatted Go (the old `gofmt -s -l .` still flags
it), tracked gofmt drift still exits 1, `make fmt` leaves the scratch file
byte-identical, shellcheck clean on both scripts at default severity, actionlint
clean.
Refs cli#549.
* test(fmt): pin each propagation path, not "at least one" (cli#549)
@LukasWodka on #550, and he mutation-proved it rather than asserting it:
swallowing ONLY the gofmt check-mode call site left format-verify green.
The `explode` stub failed BOTH formatters, so whichever call site still had
`|| exit 2` carried the script to a non-zero exit and the case read that as
success. What the suite actually asserted was "at least one propagation path
works" — or-coverage under a name that promises per-formatter coverage. A
regression breaking only gofmt, or only goimports, shipped green through it.
Which is the scenario the file exists to prevent, because ONE call-site shape
being wrong is exactly how the original bug arrived.
The two formatters now get independent stubs (`write_stub`, mode per tool), and
two cases fail exactly one of them, so only that call site can produce the
non-zero exit. Mutation-proved in both directions:
swallow only the gofmt call site
-> FAIL: a failing gofmt alone exited 0 — that call site swallows failures
swallow only the goimports call site
-> FAIL: a failing goimports alone exited 0 — that call site swallows failures
unmutated
-> format-verify: 10 properties hold
Before this commit the first of those was `10 properties hold`, exit 0.
`make check` stays green and inside budget; shellcheck clean at default severity.
Refs cli#549.
…eader (backend#2320) (#551)
* fix(cli): resolve the backend env once per invocation, not once per reader (backend#2320)
The env/base-URL resolution family, not the one site #540 named. Each of the
last recuts produced one more finding about a site that answered "which
backend?" differently from its neighbour (cli#528 review, #542 review, now
#540) — the failure mode is additive, so only the SECOND site is ever a bug.
- telemetry: recordCommandOutcome took the resolved env as a parameter instead
of calling telemetryEnv(signedInEnv()) a second time. The label and the sink
(spool path + POST destination) now derive from one value, which is what the
comment above RecordCommandOutcome already claimed. This is #540's finding.
- sessionEnv is now the single config -> session-env resolution point, and it
normalises (trim + lower-case) like api.ResolveEnv. Returning cfg.CurrentEnv
verbatim made it the one env-resolving function whose output was not
normalised: invisible where the value only reaches api.BaseURL (which
lower-cases again), load-bearing where it is COMPARED, or where one consumer
trims and another does not — api.BaseURL does not trim, so " dev " fell
through to PROD.
- `cluster doctor` built its API client from cfg.CurrentEnv raw and
`auth status --check` compared it raw against an already-normalised target.
Both go through sessionEnv now: a doctor probing prod with a dev token
reports "session expired" for a session that is fine, and the installer,
whose contract is --check's exit code, re-ran login against a working
session.
- internal/doctor.backendHost derives its host from api.BaseURL instead of
restating the same three hosts in a second switch. Behaviour-identical
(BaseURL already lower-cases); it removes the copy that drifts.
- A guard test pins the closed set of sanctioned resolution sites, so the next
one fails a check instead of a review.
NOT changed: api.BaseURL's unknown/empty -> prod fail-open. It is shared with
the installer's _backend_url and contradicted by client-runtime's
controller.py, so it is a three-component decision tracked on backend#2171.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(telemetry): reflow the BaseURL-mirroring note left ragged by the previous edit
Comment-only; no behaviour change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(auth): `auth status` must report the env the client dials, not the stored string (backend#2320)
The audit's last site: `auth status` printed cfg.CurrentEnv as its "backend"
field while runAuthCheck — the machine-facing answer to the same question, 40
lines below in the same file — compares the resolved one. A status command that
disagrees with the client is worse than no status command.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(cli): keep the doctor env test off the network (backend#2320)
My own bug, and worth the comment it now carries. Past the session probe,
`cluster doctor` loads the real kubeconfig and calls the real doctor.Run, whose
checkBackendEgress probes backendHost("") — a live GET to
https://api.tracebloc.io/. On a developer machine with a real k3d cluster the
test therefore made a production request; the run time (~16s vs 0.01s stubbed)
is the tell. Stubbing loadClusterFn returns right after the session probe, which
is all this test needs: the env is decided before it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(cli): make the resolution guard actually guard (backend#2320)
Four review findings, all the same class — a check that verifies the FORM of a
thing rather than the property the form exists to guarantee. Which is the class
this PR is about, so the guard having it was the worst possible place for it.
- Scan Go as Go. The hand-rolled comment stripper was fail-open on string
literals: the `//` inside an `https://…` literal started a "comment" that ate
the rest of the line, needle included, and a `/*` inside a literal like
`"/*.json"` swallowed every needle below it to the next `*/` or EOF. Seven
non-test files in internal/cli already carry an https:// literal, so this was
one future line away. go/scanner with mode 0 drops comments and knows literals,
so neither evasion exists. Literals are kept in the output — a needle inside a
string is then a loud false positive, which is the cheap direction.
- Walk the module root, not the test's own package dir. The guard covered 1 of
the 17 packages under internal/, i.e. it was blind exactly where the next site
is most likely to land: a new package written by someone who never reads
internal/cli. Keys are now repo-relative, and internal/api, internal/config and
internal/doctor join the allowlist with the reasons the PR body already gave.
- An inert allowlist entry now FAILS. Checking only that a sanctioned file exists
let my own change turn the telemetry.go entry into a licence: it matched no
needle any more, so it checked nothing while silently pre-approving the next raw
read in the very file whose double resolution this PR removes. The entry is gone
and the staleness assertion stops the next one going inert unnoticed.
- signedInEnv's docstring was false: it can no longer return "". Says so now,
including that `if signedInEnv() == ""` cannot fire — and telemetryEnv's empty
arm is marked production-unreachable-but-test-reachable rather than left to be
traced.
Also corrects an overclaim I made in the first draft of goCodeTokens' comment:
go/scanner is lexical, so the error covers unterminated literals and comments —
the faults that would desynchronise boundary tracking — not `func f( {`, which
scans clean. Stated precisely rather than left flattering.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(cli): one needle set, one matcher, one allowlist — each checked both ways (backend#2320)
Adopts Lukas's unified shape for the guard, which is better than what I pushed in
f33a3d1 in two concrete ways:
- `matchesAnyNeedle` is now THE matcher, called from the detection sweep AND the
allowlist audit. Two copies of "does this file resolve an env?" is the same
shape as the two copies of "which env?" this PR removes, and it would let
detection and allowlisting drift apart exactly where nobody looks.
- The allowlist audit is per ENTRY, not per suite, so staleness, the empty-reason
check and the needles-went-stale backstop all fall out of one loop and the
failure names the entry to delete. Renaming a needle now reports all five
entries by name instead of a global counter hitting zero.
Kept a narrow anchor the per-entry loop genuinely cannot see: an EMPTY allowlist
makes that loop vacuous, so a needle rename plus an empty allowlist would pass in
silence. It asserts both counts are non-zero.
Eight reproductions, all red, all restored green — the control, both string-literal
evasions, the sibling-package site, a re-inerted sanctioned entry, an empty reason,
a needle rename, and a lexical fault elsewhere in the module.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@tracebloc-release-traintracebloc-release-trainBot added the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Aug 21, 2026
Comment threadinternal/cli/telemetry_transport.go
@tracebloc-release-traintracebloc-release-trainBot added gate-nudge Toggled by the release train to (re-)fire the fr-gate and removed gate-nudge Toggled by the release train to (re-)fire the fr-gate labels Aug 21, 2026
@tracebloc-release-traintracebloc-release-trainBot added gate-nudge Toggled by the release train to (re-)fire the fr-gate and removed gate-nudge Toggled by the release train to (re-)fire the fr-gate labels Aug 21, 2026
Bumps the golang-x group with 1 update: [golang.org/x/text](https://github.com/golang/text).
Updates `golang.org/x/text` from 0.40.0 to 0.41.0
- [Release notes](https://github.com/golang/text/releases)
- [Commits](golang/text@v0.40.0...v0.41.0)
---
updated-dependencies:
- dependency-name: golang.org/x/text
dependency-version: 0.41.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: golang-x
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…nder (cli#552) (#553)
Bugbot's High on the #540 promotion. Two keys for one concept, and the failure
was silent by construction.
`telemetryToken` took the telemetry LABEL and looked the profile up by it.
Profiles are keyed on the RAW `cfg.CurrentEnv`; the label has been through
`telemetryEnv`, which lower-cases and trims (via `sessionEnv`) and remaps
anything unrecognised onto prod. Whenever those disagreed the lookup did not
miss loudly — `Profile()` CREATED an empty profile and returned no token, so
delivery took the no-token spool path forever while `authedClient`, reading
`cfg.Current()` on the raw key, kept working. The CLI looked signed in and
healthy; outcomes simply never arrived.
THE PARAMETER WAS THE DEFECT, so it is gone rather than corrected.
`telemetryToken()` now reads the current session's token, which is the same
thing `authedClient` reads and the same key `SignedIn()` tests. Passing a label
in is no longer possible, so the two cannot disagree again — as opposed to
agreeing today and drifting the next time `telemetryEnv` gains a case.
`config.CurrentToken()` is the reader, and it READS WITHOUT CREATING.
`Profile()` storing an absent profile is correct for the write paths it exists
for — sign-in mutates the returned pointer, then Saves — and a trap as a lookup:
the miss is recorded, so the second call finds a profile and looks like a hit.
That is the ticket's separate observation and it is fixed here, not deferred.
THE TOKEN STILL GOES WHERE THE SESSION ALREADY TALKS. `api.BaseURL` routes an
unrecognised env to prod exactly as `telemetryEnv` does, so the destination the
label picks is the one this session's client is already using. That BaseURL does
that at all is a real defect — shared with the installer's `_backend_url`,
contradicted by client-runtime's controller.py, tracked across three components
on backend#2171 — and `telemetryEnv`'s own note says this code must match that
behaviour until it changes rather than diverge from it. This fix does not
diverge from it.
TESTS. There were NONE for `telemetryToken`, which is why this reached a
promotion. Added at both levels:
* `internal/config` — the reader is keyed on the raw `CurrentEnv` and agrees
with `SignedIn()`; it does not create on a miss, with the contrast against
`Profile()` asserted in the same test so a refactor cannot route one through
the other and still pass; and it is empty on every genuinely-tokenless shape.
* `internal/cli` — a table of raw keys that each differ from their resolved
label (unrecognised → prod, upper-case, surrounding whitespace, a v1
verbatim `Dev`), plus `prod` as the control that worked before and must still.
Each case first asserts the label really does diverge, so the table still
means something if `telemetryEnv`'s mapping changes.
And the composed behaviour the ticket asks for: a signed-in session whose label
was remapped POSTs rather than spools. Real `telemetryToken` over a real on-disk
config, real `deliver`, only the URL substituted — `api.BaseURL` has no test
seam and would otherwise have sent the test at production.
Mutation-proven:
the two-key lookup restored 2 failed (incl. the POST-vs-spool test)
CurrentToken routed via Profile() 1 failed (the creates-on-miss test)
No survivors; green on restore. gofmt, go vet and the full suite clean.
Closes#552
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit e8af6f1. Configure here.

@tracebloc-release-traintracebloc-release-trainBot added gate-nudge Toggled by the release train to (re-)fire the fr-gate and removed gate-nudge Toggled by the release train to (re-)fire the fr-gate labels Aug 22, 2026
@tracebloc-release-train
tracebloc-release-trainBot merged commit d591943 into stagingAug 22, 2026
38 checks passed
@tracebloc-release-train
tracebloc-release-trainBot deleted the release-train/to-staging branch August 22, 2026 15:58
@tracebloc-release-train
tracebloc-release-trainBot restored the release-train/to-staging branch August 22, 2026 16:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gate-nudgeToggled by the release train to (re-)fire the fr-gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@LukasWodka@saadqbal@shujaatTracebloc