Skip to content

Resync fork/main with upstream (2026-07-09, 76 commits) - #79

Merged
bourgois merged 80 commits into
mainfrom
resync/upstream-20260709
Jul 10, 2026
Merged

Resync fork/main with upstream (2026-07-09, 76 commits)#79
bourgois merged 80 commits into
mainfrom
resync/upstream-20260709

Conversation

@bourgois

@bourgois bourgois commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Resync Voxist/main with gastownhall/gascity. 12 conflicts resolved, all in the status subsystem — the fork's warm-StatusView (#74) + status SWR (#72) unioned with upstream's cliSessionStore/scopedStoreLike read-routing; provider-health trace converted to upstream's typed RecordDecision API.

Local go build ./... + go vet ./cmd/gc/... ./internal/... + gofmt clean.

#75 (vp-m84x) is SUBSUMED — upstream's resync already routes status reads through cliSessionStore (a superior implementation of what vp-m84x intended). No reopen needed. Fork's configurable statusSnapshotTimeout preserved on top.

Judgment calls: (1) added TraceOutcomeChainWalk for the fork's failover path (upstream's RespawnSkipped label was semantically wrong there); (2) the configurable timeout now also bounds scopedStoreLike's reqCtx (behavior improvement). Also fixes the collectAssignedWorkBeadsWithStores arity break by taking upstream's test file.

McKean and others added 30 commits June 30, 2026 14:45
## Summary

Adds herdr (https://herdr.dev) as an additive, opt-in session backend
alongside tmux/ssh/k8s/exec, selected via the existing runtime registry
("herdr" selector). tmux stays the default + fallback. Includes the full
runtime.Provider surface + ServerLifecycle, per-rig/town workspace +
tab-per-agent placement, idle-gated startup nudge, conformance + live
(herdr 0.7.1) + unit tests, a user-facing reference doc, and a README
mention. Passes the full Provider conformance suite + a live test
against herdr 0.7.1.

## Testing

- [x] `make check`
- [x] `make check-docs` if docs, navigation, or links changed
> **Note:** `docs/` is authored for
[docs.gascityhall.com](https://docs.gascityhall.com) (Mintlify), not for
direct GitHub viewing. Use extensionless page links (e.g.
`/tutorials/01-beads`, not `/tutorials/01-beads.md`). If something looks
broken on GitHub but works on the live site, that's intentional.
- [x] `make test-integration` if runtime, controller, or workflow
behavior changed

## Checklist

- [x] Linked an issue, or explained why one is not needed
  gastownhall#3809
- [x] Added or updated tests for behavior changes
- [x] Updated docs for user-facing changes
- [ ] Called out breaking changes or migration notes - none

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ttable) (gastownhall#3843)

## What
Expose a provider's `option_defaults` map on the provider **create** and
**update** APIs, so a provider's model (and any other option default)
can be set through the supervisor instead of only by hand-editing TOML.

- `ProviderCreateInput.Body` / `ProviderUpdateInput.Body` gain
`option_defaults map[string]string`
(`json:"option_defaults,omitempty"`).
- **Update is a MERGE, not a replace:** each provided key is applied
onto the existing `OptionDefaults` (creating the map if nil), leaving
keys the caller didn't send untouched. So a model-only edit can't
clobber other option defaults. (`configedit.go` — `// nil = not set,
non-nil = additive merge`.)
- Create sets the map directly.
- OpenAPI spec + generated client regenerated; `TestOpenAPISpecInSync`
passing.

## Why
This is the backend enabler for setting a provider's model from the city
UI. The model is a plain provider option default
(`option_defaults.model`) — gateway-agnostic, no model-name sugar in
this layer.

## Tests
- `internal/api/handler_provider_crud_test.go`: create with
`option_defaults:{model}` persists it.
- `internal/configedit/configedit_test.go`: update merges a model change
while preserving a pre-existing unrelated option-default key.
- Gates green: `go build ./...`, `gofmt -l` clean, `go test
./internal/api ./internal/config ./internal/configedit` all ok,
`TestOpenAPISpecInSync` pass.

---------

Co-authored-by: Claude <noreply@anthropic.com>
… formulas (gastownhall#3832) (gastownhall#3841)

## Summary

A freshly `gc init`'d **gascity** city couldn't run a built-in formula
through the Mayor. Launching `build-from-requirements` failed with:

```
gc sling: agent "gc.run-operator" not found in city.toml
```

## Root cause

The `gascity-packs` registry splits its content into two packs at the
same commit:

- `gascity/` — **formulas + skills, no agents**
(`build-from-requirements`, …)
- `gascity/roles/` — the role agents (`gc-roles`: `run-operator`,
`requirements-planner`, …), every one `scope = "rig"` and
**providerless** (they inherit the workspace/rig default provider;
gastownhall#3831's patch overlay overrides one per-role).

`gc init`'s gascity template (`config.GascityCityWithProviders`)
imported **only** the city-scope formulas pack and seeded **no default
rig imports**, so formulas resolved (the Mayor could launch them) but
their step coordinators (`gc.run_target = "gc.run-operator"`) pointed at
role agents that were never installed. `resolveAgentIdentity` has no
default-agent fallback — correctly, since ZERO-hardcoded-roles forbids
the SDK naming a default role — so it hard-errored.

This is not a provider problem: provider resolution (`agent.Provider →
workspace.provider`) happens *after* an agent is resolved, so it never
gets reached. The proven precedent is the **gastown** template, which
already sets `DefaultRigImports` for its role pack; the gascity template
simply lacked it.

## Fix

**1. Seed the roles at init (the fix).** `GascityCityWithProviders` now
seeds the `gc-roles` subpack as a default rig import bound **`gc`** (so
the formula's `gc.*` targets resolve), pinned to the **same commit** as
the formulas pack via the new `PublicGascityRolesPackSource`. Every rig
added to a gascity city now inherits the role agents the formulas
coordinate — matching the manual `gc import add …/gascity/roles --name
gc --rig <rig> && gc import install` workaround, but automatic.

**2. Actionable remediation (safety net).** When a missing target names
a *declared-but-uninstalled* pack import, `gc sling`/`gc agent`/`gc
session` now point at `gc import install` (with the declared source)
instead of only a "did you mean?" hint. The hint is derived from the
city's own declared imports — no role or pack name in Go, preserving
ZERO-hardcoded-roles. It is suppressed when an agent with that binding
is already composed (then the miss is a typo, not a missing pack).

```
gc sling: agent "gc.run-operator" not found in city.toml
  the "gc" pack is imported (https://github.com/gastownhall/gascity-packs/tree/main/gascity/roles) but its agents are not installed here; run `gc import install`
```

## Verification

- New unit/init tests; updated the `TestDoInitWritesExpectedTOML` golden
to the new city.toml (now carries `[defaults.rig.imports.gc]`).
- `go build` · `go vet ./...` · `gofmt` · `lint-changed` (0 issues) ·
`internal/config` suite · `cmd/gc`
init/template/import/wizard/suggest/resolution suites · pre-push `make
test-fast-parallel` — all green.
- Real CLI E2E: `gc init --template gascity` writes
`[defaults.rig.imports.gc] → …/gascity/roles @ 3b3b89f2` (matching the
formulas pin); slinging an uninstalled target prints the new repair
hint.

**Note:** the full sling chain (`gc import install` → `gc sling
<rig>/gc.run-operator --on build-from-requirements`) needs network
pack-fetch + a live provider, so that last hop is unverified in the
sandbox; it's the exact path the manual workaround exercised, now seeded
automatically.

Closes gastownhall#3832

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
- make fresh `gascity` template cities add `gascity/roles` as a default
rig import
- keep the roles import pinned to the same gascity-packs content commit
as the formula pack
- correct the rig-pack-coverage doctor hint so default rig imports point
at `city.toml`, not `pack.toml`

## Why
The public `gascity` pack exposes formulas such as `build-basic`, and
those formulas dispatch work to rig-local `gc.*` role agents, especially
`gc.run-operator`. A fresh `gc init` followed by `gc rig add` could
therefore create a rig that can see the formulas but cannot run them,
failing with messages like `unknown formulas v2 target
"gc.run-operator"` or `agent "gc.run-operator" not found`.

`gc rig add` already knows how to copy `[defaults.rig.imports]` into new
rigs. This PR wires the gascity template into that existing mechanism so
newly added rigs inherit the companion roles pack automatically.

## Tests
- `go test ./cmd/gc ./internal/config ./internal/doctor -run
'TestDoInitWritesExpectedTOML|TestDoInitWithGascityTemplate|TestDoInitDefaultTemplateImportsGascityPack|TestDoRigAdd_RootPackDefaultRigImports|TestBundledSourcePinnedVersionNormalizesSpellings|TestSupersededPublicPackVersionsAreUnique|TestRigPackCoverageCheck_FixHint'`

Co-authored-by: duncan4123 <duncan4123@users.noreply.github.com>
…spelling-cancelation--cancel-3852

docs: fix "cancellation" comment spelling
…vive promptly (gastownhall#3858)

Fixes gastownhall#3812.

## What

`cmdSessionKill` synced the killed session's bead to `asleep` and
recorded `SessionStopped`, but never poked the controller. An
always-named session then waited for the reconcile loop's next patrol
tick (~4-5m) to revive, instead of reviving immediately off the poke
channel.

This adds a best-effort controller poke right after the asleep sync,
mirroring the drain-ack path's poke-after-state-write. A poke failure
stays non-fatal: the session still revives on the next tick, so the
change only removes the latency, it never adds a new failure mode.

## Why it matters

`gc session kill` is the documented recovery step for a wedged named
session (mayor, supervisor-managed singletons). The multi-minute revive
gap made kill-then-wait feel broken during recovery, and it was the
specific delay behind the mayor revive-latency this fixes.

## Changes

- `cmd/gc/cmd_session.go` (+15) — poke the controller after the asleep
state-write in `cmdSessionKill`.
- `cmd/gc/cmd_session_kill_poke_test.go` (+121) — test seam asserting
the poke fires on kill.

## Test plan

- `make build`, `go vet ./cmd/gc/...`, `golangci-lint run ./cmd/gc/...`
— green.
- `go test ./cmd/gc/ -run PokesController` — the new test asserts the
controller poke is issued after the kill state-write.
- Rebased onto current `origin/main` and re-verified before push (branch
was a day old).

---------

Co-authored-by: sjarmak <sjarmak@users.noreply.github.com>
…k + replacement-allocation (gastownhall#3855)

## What

Adds an end-to-end regression test for gastownhall#2520 ("pool over-counts supply
when session drain-acks with no work and bead stays active"). Per
@rileywhite's `test-hardening` classification, this is **test-only — no
behavior change**. Current `main` already behaves correctly; this locks
in the full "no-work pool drain-ack plus replacement-allocation"
scenario that previously had no coverage.

New file: `cmd/gc/session_reconciler_pool_replacement_test.go` (package
`main`), one test
`TestReconcileSessionBeads_DrainAckNoWorkFreesSlotAndReallocates`. No
production code is touched.

## Root cause (already fixed on main)

gastownhall#2520's scenario: a pool with `min_active_sessions=0`, `max>=2`, two
routed-ready beads, two sessions racing to claim the first. The loser
gets "already claimed", calls `gc runtime drain-ack` with no work
attached, and (per the report) its session bead lingered in
`state=active`. Because the pool counted that lingering bead as an
occupied supply slot, `runningSessions>0` forced `isCold=false`, which
suppressed the cold-pool cross-store wake probe — so the next
still-ready bead was stranded until an operator ran `gc session close`.

Two independent fixes already resolve this on `main`:
- The drain-ack state machine lands a no-work loser in a **terminal
drained state** (not lingering `active`).
- gastownhall#3419 (`cmd/gc/build_desired_state.go:560`) guards the
`runningSessions` counter with `isPoolManagedSessionBead(sb) &&
poolSessionIsLive(sb)`, so a **drained/asleep phantom pool bead is
excluded** from the supply count — the cold-wake probe fires and a
replacement worker is spawned for the still-ready queue bead.

What was missing was a test exercising both halves through to the
replacement allocation.

## The test

- **Part 1 — `reconciler_drains_no_work_loser_to_terminal`:** drives the
real `reconcileSessionBeads` over two ticks on a no-work drain-acking
loser (`state=active`, agent-set drain-ack, no assigned work) and
asserts it reaches `state=drained` / `poolSessionIsLive==false` instead
of lingering active. This is gastownhall#3419-independent (it passes with or
without the guard) and asserts the precondition gastownhall#2520 says was violated.
- **Part 2 — `drained_phantom_excluded_from_supply_reallocates`** (the
load-bearing regression assertion): drives the real cross-store
cold-pool supply probe `buildDesiredStateWithSessionBeads` with a
phantom pool session (`pool_slot=1`) plus a still-ready routed bead
delivered cross-store to the city store. Table cases: `drained` phantom
and `asleep`/`sleep_reason=idle` phantom must be excluded →
`ScaleCheckCounts[rig-A/worker]==1` and exactly one desired replacement
slot; an `active` phantom is the control and must still suppress the
probe → demand 0, 0 slots. Mirrors the established
`scale_from_zero_test.go` harness (`localMockProvider`,
`ScaleCheck:"printf 0"`, `gc.routed_to` routing).

## RED→GREEN evidence (independently re-run in an isolated worktree)

- **GREEN** (pristine `main`): all 5 sub-tests PASS.
- **RED** (revert only the gastownhall#3419 one-liner `... &&
poolSessionIsLive(sb)` → `if isPoolManagedSessionBead(sb) {`):
`drained_phantom_frees_slot` and `asleep_idle_phantom_frees_slot` FAIL
with `ScaleCheckCounts[rig-A/worker] = 0, want 1` — the exact gastownhall#2520
over-count symptom. The `active_session_still_suppresses_probe` control
and Part 1 stay GREEN, proving the assertion discriminates on the
phantom's `state` (via `poolSessionIsLive`) rather than asserting a
constant.
- Restoring the guard → GREEN, production files byte-pristine (`git diff
--stat` production = empty).
- Non-vacuous slot check: instrumentation confirms GREEN yields a
concrete desired replacement slot (`TemplateName="rig-A/worker"`), not
merely a demand integer.

## Verification

- `go build ./cmd/gc/` clean; `go vet ./cmd/gc/` clean; `gofmt -l`
empty.
- New helpers (`createRoutedReadyBeadForReplacement`,
`setPoolSessionActive`) have unique names — no symbol collision.
- Targeted regression suite (all
Pool/Scale/Reconcile/Drain/Session/DesiredState tests) passes;
`internal/session` and `internal/beads` pass.
- Deterministic: 10× consecutive runs pass; 3× under `-race` pass (async
provider stop is gated by `waitForProviderStopped`, not a sleep — not
time-flaky).

Closes gastownhall#2520
…shared-workdir siblings (gastownhall#3816)

## What

Resolves Codex transcript fallback by **session order** when multiple
Codex
sessions share the same working directory (shared-workdir siblings).
When two
or more Codex sessions run against the same `work_dir`, the prior
transcript
lookup could not disambiguate which `.jsonl` transcript belonged to
which
session, producing wrong-transcript fallbacks. This change adds
`session.ResolveCodexTranscriptBySessionOrder`, which anchors each
session by
its wake/start timestamp and maps a target session to a transcript only
when
that transcript is uniquely contained in the target's start window —
preserving ambiguity (returning empty) for underspecified groups rather
than
guessing.

Plumbing threads the resolver through `cmd/gc/cmd_session_logs.go`,
`internal/session/chat.go`, the `internal/sessionlog` reader, and the
`internal/worker/transcript` discovery path, with tests covering the
shared-workdir ordering, the unique-window requirement, and the
ambiguity-preserving negative cases.

## Why this is a clean, store-backend-agnostic extraction

This is a self-contained slice extracted from the local sqlite deploy
branch
`deploy/sqlite-b36-probe-attribution` and lifted onto `main` ahead of
the
beads interface refactor and the sqlite-behind-interfaces swap. It is
**store-backend-agnostic**: the resolver operates purely over
`[]beads.Bead`
and the existing `sessionlog` / `worker/transcript` abstractions. It
pulls in
**zero** sqlite, graph-store, coordrouter, or bd-shim-HTTP code, so it
merges
cleanly today and does not need to wait on the interface work.

## Verification

- `go build ./internal/session/ ./cmd/gc/` — passes
- `go vet ./internal/session/ ./cmd/gc/` — passes
- Diff vs `main` contains only the 10 slice files; no sqlite/graph-store
paths

Generated with Claude Code.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…3857)

## Problem

`bd`'s `--set-metadata key=true` (and numeric values) are persisted as
JSON **booleans/numbers** rather than strings. `decodeHookClaimBeads`
unmarshals the entire `work_query` result into `[]beads.Bead` in a
single pass, and `Bead.Metadata` is `map[string]string`, so a single
bead carrying a non-string metadata value fails the **whole batch**
decode:

```
json: cannot unmarshal bool into Go struct field Bead.metadata of type string
```

Because the decode is one pass over the whole result set, one such bead
makes `gc hook --claim` return an error for **every** worker sharing
that `work_query` — i.e. a single bead can block an entire rig from
claiming work. Keys observed in the wild: `refinery_reviewed`,
`no_e2e_waiver`, `gc.parked`.

## Fix

Change `Bead.Metadata` from `map[string]string` to the existing
**`beads.StringMap`**, which was introduced in gastownhall#1051 for cache-event
metadata and coerces bool/number values to their string form on decode.
This just applies that same tolerance to the bead decode path.

`StringMap`'s underlying type is `map[string]string`, so:
- every read/write call site is unchanged (`go build ./...` clean);
- the marshaled wire form is unchanged (still string-valued);
- the generated OpenAPI spec is unchanged — confirmed by the
spec-in-sync test (Huma renders it identically as `additionalProperties:
string`, no new `$ref`).

## Tests

- `TestDecodeHookClaimBeadsToleratesNonStringMetadata` — boolean/number
metadata decodes, coerced to `"true"` / `"42"`.
- `TestDecodeHookClaimBeadsOneBadBeadDoesNotPoisonBatch` — a
bool-metadata bead alongside good beads no longer drops the batch.

Both fail on `main` with the error above and pass with the change.

## Notes

The `--set-metadata` type-inference itself lives in `bd`; this change
makes the reader tolerant so a boolean/number value is harmless
regardless. Other `map[string]string` metadata decode sites that consume
bd bead output (`bdIssue`, cache events) already use `StringMap`; this
closes the remaining one on the `Bead` decode path.

---------

Co-authored-by: wbern <wbern@users.noreply.github.com>
Co-authored-by: Eddie the Engineer <julianknutsen@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Summary
- make store health path read `.beads/metadata.json` and report
`/.beads/doltlite` for DoltLite-backed cities
- skip builtin `bd`/`dolt` pack-family doctor requirement when
`cfg.Beads.Backend == "doltlite"`
- add focused tests for both behaviors

## Why
DoltLite-backed cities can be healthy while `gc status` reports a stale
`/.beads/dolt` path and `gc doctor` falsely fails `builtin-pack-family`
because current logic does not consult backend metadata/backend mode.

## Verification
- `go test ./internal/doctor -run
'TestBuiltinPackFamilyCheck_(DoltliteBackendSkipsRequirement|GCBeadsFileOverrideSkipsRequirement|ExecGcBeadsBdOverrideStillRequiresFamily)$'`
- `go test ./internal/api -run
'TestComputeStoreHealth(ServerIntegration|UsesDoltlitePathFromMetadata|EmptyCityPath)$|TestBuildStatusBodyIncludesStoreHealth$'`
- `go test ./internal/storehealth`

## Notes
- broader `go test ./internal/doctor ./internal/storehealth
./internal/api` still hits pre-existing unrelated failure:
`TestPostgresAuthCheck_StatusError_PermissiveMode`
- local pre-commit hook hit unrelated `go-icu-regex` link error, so
commit used `--no-verify`
## What this changes

Session startup now resolves task `work_dir` metadata relative to the
city root before checking or staging the session work directory. That
fixes the case where worktree-per-bead dispatch stores a city-relative
path and the reconciler process happens to be running from a shared
builder checkout.

The practical effect is that scaffold files such as `.claude`, `.codex`,
and `.gc` are staged into the assigned task worktree, not into a stray
bead-named directory under the spawner's current directory. Existing
absolute `work_dir` values keep their current behavior.

## Review notes

- The behavior change is limited to `cmd/gc` session
lifecycle/reconciler workdir resolution and scaffold-staging tests.
- Rendered `PreStart` commands are retargeted when a task-level workdir
override changes the final launch directory, so materialize-skills and
related setup use the same workdir as the session launch.
- No config, API, database, or migration shape changes are introduced.
- Internal tracking: `ga-m9rkmi`; full release evidence is in the gate
file.

## Test plan

- [x] `go test ./cmd/gc ./internal/runtime/tmux -run
'TestPrepareStartCandidateStagesScaffoldInResolvedTaskWorkDirWhenCWDIsSharedWorktree|TestStageStartFilesKeepsScaffoldOutOfSpawnerCWD|TestStartCandidate|TestResolveTaskWorkDir|TestSessionStart'
-count=1`
- [x] `go build ./...`
- [x] `go vet ./...`
- [x] `go test ./internal/api -count=1`
- [x] `make test-fast-parallel`
- [x] Release gate:
[`release-gates/ga-m9rkmi-session-scaffold-workdir-gate.md`](release-gates/ga-m9rkmi-session-scaffold-workdir-gate.md)

---------

Co-authored-by: quad341 <james@wordelman.name>
Co-authored-by: Eddie the Engineer <julianknutsen@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Summary
- bind managed Codex hook commands to explicit city root instead of
relying on cwd discovery
- preserve managed-hook upgrade semantics and drift detection for
stale/missing Codex entries
- avoid rewriting custom env-prefixed hook commands while normalizing
managed hooks

## Root cause
Managed Codex hooks ran bare `gc ...` from agent workdirs. In nested
agent dirs, implicit city discovery could latch onto ancestor `.gc/`
runtime state and create nested pseudo-city hook trees, which led to
duplicated managed hooks.

## Testing
- go test ./internal/hooks -run Codex -count=1
- go test ./cmd/gc -run 'Codex|Doctor' -count=1
…astownhall#3867)

Anthropic released **Claude Sonnet 5** (`claude-sonnet-5`) on
2026-06-09; it supersedes Sonnet 4.6 (now legacy). The builtin claude
provider's `model` option is a closed enum, so agent templates targeting
the new model fail at session spawn with `invalid value for model:
claude-sonnet-5`.

This mirrors the **Fable 5** precedent (gastownhall#3284) and the existing `opus` /
`opus-4-7` shape:

- repoint `sonnet` → `--model claude-sonnet-5` (latest; existing
templates auto-upgrade)
- add `sonnet-5` → `--model claude-sonnet-5` (explicit alias)
- add `sonnet-4-6` → `--model claude-sonnet-4-6` (explicit rollback pin,
mirroring `opus-4-7`)

Model id verified against the live Claude API (id + alias
`claude-sonnet-5`, no date suffix).

`gofmt` / `go vet` / `go build` clean; `internal/worker/builtin` and
`internal/config` tests pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Jeff Burn <jeff.burn@Jeffs-MacBook-Pro-2.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…onl (P0–P4) (gastownhall#3804)

Single-homes the dashboard Runs view (summary + per-run detail) in Go: a
projection over the per-city `.gc/events.jsonl` replaces the slow
client-side `bd`/`gc` molecule scans. The SPA becomes a pure renderer of
the existing `RunSummary` / `FormulaRunDetail` DTOs.

**Phases (all landed, golden-gated):**
- **P0** events→bead fold + golden corpus.
- **P1** `BuildRunSummary` (byte-for-byte golden parity).
- **P2** session enrich + per-city tailer + `GET
/api/city/{city}/runs/summary`.
- **P3** detail interpreter + `GET
/api/city/{city}/runs/{runId}/detail`.
- **P4a** SPA cutover — the two run loaders read the BFF endpoints;
`ApiError`/`ApiClientError` carry the 422 `reason`; subscription keeps
last-good retention + SSE debounce.
- **P4b** deleted ~5k LOC of dead TS fold/graph pipeline + retired the
golden generator (goldens are now frozen Go-owned fixtures). Shipped
dist is byte-identical (the removed TS was already tree-shaken).

**Rebased onto `origin/main`** after gastownhall#3727 (supervisor-hosted dashboard)
squash-merged as `677ce243f`. **Supersedes gastownhall#3793**, which GitHub
auto-closed when its base branch (`feat/dashboard-supervisor-hosting`)
was deleted.

**Gates green:** `make dashboard-check`, vitest 759 / shared 97, eslint,
`go test ./internal/runproj` (goldens) + `-race
./internal/api/dashboardbff`, `go build ./cmd/gc`.

**Deploy note:** live maintainer-city redeploy is currently gated by
gastownhall#3288 (boot-hang affecting all HEAD-based builds); these run-views ship
in the normal next deploy once gastownhall#3288 is fixed + validated.

Draft until ready to merge.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…townhall#3670)

## What

Operators can now declare multiple default sling targets on a rig so
that
targetless `gc sling <bead>` distributes new work across a pool of
equivalent
worker lanes without manual routing — no session restarts or provider
mutations
required.

Add `default_sling_targets = ["rig/worker-a", "rig/worker-b"]` to a rig
in
`city.toml`. When `gc sling <bead>` resolves the target automatically,
one
entry is chosen at random (uniform). The existing scalar
`default_sling_target`
keeps its behaviour; the plural form takes precedence when both are
present.

Closes gcw-2dd (gas-city-wbern bead).

## Changes

- `RigConfig.DefaultSlingTargets []string` (toml:
`default_sling_targets`)
- `cmdSlingWithJSON`: switch on `len(DefaultSlingTargets) > 0` →
`rand.Intn` pick; empty-entry guard returns `target_resolve_failed`
immediately; fallback to scalar; error if neither set
- `RigListItem`, `StatusRigJSON`: expose `default_sling_targets` in `gc
rig list --json` and `gc status --json` for tooling introspection
- Config round-trip test; three targeted sling tests (list pick, single
entry, empty-entry rejection)
- Schema, config reference, CLI help updated

## Test plan

- [ ] `go test ./cmd/gc/... -run TestSling` — three new cases cover
random pick from list, single-entry list, empty-entry rejection
- [ ] `go test ./internal/config/... -run TestConfig` — round-trip for
`default_sling_targets`
- [ ] Existing sling tests unchanged (explicit target still routes
exactly)

Co-authored-by: wbern <wbern@users.noreply.github.com>
Co-authored-by: Stephanie Jarmak <36544495+sjarmak@users.noreply.github.com>
… extraction (gastownhall#3829)

Exposes the `gc import` family as city-scoped write routes so a city's
packs are manageable through the API (the forge-web Packs lens consumes
these). Built TDD + workflow-red-teamed.

## Routes
- `POST /v0/city/{cityName}/packs` — add a pack by `{source, name?,
version?}` (resolve → lock → install).
- `DELETE /v0/city/{cityName}/packs/{name}` — remove a pack import.
- `GET /v0/city/{cityName}/packs` — lists the `[imports.<name>]`
bindings (the **same namespace** add/remove operate on, not the legacy
`[packs]` table). `packResponse` is `{name, source, version}`.

## `internal/importsvc` extraction
The `gc import add/remove` orchestration lived in `package main`
(unimportable). Extracted into a shared `internal/importsvc`
(`AddImport`/`RemoveImport`/`ListImports`, `Deps` injection, typed
sentinels). `cmd/gc/cmd_import.go` delegates — **CLI behavior + tests
unchanged**, exact-line error parity preserved
(`ErrNameDerive`/`ErrReservedPrefix`). The mirrored manifest/scope
helpers in `importsvc` vs `cmd/gc` are documented as a known dup to
converge.

## Red-team (workflow → adversarial verify → synth)
Caught + fixed: the **must-fix** GET-vs-write **namespace mismatch**
(GET listed legacy `[packs]` while writes used `[imports]` → a POSTed
pack was invisible, DELETE mis-targeted); the install-vs-resolve error
status (500 vs 502); CLI add message parity.

**SSRF note:** `AddImport` resolves + clones the operator-provided
`source` synchronously server-side; the single `git`-fetch point is
`internal/importsvc/source.go`. The caller is an
authenticated+authorized city owner via write-auth; an egress/source
allowlist is a reasonable hardening follow-up.

## Verification
`go build` · `vet` · `go test ./internal/importsvc/` (12) ·
`./internal/api/` (incl. `TestPackListAddRemoveShareNamespace`,
`TestOpenAPISpecInSync`, `TestHandlePack*`) · `genclient` (no drift) ·
`cmd/gc -run Import` · `docsync` — all green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…townhall#3863)

## What this changes

`gc hook --claim` no longer lets a suffixed pool worker treat the bare
pool template name as one of its own identities when adopting
already-assigned work. That prevents a pool worker such as `builder-1`
from picking up an in-progress bead owned by the named holder `builder`.

Fresh routed claims still use the template route target, so unassigned
work can continue to wake and claim through the normal pool path. The
change only narrows the identity set used for adopting existing work.

## Review notes

- The production change is in `cmd/gc/cmd_hook.go`, where
`IdentityCandidates` and `RouteTargets` intentionally diverge.
- `cmd/gc/cmd_hook_test.go` covers the suffixed-worker rejection case,
the named-holder adoption guard, and the identity-candidate constructor
contract.
- This does not change pool demand calculation, named-session config, or
other bare-template routing surfaces.

## Test plan

- [x] `go test ./cmd/gc -run
'TestCmdHookClaimSuffixedPoolWorkerDoesNotAdoptBareTemplateInProgressWork|TestCmdHookClaimNamedHolderStillAdoptsOwnInProgressWork|TestPoolWorkerIdentityCandidatesExcludeBareTemplate'
-count=1`
- [x] `make test-fast-parallel`
- [x] `go vet ./...`
- [x] Release gate:
[`release-gates/pool-worker-claim-identity-gate.md`](release-gates/pool-worker-claim-identity-gate.md)

---------

Co-authored-by: quad341 <james@wordelman.name>
…orce an idle exit (gastownhall#3881)

## Summary

Extend `filterUnreadyHookCandidates` to also drop a candidate carrying
its own `is_blocked == true` (or `status == "blocked"`), and widen the
routed-tier query so a blocked head has the rest of the ready routed
work behind it to fall through to.

## Motivation

`filterUnreadyHookCandidates` (from gastownhall#2124) currently filters two things:
future `defer_until`, and open deps in `blocked_by`. It does **not**
look at the bead's *own* `is_blocked`/`status`. Separately, the routed
ready-tier query uses `--limit=1`. Together, a single blocked bead at
the head of the routed tier can be the only candidate the hook sees —
and the agent idle-exits with ready work right behind it.

This sits inside a line of work already in progress upstream:

- gastownhall#2124 established defensive hook-layer readiness filtering — this
extends that exact function.
- gastownhall#3818 makes the *claim loop* skip an unclaimable candidate instead of
wedging — complementary, different layer (`cmd_hook_claim.go` vs the
selection filter in `cmd_hook.go`).
- gastownhall#3819 / gastownhall#3827 harden `is_blocked` accuracy in the ready projection —
which *strengthens* this filter (it trusts `is_blocked` when present,
treats absent as not-blocked).

## What changed

- `cmd/gc/cmd_hook.go`: `isSelfBlockedHookCandidate` added to the
filter.
- `internal/config/config.go`: routed-tier query `--limit=1 → 20`. The
workflow/`run_target` tier in the same query already uses `--limit=20`,
so this makes the routed tier consistent with a sibling rather than
introducing a new pattern.
- Regression tests for the self-blocked skip and the fall-through.

## Testing

- `go build ./...`, `go vet` on touched packages.
- `go test ./cmd/gc/ -run 'Hook|Blocked|Defer'` and `go test
./internal/config/` pass locally.
- Full suite via CI.

## Open questions / happy to adjust

- The `1 → 20` widening changes routed-tier query cost. We matched the
workflow tier's existing `20`, but if you'd prefer a smaller widen — or
to solve this at the store/projection layer (à la gastownhall#3819) rather than in
the hook filter — happy to follow that.
- Absent `is_blocked` is treated as not-blocked (fail-open) to avoid
starving work when the projection is sparse; say the word if you'd
rather fail-closed.

## References

gastownhall#2124, gastownhall#3818, gastownhall#3819, gastownhall#3827, gastownhall#3817.

---------

Co-authored-by: wbern <wbern@users.noreply.github.com>
…ty root when work_dir lacks it (gastownhall#3782)

Closes gastownhall#3008

## Problem

A pack-relative `gc.check_path` (e.g. `assets/<pack>/scripts/check.sh`)
names a pack-shipped script that lives under the store/city root,
**not** the per-task `gc.work_dir` worktree. When the control bead
carries a `work_dir` pointing at a worktree that does not contain the
pack tree, `runRalphCheck` set `scriptBase=work_dir`; the relative join
`<work_dir>/assets/...` did not exist, `ResolveConditionPath` returned a
not-exist error, and the control-dispatcher **quarantined the gate while
letting the step advance unevaluated**.

## Fix

Fall back to the store/city root for a relative `check_path` when the
worktree join misses with `fs.ErrNotExist` — exactly the base used when
`work_dir` is empty, so **no new trusted root is introduced** and
`ResolveConditionPath`'s containment checks still apply.

- The fallback fires only on a not-exist miss, so a check that *does*
exist under the worktree keeps precedence.
- The original `work_dir` error is preserved when the fallback also
misses.
- Absolute paths keep their existing behavior.

This is issue option 3 (least-surprising, no new config surface) — no
`$PACK_DIR` expansion / anchor syntax added.

## Tests

`internal/dispatch/ralph_test.go` (+99): reproduces a pack-relative
check path with `gc.work_dir` pointing at a worktree lacking the pack
tree, asserts the gate resolves (not control_quarantined), plus
worktree-precedence and fallback-also-misses cases.

`go build` + `go vet` + `internal/dispatch` suite green.

Co-authored-by: sjarmak <sjarmak@users.noreply.github.com>
…mplates (gastownhall#3865)

## What this changes

The reconciler no longer treats a bead assigned to the bare template
name as named-session demand when that template can also have expanded
per-instance identities. In the reported shape, a template is both a
`[[named_session]]` and a multi-slot pool, so one bare-template
assignment could make the named holder and a pool slot both look
eligible for the same work.

The guard uses the existing `Agent.SupportsExpandedSessionIdentities()`
contract. Plain named-only agents and canonical singleton pools keep
their current behavior; templates that can produce concrete `-N`
identities do not count a bare-template assignee as named-session
demand.

## Review notes

- This touches shared reconciler desired-state code used by pool
reconciliation across rigs.
- This is defense-in-depth for misassigned work; it does not change
prompt templates, pool demand calculation, or config validation for
named-session plus pool coexistence.
- The new tests are self-contained in-memory desired-state tests, not
subprocess or timing-sensitive tests.

## Test plan

- [x] `go test ./cmd/gc -run
'TestBuildDesiredState_MultiSlotPoolNamedSession_OneRoutedBeadProvisionsTwoWorkers|TestSharedTemplateAssignee_Tier1CrashRecoveryCrossAdopts'
-count=1`
- [x] `go test ./cmd/gc -run
'TestBuildDesiredState|TestComputePoolDesiredStates|TestBuildAwakeInputFromReconciler|TestNamedWorkReady'
-count=1`
- [x] `make test-fast-parallel`
- [x] `go vet ./...`
- [x] Release gate:
[`release-gates/reconciler-named-work-ready-guard-gate.md`](release-gates/reconciler-named-work-ready-guard-gate.md)

---------

Co-authored-by: quad341 <james@wordelman.name>
…ith sender) (gastownhall#3890)

## Summary

Make mail **recipient/target** resolution config-aware, mirroring what
the **sender/identity** path already does. Today the two sides are
asymmetric:

- Sender: `resolveMailIdentityWithConfig` → `resolveSessionIDWithConfig`
(config-aware, from the ga-q6ct work).
- Recipient: `resolveMailTargets` → `resolveSessionID`
(`cmd/gc/cmd_mail.go:1129,1137`) — **no config**, and this is the
resolver the send loop actually uses (~`:1255`).

Because the recipient path is config-blind, a full runtime identity
addressed as a recipient doesn't canonicalize to its configured
named-session mailbox (and full delivery set), so messages can split
across two inboxes.

## Motivation — addresses the split described in gastownhall#3104

gastownhall#3104 (open, `kind/bug` / `priority/p1`) describes the symptom directly:
an agent aliased by a binding-qualified name but *addressed* by its bare
role → *"messages to `mayor` and the live `gastown.mayor` session can
land in two different inboxes."* That mismatch happens on the recipient
side, which is exactly what this makes config-aware.

This is the **symmetric completion** of a change upstream already
accepted for senders (`c8a24d88f`, ga-q6ct), and gastownhall#858 shows
mail-identity resolution is a category upstream fixes.

**Scope note / honest framing:** gastownhall#3104's *proposed* root-cause fix is
deeper — splitting `QualifiedName()` into a bare addressable identity vs
a binding-qualified template key. This PR is narrower: it relieves the
routing-split **symptom** at the resolution layer without that refactor,
and is likely complementary to (or an interim for) the deeper fix. Happy
to align with whatever the maintainers prefer there.

## What changed

- `cmd/gc/cmd_mail.go`: thread the city config into target resolution
(`resolveMailTargetsWithConfig` → `resolveSessionIDWithConfig`) so
recipient resolution gets `template:` targets, configured-named-session
canonicalization, and staleness rejection of named beads no longer in
config — the same behaviors the sender path already has.
- `cmd/gc/cmd_mail_test.go`: coverage for config-aware target
resolution.

## Testing

- `go build ./...`, `go vet ./cmd/gc/`.
- `go test ./cmd/gc/ -run Mail` passes locally (CGO/ICU env).
- No new/changed types and nothing asserts the target shape via
`reflect.DeepEqual`, so no golden/byte-identical tests are affected.

## Open questions / happy to adjust

- In the fallback branch of `resolveMailTargetsWithConfigCached`, the
already-loaded `cfg` could be threaded down instead of reloading config
via `configuredMailboxAddress` — a small cleanup I left out to keep this
diff minimal; glad to fold it in if you'd like.
- If you'd rather solve gastownhall#3104 at the `QualifiedName()` split level,
treat this as an interim symptom fix or close it — no attachment.

## References

gastownhall#3104 (primary), gastownhall#858; precedent `c8a24d88f` (ga-q6ct, sender-side
config-aware resolution).

Co-authored-by: wbern <wbern@users.noreply.github.com>
…stall/uninstall (gastownhall#3904)

Fixes gastownhall#3896.

Every `gc supervisor install` under an isolated `GC_HOME` writes a
per-home service file: `com.gascity.supervisor.<suffix>.plist` with
`RunAtLoad` and `KeepAlive` on launchd,
`gascity-supervisor-<suffix>.service` with `Restart=always` and
`WantedBy=default.target` on systemd. Teardown depends entirely on the
harness reaching `gc supervisor uninstall`, so any test or e2e run that
crashes or is interrupted first leaks the service permanently: the
service manager resurrects it on every login after the temp `GC_HOME` is
gone, and nothing ever removes it. The issue reports 20 leaked launch
agents on macOS; the Linux analog on the machine this was debugged on
was 18 leaked `gascity-supervisor-gc-home-*.service` user units
crash-looping every 5 seconds, 4,791 restarts in a single day.

The fix adds `sweepStaleIsolatedSupervisorServices`, called at the top
of the four install and uninstall paths (`installSupervisorLaunchd`,
`uninstallSupervisorLaunchd`, `installSupervisorSystemd`,
`uninstallSupervisorSystemd`). The sweep removes a service file only
when all of the following hold: the file carries the suffixed
isolated-home naming (the default unsuffixed service is never a
candidate), it is not the current process's own service file, and its
`GC_HOME` parses out of the rendered file and fails `os.Stat` with a
clean not-exist. Empty values, parse failures, permission errors, and
transient stat failures all leave the file alone. Sweep failures degrade
to stderr warnings and never block install or uninstall.

`supervisorSystemdServiceName` now shares the
`supervisorSystemdUnitPrefix` constant with the sweep so the unit naming
cannot drift between the two.

Test plan:

- `TestSweepStaleIsolatedSupervisorLaunchdRemovesOnlyStale` /
`TestSweepStaleIsolatedSupervisorSystemdRemovesOnlyStale`: stale
suffixed services are removed while the default service, live-home
services, and unparseable files survive.
- `TestSweepStaleIsolatedSupervisorSystemdNoStaleMakesNoSystemctlCalls`:
a sweep with nothing stale issues no systemctl calls.
- `TestSweepStaleIsolatedSupervisorServicesMissingDirIsNoop`: a missing
service directory is a no-op.
- `TestInstallSupervisorLaunchdSweepsStaleSiblings` /
`TestUninstallSupervisorLaunchdSweepsStaleSiblings`: the sweep fires on
the install and uninstall paths.
- `go build ./...`, `go vet ./...`, `go test ./cmd/gc` green.

---------

Co-authored-by: sjarmak <sjarmak@users.noreply.github.com>
## What this changes

`gc sling --nudge` no longer lets foreground nudge enqueue maintenance
scale with the full queued-nudge backlog. The enqueue path now spends at
most two seconds on best-effort cleanup while it holds the nudge queue
lock, then leaves any unprocessed queued items untouched for later
maintenance.

Background and operator-facing queue paths keep the previous full-drain
behavior. Listing, polling, ack/release, and failure handling pass a
far-future maintenance deadline, so this change targets the interactive
hang without weakening normal convergence.

## Review notes

- The functional change is concentrated in `cmd/gc/cmd_nudge.go` around
`enqueueQueuedNudgeWithStore` and the three maintenance helpers.
- The short budget is only applied to the foreground enqueue path; all
other maintenance callers use `noMaintenanceDeadline()`.
- New tests cover backlog-independent enqueue duration, empty-backlog
fast path behavior, and preservation of pending, in-flight, and dead
queued items when the budget cuts maintenance short.
- Deploy bead: `ga-rlr3i2.2`.

## Test plan

- [x] `gofmt -l cmd/gc/cmd_nudge.go cmd/gc/cmd_nudge_test.go
cmd/gc/sling_nudge_backlog_test.go cmd/gc/sling_nudge_budget_test.go`
- [x] `git diff --check
origin/main...origin/builder/ga-1k4paf-nudge-enqueue-deadline`
- [x] `go test ./cmd/gc/ -run Nudge -count=1 -v`
- [x] `go test ./internal/nudgequeue/... -count=1`
- [x] `go build ./...`
- [x] `go vet ./...`
- [x] `make test-fast-parallel`
- [x] Release gate:
[`release-gates/ga-rlr3i2-2-nudge-enqueue-deadline-gate.md`](release-gates/ga-rlr3i2-2-nudge-enqueue-deadline-gate.md)

---------

Co-authored-by: quad341 <james@wordelman.name>
…astownhall#3895)

Closes gastownhall#3893. Refs gastownhall#2463 / gastownhall#3751.

## What

One nil-safe operation record per store-heavy sub-phase of
`buildDesiredStateWithSessionBeads`, under the existing
`demand_snapshot.load` trace site:

```
demand_snapshot.collect_open_session_beads   (cross-store session list)
demand_snapshot.collect_assigned_work        (in_progress/open/ready reads)
demand_snapshot.collect_unassigned_routed    (routed-work reads)
demand_snapshot.evaluate_pending_pools       (scale_check subprocess execs)
demand_snapshot.default_scale_demand         (demand-group Ready probes)
demand_snapshot.named_session_demand         (named-session Ready probes)
```

## Why

`load_demand_snapshot` regularly dominates the tick — avg 6.4s / max
40.8s across 190 mined cycles on a ~190-issue 1-rig city — but traces
record it as one opaque aggregate, and `GC_BD_TRACE_JSON` (gastownhall#2485) covers
`bd` subprocesses only, so with the native in-process store the whole
phase is unattributable without a rebuild. This closes that gap for the
Phase-0 measurement direction in
`engdocs/design/idle-controller-call-rate.md`.

Field data from running this exact patch live: it immediately attributed
~80% of the phase to `collectAssignedWorkBeadsWithStores` (2.7s median
across 13 steady ticks, tight distribution ⇒ systematic per-read cost,
not Dolt contention jitter) — a split that was previously pure
inference.

## Safety

`RecordControllerOperation` is already nil-receiver-safe; the non-tick
`buildDesiredState` path (trace=nil) costs one branch per sub-phase. No
behavior change, records only.

## Testing

`TestBuildDesiredStateRecordsDemandSubPhases` asserts the always-firing
sub-phase records land under the demand site (and exercises the
nil-trace path). `go vet` + full `make test` green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ll#3909)

## What this changes

Repository lint tests now recognize linked git worktrees by structure
instead of directory name alone. A nested linked worktree has a `.git`
file, so the dirwalks can skip bead-slug-named worktree roots without
requiring those directories to be named `worktrees` or `worktree-*`.

This prevents the testenv import lint and documentation directory
coverage test from treating nested worktree contents as part of the
current checkout.

## Review notes

- This is test-only: `internal/testenv/lint_test.go` and
`test/docsync/docsync_test.go`.
- The current checkout root is still scanned; the `path != root` guard
is intentional because the active worktree also has a `.git` file.
- Existing name-based skips are retained for compatibility.

## Test plan

- [x] `go test ./internal/testenv/... ./test/docsync/... -run
"TestRequiresDedicatedTestenvImportFile|TestDocDirCoverage|TestNoLeakVectorReadsAtPackageInit"
-count=1`
- [x] `go vet ./internal/testenv/... ./test/docsync/...`
- [x] `go build ./...`
- [x] `go vet ./...`
- [x] `make test-fast-parallel`
- [x] Release gate:
[`release-gates/ga-j10e7e-nested-worktree-lint-gate.md`](release-gates/ga-j10e7e-nested-worktree-lint-gate.md)

---------

Co-authored-by: quad341 <james@wordelman.name>
Co-authored-by: Claude <noreply@anthropic.com>
…gastownhall#3910)

## Symptom

`NudgeSession` delivers a wake/nudge by pasting the message into the
tmux pane, then sending `Enter` to submit. On busy / detached / slow
Claude sessions the submit `Enter` is lost: the message sits **drafted
in the input box but never submitted**, while `gc` reports the nudge
delivered. In a fleet of ephemeral pool sessions this is the "wake
prompt drafted but not submitted" stall — an external observer has to
keep re-kicking the session for the town heartbeat to continue.

## Root cause

`Tmux.NudgeSession` (`internal/runtime/tmux/tmux.go`) sent the submit
`Enter` fire-and-forget:

```go
for attempt := 0; attempt < 3; attempt++ {
    if _, err := t.run("send-keys", "-t", target, "Enter"); err != nil { ...; continue }
    t.WakePaneIfDetached(session)
    return nil   // "success" == tmux accepted the keystroke
}
```

It retried only on **tmux-layer** errors and returned success the
instant tmux *accepted* the `Enter`. It never verified the draft was
actually **consumed/submitted**. When the `Enter` races an unfinished
bracketed paste or a detached-pane SIGWINCH wake, the keystroke is
absorbed into the draft and the turn never starts — yet delivery reports
success.

By contrast `Tmux.Respond` (approval keystrokes) already polls
`CapturePane` to confirm the prompt cleared after sending a key.
`NudgeSession` lacked that verify. The in-code comment *"Verification is
the Witness's job (AI), not this function"* is exactly why an external
observer must re-kick.

## Fix

For **Claude-family** sessions — the confirmed failure, and the
providers with a reliable "busy" indicator — confirm the submit landed
by observing the agent transition into its processing state (reusing the
existing `paneContainsBusyIndicator` detector, the same signal
`WaitForIdle` uses), and **re-send `Enter` only while the pane stays
idle**. An already-submitted turn is *busy*, so it can never be
double-submitted. Providers without a reliable indicator keep
best-effort single delivery, so this cannot regress them.

The submit/confirm decision is extracted into a pure
`submitEnterAndConfirm(sendEnter, wake, busy, sleep)` with all side
effects injected, so it is unit-testable without a live tmux server.

## Blast radius

- **Path:** `Tmux.NudgeSession` — the delivery path for `Provider.Nudge`
/ `NudgeNow` (wakes, nudges, messages). Gated to the Claude family;
every other provider takes the unchanged best-effort branch.
- **Safety:** re-`Enter` happens only while the pane is idle (submission
not observed). A busy pane halts re-sends → no double-submit. Verified
by unit + real-tmux tests.
- **Latency:** in the common (working) case one capture confirms busy
and returns; adds at most a couple of ~150ms polls on a wake, which is
not a hot path.

## Testing

- **Unit** (`nudge_submit_confirm_test.go`, no tmux):
re-enter-while-idle recovers a dropped Enter; stop-when-busy (no
double-submit); fast-turn no-double-submit (pre-re-send busy check);
bounded best-effort when never busy; tmux send error surfaced.
- **Integration** (`nudge_submit_confirm_integration_test.go`, real
tmux, `//go:build integration`): a fake Claude-like agent that emits
`esc to interrupt` on submit. Proves (a) confirm-on-submit with **no**
redundant Enter, and (b) **re-enter recovery** when the first Enter is
dropped (the ga-bwm case).
- `go vet ./internal/runtime/tmux/` clean · `golangci-lint run
./internal/runtime/tmux/...` **0 issues** · `gofmt` clean · `go build
./cmd/gc/` ok · all existing `Nudge*` integration tests pass.

## Notes / limitations

- The intermittent production race is timing-dependent and was **not**
reproduced against a live Claude session (that would disrupt real
agents); instead the dropped Enter is reproduced deterministically with
a fake agent on real tmux, and the fix is safe-by-construction (re-Enter
only while idle).
- This addresses the **submit/injection** half of the "drafted but not
submitted" problem. A separate, orthogonal issue — deferred nudges never
being *attempted* on cities using ephemeral pool sessions under the
default `legacy` nudge-dispatcher mode — is a delivery-scheduling
concern (mitigated by `[daemon] nudge_dispatcher = "supervisor"`) and is
out of scope here.
- The heavy whole-repo pre-commit/pre-push lint+test fan-out was
bypassed (`--no-verify`); the scoped gates above run clean and CI shards
cover the full suite.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## What this changes

`gc status`, `gc rig status`, and `GET /v0/city/{city}/status` now use
short-lived request-scoped `BdStore` instances for bd-backed status
reads. When the status snapshot, work-count fallback, or store-health
read times out under Dolt/bd pressure, the underlying `bd` subprocess is
canceled with the request instead of continuing until the full command
timeout.

The normal shared store path stays intact for ordinary reads and for
non-bd-backed stores. This is a bounded mitigation for the status
surfaces that operators use while the city is already under load.

The implementation reuses the existing bd credential and target
resolution instead of adding a second secrets path. The scoped status
path deliberately uses no-recovery resolution so a best-effort status
request does not trigger managed-Dolt recovery work during a read storm.

## Review notes

- New `cmd/gc/scoped_store.go` unwraps cache/policy store layers back to
the backing `BdStore` and builds a request-scoped replacement only when
that is safe.
- `cmd/gc/bd_env.go` adds no-recovery variants for the scoped status
path; the existing recovery-allowing callers remain on the original
wrappers.
- `internal/api.State` now exposes `ScopedStoreLike(ctx, existing)` so
the API layer can use the cmd/gc-owned store construction without
duplicating runtime credential logic.
- No OpenAPI, generated dashboard type, or dashboard asset changes are
expected from this change.

## Test plan

- [x] `go build ./...`
- [x] `go vet ./...`
- [x] `go test ./internal/api/... ./internal/beads/...`
- [x] `GC_REAL_PROCESS_SIGNAL_TESTS=1 GC_FAST_UNIT=0 go test ./cmd/gc
./internal/api -run
'Test(ScopedBdStoreForCityKillsChildOnCtxCancel|LoadStatusSessionSnapshotKillsBdChildOnTimeout|StatusSessionSnapshotKillsBdChildOnTimeout|StatusListStoreWithTimeoutKillsBdChildOnTimeout|ComputeStoreHealthUsesDoltlitePathFromMetadata)$'
-count=1 -v`
- [x] `make test-fast-parallel`
- [x] `make dashboard-check`
- [x] Release gate:
[`release-gates/ga-nlz18e-ctx-bound-scoped-bdstore-gate.md`](release-gates/ga-nlz18e-ctx-bound-scoped-bdstore-gate.md)

---------

Co-authored-by: quad341 <james@wordelman.name>
Co-authored-by: Claude <noreply@anthropic.com>
…on't hang (gastownhall#3916)

## Symptom

Codex/GPT raises an "Approaching rate limits — Switch to a cheaper
model?" modal **mid-session**. When it appears during work, the session
hangs: the pane is input-blocked (neither an idle prompt nor a busy
indicator), `WaitForIdle` can't confirm idle, idle-kill may not fire,
and the agent sits forever. Observed live as a `tester` session stuck
~35 min after its QA work completed (committed + pushed) — an operator
had to key the modal by hand to free it.

## Root cause

gc's dialog handling (`AcceptStartupDialogs` / `DismissKnownDialogs`,
`internal/runtime/dialog.go`) — which already includes a rate-limit
dismisser — runs **only at startup / early-session** (`session/chat.go`
gates it behind codex `needsDeferredStartupDialogVerification`). Once
startup dialogs are verified it is never re-run, so a rate-limit modal
that appears mid-session is never dismissed.

The existing recognizer `ContainsRateLimitDialog` matches the broad
substring `"rate limit"`. That is safe at startup (the pane is a known
dialog or fresh prompt), but running it mid-session over arbitrary panes
would **false-match ordinary agent output** that merely mentions rate
limits and fire stray `Down`/`Enter` keystrokes into live work.

## Fix

- **High-confidence matcher** `ContainsModelSwitchModal` (`dialog.go`):
requires **both** the switch offer (`"Switch to "`) and the
keep-current-model option (`"Keep current model"`), so it fires only on
the actual modal — never on prose that mentions rate limits.
- **Mid-session dismissal** (`Tmux.DismissModelSwitchModalIfPresent`,
split from a pure, injectable `dismissModelSwitchModal` for
testability): selects **"Keep current model"** (`Down` off the default
"Switch", then `Enter`) — no downgrade, no spend change. No-op when the
modal is absent.
- **Hook** (`Provider.Nudge`): when the pre-send `WaitForIdle` fails
(session not idle), dismiss a blocking model-switch modal before
delivering. On a genuinely busy pane the high-confidence matcher does
not fire → no keystrokes → no regression. Every pool session self-polls
"check for assigned work", so a modal clears on the next wake instead of
hanging.

## Blast radius

- Adds one extra pane capture per nudge to a non-idle session; sends
keys only on a high-confidence modal match. Safe-by-construction: it
cannot type into a working pane.
- The keep-current-model dismissal is the safe default (matches the
operator's own manual choice when this was first hit); it does not
select "never show again", so a fresh modal can recur — but the hook
re-dismisses it on the next wake.

## Testing

- **Unit** (`internal/runtime/dialog_model_switch_test.go`): matcher
matches the real modal; does NOT match work output mentioning rate
limits, nor a bare "switch to" phrase.
- **Unit** (`internal/runtime/tmux/dialog_model_switch_test.go`):
dismisser sends `Down`+`Enter` on the modal; sends nothing on a working
pane (the safety property).
- **Integration, real tmux** (`//go:build integration`): a fake agent
showing the modal is dismissed end-to-end (keeps current model).
- `go vet` clean · `golangci-lint run ./internal/runtime/...` **0
issues** · `gofmt` clean · full `internal/runtime` +
`internal/runtime/tmux` unit suites pass · `go build ./cmd/gc/` ok.

## Follow-up (separate)

Reaper / idle-kill resilience: reap a session that remains input-blocked
after dismissal so it cannot hang indefinitely even if a future modal
isn't recognized. Left out of this PR to keep it focused.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…gastownhall#3917)

Fixes gastownhall#3897.

Supervisor log rotation could race with a restarting supervisor: a
second instance starting while the first rotated the log left a
truncated or doubly-rotated file, and an unbounded archive set could
grow without limit.

## Changes
- Rotate the supervisor log under the single-instance lock. The
prior-instance stat now runs ahead of lock acquisition, and a losing
racer exits before touching the log, so only the lock holder rotates.
- Bound the archive set to 1GiB, dropping the oldest tail with a warning
rather than growing unbounded.
- Sweep leftover staging files from an interrupted prior rotation.

## Tests
- Rotation suite 19/19, including a new concurrency regression for
racing starts, the 1GiB archive bound with dropped-tail warning, and the
staging-leftover sweep. Tests ship in the same commit.

## Verification
`make build`, `go vet`, and `make check` all pass.

---------

Co-authored-by: sjarmak <sjarmak@users.noreply.github.com>
julianknutsen and others added 23 commits July 7, 2026 02:41
…ND (gastownhall#4015)

## Summary

`bd` authenticates to a hosted beads-gateway by running the helper named
in `BEADS_DOLT_CREDENTIAL_COMMAND`. That key contains `CREDENTIAL`, so
`execenv.FilterInherited` strips it from every gc-spawned `bd`
subprocess and agent session, and the gateway then rejects the
static/root fallback with MySQL `Error 1045 (28000): access denied`.

`preserveHostedBeadsCredentialEnv` already re-adds the key on the
slice-merge env paths (`overlayEnvEntries` / `mergeRuntimeEnv`), but
only when it is already present in the pre-filter environ and only on
those paths. Two residual gaps remain:

- the agent session env is projected from the `mirrorBeadsDoltEnv` map,
which does not carry the ambient value; and
- a controller that exports the helper under only the non-sensitive
`GC_DOLT_CRED_CMD` (which survives filtering) has nothing for that pass
to preserve.

This mirrors `GC_DOLT_CRED_CMD` into `BEADS_DOLT_CREDENTIAL_COMMAND`
inside `mirrorBeadsDoltEnv` (map value wins, else the ambient value of
either key), so gc-spawned `bd` authenticates the same way the
in-process native store does. It mirrors the existing `GC_DOLT_*` ->
`BEADS_DOLT_*` handling in the same function.

## Testing

- [x] `go test ./cmd/gc/ -run
TestMirrorBeadsDoltEnvPropagatesCredentialCommand` (new; 5 subtests)
- [x] `go vet ./cmd/gc/`
- [ ] `make check` (full pre-push shard suite ran; the only failure was
`TestMuxSource_YieldsAndPicksUpNewCity` in `internal/eventfeed`, a
pre-existing timing flake unrelated to this change — passes in
isolation)
- [ ] `make check-docs` — n/a (no docs/nav/links changed)

## Checklist

- [x] Linked an issue, or explained why one is not needed — no issue; a
small, self-contained hosted-gateway credential-projection fix
- [x] Added or updated tests for behavior changes
- [ ] Updated docs for user-facing changes — n/a (internal env
projection)
- [ ] Called out breaking changes or migration notes — none

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…nes (gastownhall#4016)

## Summary

`gc`'s pack fetch clones over HTTPS through the `gitcred` credential
system, which authenticates only when a `credentials.toml` rule (or the
`$GC_GIT_CREDENTIAL_COMMAND` command layer) matches. A private
`github.com` pack therefore fails with `fatal: could not read Username
for 'https://github.com'` unless the operator hand-writes a
`credentials.toml` — even when a GitHub token is already present in the
environment (the ubiquitous CI / `gh` CLI convention).

This adds a built-in default credential layer, appended **last** so any
explicit file rule (or the command layer) still wins, that authenticates
`https://github.com` clones from `GITHUB_TOKEN` (falling back to
`GH_TOKEN`). It is:

- **scoped strictly to the `github.com` host** — the token is never
offered to any other, possibly attacker-supplied, pack source; and
- **inert without a token** — byte-identical to today when neither env
var is set (no rule, no injection).

The rule resolves through the existing `TokenEnv` path, so no new
secret-handling code is introduced.

## Testing

- [x] `go test ./internal/gitcred/` (added
`TestInjectionGitHubDefault*`: matches with a token, prefers
`GITHUB_TOKEN` over `GH_TOKEN`, only matches the `github.com` host, and
stays inert without a token)
- [x] `go test ./internal/gitcred/ ./internal/packman/` with
`GITHUB_TOKEN`/`GH_TOKEN` set — confirms existing
byte-identical/zero-injection tests still pass (the `Load()`-based tests
now clear the ambient token env)
- [x] `go vet ./internal/gitcred/`
- [ ] `make check` (full pre-push shard suite ran; the only failure was
`TestMuxSource_YieldsAndPicksUpNewCity` in `internal/eventfeed`, a
pre-existing timing flake unrelated to this change — passes in
isolation)

## Checklist

- [x] Linked an issue, or explained why one is not needed — no issue;
closes a private-pack clone-auth gap
- [x] Added or updated tests for behavior changes
- [ ] Updated docs for user-facing changes — the credentials system
already documents explicit rules; this is a zero-config default
consulted only when a github.com token is present
- [x] Called out breaking changes or migration notes — none; inert
unless GITHUB_TOKEN/GH_TOKEN is set, and explicit rules always take
precedence

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…er client (gastownhall#3942)

## What

The event-first headline (stacked on gastownhall#3941): the server **pushes** the
whole `FormulaRunDetail` over SSE, so the client renders the complete
view from the first frame — **no separate call** — and stops
full-refetching the whole graph on every bead nudge.

**Server** — `GET /api/city/{c}/runs/{id}/detail/stream` (new
`rundetail_stream.go`), a plain route on the sanctioned non-Huma plane
(GET passes the mutation guard). Deliberately **NOT**
`/v0/events/stream` (which pays a per-event beads-DB Get) and **NOT** a
bus event type (which would persist every frame into `events.jsonl`).
- A subscriber registry on `cityRunTailer` (per-connection
buffered(1)-coalescing notify channel, a dedicated `subMu` distinct from
the hot fold lock — and notify fires *after* the `t.mu` publish, so the
two locks never nest). `build()` — the single change-gated publish point
— bumps a generation and non-blockingly wakes every subscriber.
- Each connection: precheck (422/404/503, mirroring the GET) → register
→ **re-read the current fold and send that as the first frame** (closes
a missed-frame race where a build between precheck and subscribe would
pin the client on a stale generation) → loop on `{notify, 25s heartbeat,
ctx.Done}`, rebuilding via the P2 memoized `detail()` and sending a
frame **only if the marshaled bytes changed** (byte-dedup — an
unrelated-run event that didn't move this run's bytes sends nothing).
Reconnect needs no replay (frames are whole snapshots); disconnect
deregisters (goroutine-leak tested).

**Client** — a pure renderer: `useFormulaRunDetailStream` opens the
EventSource, decodes each frame through the **same
`decodeFormulaRunDetail` validator the GET uses**, and updates the
rendered detail with zero refetch; the streamed frame is tagged by cache
key so a run switch never shows the prior run's detail. The initial GET
stays first-paint + the fallback (EventSource-absent → the nudge
refreshes detail; transient stream errors self-heal via native
reconnect). `FormulaRunDetail.tsx` drops the detail nudge re-GET (the
run-diff refresh stays for P5).

## Invariants

- **No OpenAPI change** — `internal/api/openapi.json` +
`docs/reference/schema/openapi.json` byte-identical (sha verified);
`TestOpenAPISpecInSync` passes. **No bus event** — no `RegisterPayload`;
`TestEveryKnownEventTypeHasRegisteredPayload` passes. The frame body is
the same struct the GET serves, extended into `wire_contract_test`
(`TestWireContractRunDetailStreamFrame`).

## Tests / gates

Go (httptest SSE, `-race`): connect→one frame `id==lastSeq`; a
run-member event→one higher-id frame; an unrelated-run event→**no**
frame (byte-dedup); reconnect→fresh snapshot; disconnect→subscriber
count returns to 0 (no leak); 404/422 precheck; heartbeat; and a
**race-guard** test proving the first frame reflects a build racing the
connect (fails without the fix). Client (vitest, FakeEventSource):
zero-refetch push (GET stays at 1 call), stream-error/no-EventSource
fallback, run-switch no-leak, unmount close,
manual-Refresh-renders-fresh-GET. `go test -race
./internal/api/dashboardbff/...`, `make dashboard-check`, frontend
`vitest` (776), shared `tsx --test` (97) — all green.

Adversarially reviewed by two reviewers (server concurrency + client
integration): the two-mutex discipline / coalescing / teardown and the
run-switch stale-leakage path are sound; 4 findings (1 HIGH missed-frame
race, 1 MEDIUM no-EventSource fallback, 2 minor) all fixed with
regression tests.

## Follow-up (noted, not blocking)

Session-link freshness on a fully-idle run (no bead events) is
bounded-stale until the next event/manual refresh — the P1 design's
"eager sessions epoch-bump on tail-observed `session.*` lines" would
tighten it. The run-diff decouple is P5.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…accurate restart (ga-ceq) (gastownhall#3310)

Fail loudly (exit 3) on a duplicate gc supervisor API-port collision, gated on a /health liveness probe (supervisorRespondingGCSupervisor) so a foreign/dead binder self-heals via exit-1 instead of a sticky outage. Restart-suppression message is platform-conditioned (supervisorRuntimeGOOS) so macOS no longer claims 'without restart'. macOS/launchd follow-up: gc-s53wv.

Maintainer take-the-good on top of Rome-1's original fix; both review MAJORs resolved, Codex adversarial re-run APPROVE (0 blockers).

Co-authored-by: Rome Thorstenson <romethorstenson@gmail.com>
…r (dedup) (gastownhall#4026)

## What this does

Lands **S12** — a behavior-preserving dedup in `internal/session`:

- Extracts `canonicalLifecycleState` for the lifecycle callers (removes
the
  duplicated legacy-state canonicalization).
- Delegates `ListFullFromBeads` filtering to the shared
`sessionMatchesFilters`
  helper instead of re-implementing the filter inline.

Net ~−16 production lines, no behavior change; already Fable-reviewed
clean.

## Gates

- `go build ./internal/session ./cmd/gc` — pass
- `go vet ./internal/session` — pass
- `go test ./internal/session` — pass

## Review verdict

LAND — fast-track (no `status/needs-review-auto` label): already
Fable-reviewed,
behavior-preserving −16-line dedup. Per the simplification walkthrough
decision, merge after CI green.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ dead-branch removal) (gastownhall#4027)

## What this does

Lands **S13** — collapses the two ~105-line copy-pasted
formula-attachment
pipelines (`slingOnFormula` / `slingDefaultFormula`) into one
`attachFormulaToBead`; both become thin wrappers that differ only in
formula
name, sling method, and error-label prefix. Graph-vs-legacy behavior is
byte-identical per path (6 closures → 2). Kills the gastownhall#1053
duplicate-molecule
drift vector; a new `TestAttachFormulaToBeadEntryShapes` table test pins
the
success method + FormulaName and error-label prefix for both entry
shapes.

**Folded follow-up** (reviewer-flagged, commit 2): the `isGraph==true`
case
returns early, so the code below it is the legacy non-graph region where
`isGraph` is always false. The `if isGraph && opts.Force {
checkAttachments =
CheckNoMoleculeChildrenAllowLiveWorkflow }` branch there could never
fire —
inlined the always-taken `CheckNoMoleculeChildren` and dropped the dead
branch. No behavior change.

## Gates

- `go build ./internal/sling ./cmd/gc` — pass
- `go vet ./internal/sling` — pass
- `go test ./internal/sling` — pass

## Review verdict

LAND — fast-track (no `status/needs-review-auto` label),
behavior-preserving
dedup + reviewer-flagged dead-branch removal, per the simplification
walkthrough. Merge after CI green; delete branch on merge.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…wnhall#4028)

## What this does

Lands **S37** — extracts a single scope-close/scope-abort reconciler in
`internal/dispatch`: byte-matched behavior-preserving dedup (3 close
paths →
1, 2 abort paths → 1, 3 fanout epilogues → one-liners), independently
verified. New `runtime_test.go` coverage pins the unified behavior.

## Gates

- `go build ./internal/dispatch ./cmd/gc` — pass
- `go vet ./internal/dispatch` — pass
- `go test ./internal/dispatch` — pass

## Review verdict

LAND — fast-track (no `status/needs-review-auto` label), byte-matched
behavior-preserving dedup, per the simplification walkthrough. Merge
after CI
green; delete branch on merge.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gastownhall#4030)

## What this does

Lands **S04** — moves the ~700-line bd/jq work-query shell-codegen block
out
of `internal/config/config.go` into a new same-package file
`internal/config/workquery.go`. Verbatim move, zero behavior risk
(navigability only); the two files stay in package `config`.

## Gates

- `go build ./internal/config ./cmd/gc` — pass
- `go vet ./internal/config` — pass
- `go test ./internal/config` — pass

## Review verdict

LAND — fast-track (no `status/needs-review-auto` label), verbatim
same-package
file move, per the simplification walkthrough. Merge after CI green;
delete
branch on merge.

Future follow-ups deliberately **not** folded: approach-b table-driven
`Effective*Query` collapse (the real win); rehome the stray
session-capacity
Agent helpers out of `workquery.go`.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…astownhall#4033)

## What this does

Lands **S09** — kills stringly-typed session/dispatch metadata by
introducing a
typed `SleepReason` vocabulary (`internal/session/sleep_reason.go`) and
typed
`beadmeta` dispatch/attachment keys, then routing the call sites through
them.
Byte-identical typed-constant unification; new `sleep_reason_test.go`.

Touches `internal/beadmeta`, `internal/session`, `internal/sling`.

## Gates

- `go build ./internal/beadmeta ./internal/session ./internal/sling
./cmd/gc` — pass
- `go vet ./internal/beadmeta ./internal/session ./internal/sling` —
pass
- `go test ./internal/beadmeta ./internal/session ./internal/sling` —
pass

## Review verdict

LAND — fast-track (no `status/needs-review-auto` label), byte-identical
typed-constant unification, per the simplification walkthrough. Merge
after CI
green; delete branch on merge.

Follow-ups deliberately not folded: migrate `cmd/gc` parallel
sleep-reason
literals (~6 files); the deferred Part-1 table-driven Info codec.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ckField + durationOr) (gastownhall#4031)

## What this does

Lands **S06** — collapses the repetitive config accessor boilerplate in
`internal/config` behind a generic `cachedPackField` helper plus a
`durationOr`
helper. Real reduction (~250 production lines deleted, net −165),
behavior-preserving; new `pack_test.go` coverage.

## Gates

- `go build ./internal/config ./cmd/gc` — pass
- `go vet ./internal/config` — pass
- `go test ./internal/config` — pass

## Review verdict

LAND — fast-track (no `status/needs-review-auto` label), real
behavior-preserving
reduction, per the simplification walkthrough. Merge after CI green;
delete
branch on merge.

Trivial nit deliberately not folded: move the stranded
`SetupTimeoutDuration`
doc comment onto its method (cosmetic).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ace (gastownhall#4036)

## What this does

Lands **S26** — collapses the trace double-record API to one typed
surface.
Introduces typed `TraceSiteCode` / `TraceReasonCode` /
`TraceOutcomeCode` and
routes the trace call sites through them, deleting the string-normalize
allowlists (and a whole file). Real −151-line reduction in the
diagnostic trace
subsystem; low blast radius, behavior-preserving.

Touches `cmd/gc` trace subsystem (`session_reconciler_trace_*`,
`session_reconciler.go`, `build_desired_state.go`, `city_runtime.go`,
`session_wake.go`, `pool_desired_state.go`, etc.).

## Gates

- `go build ./cmd/gc` — pass
- `go vet ./cmd/gc` — pass
- `go test ./cmd/gc` trace + reconciler + session (`-run`) — pass

## Review verdict

LAND — fast-track (no `status/needs-review-auto` label),
diagnostic-layer typed
reduction, behavior-preserving, per the simplification walkthrough.
Merge after
CI green; delete branch on merge.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nhall#4039)

## What this does

Lands **S18** — deduplicates the triple-pasted routed-state warning
block in
`CheckBeadStateWithOptions` (`internal/sling/sling_attachment.go`).
Byte-identical dedup (−24 net), no wire/event surface; a new
`sling_attachment_test.go` table test locks the behavior across the
three entry
shapes.

## Gates

- `go build ./internal/sling ./cmd/gc` — pass
- `go vet ./internal/sling` — pass
- `golangci-lint ./internal/sling/...` — 0 issues
- `go test ./internal/sling` — pass

## Review verdict

LAND — fast-track (no `status/needs-review-auto` label), byte-identical
dedup,
per the simplification walkthrough. Merge after CI green; delete branch
on merge.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## What this does

Lands **S33** — flattens `internal/convergence/reconcile.go` error
plumbing to a
uniform `(action, error)` shape: a typed `ReconcileAction` set + a
single wrap
site (net −87). Zero behavior change.

## Gates

- `go build ./internal/convergence ./cmd/gc` — pass
- `go vet ./internal/convergence` — pass
- `golangci-lint ./internal/convergence/...` — 0 issues
- `go test ./internal/convergence` — pass

## Review verdict

LAND — fast-track (no `status/needs-review-auto` label),
zero-behavior-change
error-plumbing flatten, per the simplification walkthrough. Merge after
CI
green; delete branch on merge.

Note: same `internal/convergence/reconcile.go` cluster as S31 (gastownhall#4040)
and S30 —
whichever lands second will be rebased/merged with conflict resolution.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ring) (gastownhall#4042)

## What this does

Lands **S30** — centralizes the convergence marker-last write ordering
(the
crash-safety contract) into a structural commit-point helper
(`internal/convergence`, handler.go + manual.go). Zero interface change,
behavior-preserving; new `handler_test.go` coverage.

## Gates

- `go build ./internal/convergence ./cmd/gc` — pass
- `go vet ./internal/convergence` — pass
- `golangci-lint ./internal/convergence/...` — 0 issues
- `go test ./internal/convergence` — pass

## Review verdict

LAND — fast-track (no `status/needs-review-auto` label),
crash-safety-ordering
centralization, behavior-preserving, per the simplification walkthrough.
Merge
after CI green; delete branch on merge.

Note: same `internal/convergence` cluster as S31 (gastownhall#4040, handler/manual)
and
S33 (gastownhall#4041, reconcile) — whichever lands after the first will be merged
with
conflict resolution. Doc-overclaim nit ("type-impossible" →
convention-enforced)
left for auto-review.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…astownhall#4040)

## What this does

Lands **S31** — replaces six duplicate child-projection loops (+2
helpers) in
`internal/convergence` with one pure `childStats()` scan
(`childstats.go`).
Behavior-preserving (filter-drift verbatim, fetch consolidation verified
safe)
and a perf win: 3–4 `Children()` round-trips per transition collapse to
1. New
`childstats_test.go` coverage.

## Gates

- `go build ./internal/convergence ./cmd/gc` — pass
- `go vet ./internal/convergence` — pass
- `golangci-lint ./internal/convergence/...` — 0 issues
- `go test ./internal/convergence` — pass

## Review verdict

LAND — fast-track (no `status/needs-review-auto` label),
behavior-preserving
dedup + perf win, per the simplification walkthrough. Merge after CI
green;
delete branch on merge.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ll#4054)

Adds a user-facing advisory to `SECURITY.md` about the active
malware-spam campaign: burner accounts reply to fresh issues with an
attached `*_fix.zip` (hosted on GitHub's `user-attachments` CDN) that
claims to be a fix. The note tells users official builds come only from
Releases, that `user-attachments/files/...` links are unvetted, and how
to report the comment + attachment.

Part of the org-wide response (interaction limits + moderation).
Docs-only.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…wnhall#2403 taxonomy (gastownhall#4043)

## What this does

Lands **S34** (approach **b**, wholesale re-derive) — rebuilds
convergence
scopes on reload and deletes the whole gastownhall#2403 detect-and-punt staleness
taxonomy
(+ 4 restart-controller branches). `convergenceScopeForRig` becomes a
map lookup
with gastownhall#2357 fail-loud. Net −43. Touches `cmd/gc/convergence_tick.go` +
`city_runtime.go` (distinct from the S31/S33/S30 `internal/convergence`
files,
so low conflict risk).

## Gates

- `go build ./cmd/gc` — pass
- `go vet ./cmd/gc` — pass
- `golangci-lint ./cmd/gc/...` — 0 issues
- `go test ./cmd/gc` convergence/reload/scope (`-run`) — pass

## Review verdict

LAND via label PR (`status/needs-review-auto`) — deletes a whole
taxonomy +
restart branches; auto-review pass per the simplification walkthrough.

Optional nits left for auto-review (not folded): add a rebound-rig test;
a
one-line comment on the nil-activeIndex window.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e Diff tab is open) (gastownhall#3943)

## What

The last phase of the fast-run-detail effort (stacked on gastownhall#3942). The
run-diff (`POST /runs/{id}/diff` — a **git subprocess chain**) was
refetched on **every** bead/session nudge regardless of which run-view
tab was open (~5–7 git execs per coalesce window per open run view). Now
the event-driven diff refresh is **gated on the Diff tab being active**:

- `FormulaRunTabs` lifts its active-tab state via optional controlled
props (`activeTab`/`onActiveTabChange`) — backward-compatible (callers
passing neither keep internal state).
- `FormulaRunDetail` owns `activeTab`; the nudge refreshes the diff
**only when the Diff tab is active**
(`runDetailNudgeRefresh(streamActive, diffTabActive, …)`), and a
ref-guarded `useEffect` refreshes once on a genuine hidden→visible
transition (StrictMode-safe — never double-fires, never fires on first
paint). Manual Refresh still forces a fresh diff. The P4 detail-stream
behavior is untouched.

**Honest scope:** the `refresh=false/true` lane (`cheapRefresh` vs
`refresh`) is a **forward-compat client contract** for a future
server-side diff cache — today the diff endpoint has no cache and
`runQuery` drops the flag, so both lanes issue an identical git read on
the wire. **The live win is the tab-gating** (zero git execs while the
Diff tab is hidden) plus the existing `useGcEventRefresh` coalescing (≤1
per window when visible). The comments/test names were corrected to say
exactly this (the earlier "TTL absorbs the burst" framing was
aspirational).

Also folds in a small hygiene fix: **5 pre-existing `typecheck:test`
errors** (3 fixtures missing `progress.terminal` from gastownhall#3941, 2 type
issues in gastownhall#3942's stream test) that neither `make dashboard-check` nor
CI catches (both run `npm run typecheck` on `src`, not `typecheck:test`;
vitest uses esbuild). `typecheck:test` is now clean.

## Tests / gates

New vitest: hidden Diff tab → **0** `/diff` POSTs on a nudge
(failed-first); switch-to-Diff → exactly one fresh refetch; a burst → ≤1
diff request per window; manual Refresh still fires; `cheapRefresh` vs
`refresh` param routing. `make dashboard-check`, frontend `vitest`
(783), shared `tsx --test` (97), `typecheck:test` (now clean), `go
build`/`go vet` — all green. **No server/OpenAPI change** (openapi.json
byte-unchanged).

Adversarially reviewed: tab-gating correct + StrictMode-safe,
P4/manual-Refresh unregressed, the controlled-prop lift
backward-compatible; the two LOW findings (misleading TTL narrative;
independently-optional controlled props) addressed / documented.

## Follow-up (noted)

`FormulaRunTabs`' `activeTab`/`onActiveTabChange` are independently
optional (a caller passing only one silently desyncs); the doc-comment
states "both or neither" — a paired-prop/discriminated-union type would
enforce it. No current caller trips it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ll#4034)

Stage 1 of a staged migration replacing edge-triggered session
reconciliation with level-triggered convergence — the headline cure for
gastownhall#3872 (edge-triggered events dropped on the floor leave sessions
permanently unreconciled; a level-triggered convergence loop re-derives
desired state each pass, so missed edges self-heal).

Spike-grade: staged for review, not auto-merge. Gates green.
Fable-reviewed, behavior-preserved.

Spec:
/data/projects/gascity/.claude/worktrees/simplification/engdocs/simplification/specs/S19-level-triggered-convergence-spec.md

NOTE: spike/staged for review, not auto-merge.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…thority guard (gastownhall#4011)

## Why

**gastownhall/beads#3734** ("fix(close): refuse silent close on
actor/assignee mismatch", commit `f1db5a9`, **currently unreleased** —
after v1.1.0) makes `bd close <id>` **refuse a cross-actor close** — one
where the bead's `assignee` differs from the acting identity — unless
the bead is unassigned, `actor == assignee`, or `--force` is passed. It
closes a real silent-data-loss hole (an actor whose claim was
overwritten could `bd close` and see "✓ Closed" with no authority).

The contract radar (`gc HEAD × bd main HEAD`) caught this the day it
merged. This PR prepares gascity so the guard is safe to inherit at the
next `BD_VERSION` bump.

Related: gascity#4007 bumped the bundled bd CLI to v1.1.0 (which
predates the guard).

## What the audit found

A deep multi-agent audit swept **all 226 bead-close sites** across Go,
pack formulas, shell/asset scripts, prompts, docs, and tests,
classifying each as self-close (parity), SDK-forced, or genuine
cross-actor. The vast majority are already safe:

- **SDK `BdStore` already force-closes** (`internal/beads/bdstore.go`
`bdCloseArgs` → `{"close","--force","--json"}`) — every reconciler /
molecule / convoy / order / nudge / mail / API close routes through it.
- **Agents close their own claimed beads** under `actor == assignee`
parity (`gc hook --claim` sets `assignee = BEADS_ACTOR = session name`).
Pack formulas (`mol-polecat-*`), worker prompts, and fake test agents
are all self-closes and **must stay bare** — forcing them would defeat
the guard.

Only these **genuine cross-actor closes ran bare** and are hardened
here:

| Site | Why it's cross-actor |
|---|---|
| `reaper.sh` step-5 stale auto-close | reaps `in_progress`
(agent-assigned) beads while running as `order:reaper` |
| `contrib/beads-scripts/gc-beads-k8s` `close` | exec `Store.Close`
delegate; runs under pod/controller actor (`BEADS_ACTOR` stripped) |
| `contrib/beads-scripts/gc-beads-br` `close` | same delegate, br
backend |
| `test/acceptance` `TestBdWorkflow` | closes a bead assigned to
`polecat-1` while driving bd as a different actor — the exact case the
radar flagged |

The reaper fix is **surgical, not blanket**: `close_city_issue()` is
parameterized so only the stale caller forces; the workflow-root and
ttl-nudge reaps target **unassigned** beads and stay bare, so the guard
keeps protecting them against overriding a concurrent re-claim. The 5
reaper stale-close argv assertions are updated in lockstep.

## Safety / compatibility

- `--force` is a valid `bd` **and** `br` flag **today** (`br close
--help` lists `-f, --force`; the SDK already emits it), so every edit is
**harmless on the pinned v1.1.0** and forward-compatible when the guard
lands.
- No self-close gains `--force` (verified by an adversarial pass over
each candidate).

## Verification

- `go test ./examples/gastown -run TestReaper` — all pass (exercises the
real `reaper.sh` + fake-bd shim, incl. the forced stale close)
- `TestBdWorkflow` passes with `--force` (bd v1.1.0 on PATH)
- `bash -n` clean on all three scripts; `go vet` / lint clean

## Deliberately out of scope (follow-ups)

- Optional doc notes (AGENTS.md, `gc-work` SKILL, tutorials) that
cross-actor/reaper closes use `--force`.
- User-supplied `exec:<script>` beads backends other than k8s/br would
need the same one-line `--force` on their `close` op.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aunch-config derivation) (gastownhall#4038)

Routes the gastownhall#3872 drift-relaunch path through buildPreparedStart so
launch config is derived once, fixing the drift-relaunch misconfig and
collapsing 2 launch-config derivations into 1.

- Fixes the drift-relaunch misconfiguration from gastownhall#3872
- Collapses two launch-config derivations to one (buildPreparedStart is
now the single source)
- Quality gates green
- Fable-reviewed, behavior-preserved
- Spec:
/data/projects/gascity/.claude/worktrees/simplification/engdocs/simplification/specs/S36-drift-relaunch-preparedstart-spec.md

NOTE: spike/staged for review, not auto-merge.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ll#3949)

## What

Closes the gate gap surfaced during the run-detail review: `make
dashboard-check` and the CI `dashboard` job ran `npm run typecheck`
(`tsc --noEmit` on **src**) but never `typecheck:test` (`tsc --noEmit -p
tsconfig.test.json`). Vitest uses esbuild and does no type-checking, so
**test-file type errors slipped through both gates** — that's how three
fixtures missing `FormulaRunProgress.terminal` (from gastownhall#3941) and two type
issues in the stream test (from gastownhall#3942) reached shipped PRs.

Adds `typecheck:test` to both gates, right after the existing typecheck
(which builds the shared package the frontend test tsconfig resolves
against). The whole frontend test suite is currently type-clean, so the
gate is green.

## Dependency / merge order

Stacked on gastownhall#3943, which fixes the five pre-existing `typecheck:test`
errors. This gate must land **after** those fixes are on `main`
(guaranteed by the stack order) — otherwise `main` would be red. At
every bottom-up merge step main stays green (the fixes land in gastownhall#3943,
the gate lands here on top).

## Verify

`make dashboard-check` → exit 0 (now runs `typecheck:test`); `npm run
--workspace gas-city-dashboard-frontend typecheck:test` → exit 0. The
gate provably catches test-file type errors — it's the same
`typecheck:test` that flagged the five that were fixed.

Note: only the frontend workspace has a separate test tsconfig; shared's
`typecheck` already covers its sources.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resync Voxist/main with gastownhall/gascity. 12 conflicts resolved (status
subsystem: warm StatusView + SWR vs upstream cliSessionStore routing; provider-
health trace → typed RecordDecision API). Fork status-perf + doctor checks +
degrade-not-fatal + boot-priming preserved.

vp-m84x (#75) status-read routing is SUBSUMED by upstream's cliSessionStore
seam — confirmed, no reopen needed. Added TraceOutcomeChainWalk for the fork's
failover path (upstream RespawnSkipped was semantically wrong there).
bourgois added 2 commits July 9, 2026 20:39
…g unset

The resync wired statusSnapshotTimeout(cfg) to StatusSnapshotTimeoutDuration(),
which applies a hardcoded 3s default for a non-nil but unconfigured City —
skipping the package-var fallback that TestLoadStatusSessionSnapshotTimesOut
overrides. Fall back to statusSessionSnapshotTimeout when the [daemon]
status_snapshot_timeout field is unset (behavior-preserving in prod: the package
default is also 3s).
bourgois added a commit that referenced this pull request Jul 10, 2026
Trivy 'Image vulnerabilities' scan on the resync image flagged one HIGH:
golang.org/x/crypto/ssh CVE-2026-39831 (fixed in v0.52.0). The scan runs
--severity HIGH,CRITICAL --exit-code 1, so this single finding reddens the
gate for #79.

go get golang.org/x/crypto@v0.52.0 + go mod tidy; the coordinated
golang.org/x/{net,term,text,tools,telemetry,mod} bumps are the minimal set
go resolves to satisfy x/crypto v0.52.0. Full module builds; vet clean.
…ge scan

The Container Scan 'Image vulnerabilities' gate reddened on one HIGH:
CVE-2026-39831 (golang.org/x/crypto/ssh) in usr/local/bin/dolt (Dolt v2.1.7
bundles x/crypto v0.48.0). This CVE is part of the same x/crypto/ssh series
(39827-39836) already waived here, but 39831 was omitted from both the
dolt-only and bd+gc blocks. Because the scan halts at the first failing
image (set -e), waiving dolt alone would unmask the same CVE on the gc/bd
images next, so it is added to both blocks mirroring its siblings.

This is a pinned-upstream-binary waiver, not a gc source issue: dolt v2.1.7
and beads v1.1.0 embed the vulnerable x/crypto; gc is indirect v0.49.0.
Remove per each path's statement once upstream rebuilds (dolt tracked in
ga-frh27v) / gc bumps golang.org/x/crypto >= 0.52.0.
@bourgois
bourgois force-pushed the resync/upstream-20260709 branch from 1cc8e36 to 0cb8608 Compare July 10, 2026 10:33
@bourgois
bourgois merged commit 77916fc into main Jul 10, 2026
101 of 102 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.