Resync fork/main with upstream (2026-07-15, 237 commits) - #83
Merged
Conversation
…remaining outcomes) (gastownhall#4061) Follow-on to merged S26 (gastownhall#4036): closes the last stringly trace outcomes by finishing the typed trace surface (pool-cap rejection + remaining outcomes). Part of gastownhall#3789. Gates green; Fable-reviewed, behavior-preserved. Spec at engdocs/simplification/specs/S26b-finish-typed-trace-spec.md. Spike/staged for review, not auto-merge. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on (gastownhall#4062) Follow-on to merged S09 (gastownhall#4033). Part 1 introduces a table-driven Info codec (parity-gated) and migrates cmd/gc sleep-reason literals to the codec. Part of gastownhall#3789. - Gates green (fast suite, vet, pre-commit) - Fable-reviewed, behavior-preserved - Spec: engdocs/simplification/specs/S09b-info-codec-spec.md 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>
… sessions" (gastownhall#4082) ## Why An unreachable tmux server is an observation failure, not the fact "no sessions exist." Today `tmuxFetcher.FetchState` converts `ErrNoServer` into an empty success, so `StateCache.refresh()` overwrites its last-known-good and every session instantly reads not-running. A brief server blip (a supervisor restart, a transient socket stall) then drives the reconciler to drain and close healthy pool slots. This is the source-level fix for that class, mirroring the existing `storeQueryPartial` discipline on the store side. It is one of three independent pieces split out of a polecat-reliability audit; the other two (a warm-worker wake and an idempotent-sling `--nudge` fix) are separate PRs. ## What changed - `internal/runtime/tmux/state_cache.go`: `FetchState` returns `runtime.ErrRuntimeUnavailable` (wrapping the cause) on an unreachable server instead of an empty success, so `refresh()` preserves last-known-good until the existing `staleTTL` cliff. Genuine session ends still evict immediately via `Stop()`/`EvictSession`, so they are not masked. The wrapped error still satisfies `isNoServerError`, so the existing `ErrNoServer` absorbers are unaffected. - `internal/runtime/runtime.go`: adds the `ErrRuntimeUnavailable` sentinel with a doc comment distinguishing it from the existing `PartialListError` (single-observation total failure vs. multi-backend partial-but-usable). - `engdocs/design/runtime-partial-discipline.md`: records the design, and names the reconciler-facing `ListRunning` sites (`city_runtime.go` on_death, provider-swap, shutdown) that this fix does not yet cover, with the follow-up path: emit the existing `PartialListError` from `ListRunning`/`ListSessions` on `ErrNoServer` to activate the four `IsPartialListError` guards those sites already have. ## Bounded behavior change An externally-killed last session is now reported running from last-known-good for up to `staleTTL` (~30s) before the cliff clears it. This is the intended trade: a bounded cleanup delay instead of draining every slot on a blip. ## Test plan - `go build ./...`, `go vet ./internal/runtime/...`: clean - `go test ./internal/runtime/... ./internal/runtime/tmux/`: pass, including new `TestTmuxFetcher_NoServerMapsToRuntimeUnavailable` and `TestStateCache_NoServerRefreshPreservesLastKnownGood` --------- Co-authored-by: sjarmak <sjarmak@users.noreply.github.com>
## Why
Re-slinging a bead with `--nudge` silently drops the nudge. When a bead
is already routed to the target, `preflight` short-circuits as
idempotent and returns before `result.NudgeAgent` is ever set, and the
CLI only delivers a nudge when that field is set. So the natural
operator repair for a warm worker that missed its wake ("sling it again,
with `--nudge`") is a no-op, and the bead stays unclaimed. This composes
with the warm-worker wake gap into the "bead sits unclaimed forever"
symptom.
Split out of a polecat-reliability audit; the runtime-partial fix and
the warm-worker wake are separate PRs.
## What changed
- `internal/sling/sling_core.go`: on an idempotent sling result, when
`opts.Nudge` is set and it is not a dry run, still set the nudge signal
so the CLI delivers the wake. The claim path is CAS-safe, so a redundant
nudge is harmless; this makes "re-sling with `--nudge`" a reliable
repair verb.
## Coordination note
Touches `internal/sling/sling_core.go`, also touched by open PR gastownhall#3768.
The changes are in different regions and merge cleanly (verified via
3-way merge-tree); no manual conflict resolution is needed regardless of
merge order.
## Test plan
- `go build ./...`: clean
- `go test ./internal/sling/`: pass, including new
`TestDoSlingIdempotentHonorsNudge` and
`TestDoSlingIdempotentDryRunSuppressesNudge`
Co-authored-by: sjarmak <sjarmak@users.noreply.github.com>
…gastownhall#1129) (gastownhall#4083) ## Why A warm idle pool worker on tmux has no wake path when work is routed to it. The reconciler binds the routed bead as the slot's trigger, but the session is already running with an unchanged config fingerprint, so no Start fires and no startup nudge is delivered. The one backstop that would catch this, `nudgeStalledPoolClaims`, is gated off for tmux on the premise that tmux self-heals a missed nudge through its relaunch path. That premise only holds when a session actually restarts; a healthy idle session never does, so the routed bead sits open and unclaimed. This is the structural form of gastownhall#1129. Split out of a polecat-reliability audit; the runtime-partial fix and the idempotent-sling `--nudge` fix are separate PRs. ## What changed - `cmd/gc/city_runtime.go`: removes the `CanReportActivity` gate on the `nudgeStalledPoolClaims` call so the backstop runs for tmux warm slots. The function keys on the trigger bead still being open and unclaimed and persists bounded observe-then-nudge-then-backoff state on the session bead, so a claim (which flips the bead to in_progress) stops the match; it is churn-free on any runtime. - `cmd/gc/idle_nudge.go`: comment update reflecting the un-gate. - `engdocs/design/idle-claim-nudge-followups.md`: documents the residual gap this does not close (a bead slung to the pool after the slot went idle, and left unassigned, never stamps the slot's `trigger_bead_id`, so it stays invisible), scoped as its own follow-up. ## Coordination note Touches `cmd/gc/city_runtime.go`, also touched by open PRs gastownhall#3767 and gastownhall#3772. The changes are in different regions and merge cleanly (verified via a 3-way merge-tree against both); no manual conflict resolution is needed regardless of merge order. ## Test plan - `go build ./...`: clean - `go test ./cmd/gc/ -run IdleClaimNudge`: pass, including new `TestCityRuntimeBeadReconcileTick_IdleClaimNudgeRunsForReportActivityRuntime`, which drives a reconcile tick with a report-activity runtime and asserts the nudge fires (attempt count 0 to 1) where the old gate left it at 0 Co-authored-by: sjarmak <sjarmak@users.noreply.github.com>
…ld (gastownhall#4059) Auto-landable pure-delete slice of S08: removes dead trace symbols and the LegacyArms dual field. The wire-or-delete judgment call for the remaining S08 surface is held back and not included here. Refs gastownhall#3789. - Gates green - Fable-reviewed, behavior-preserved - Spec: /data/projects/gascity/.claude/worktrees/simplification/engdocs/simplification/specs/S08s0-step0-deletes-spec.md 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>
…link freshness) (gastownhall#3951) ## What The follow-up to the run-detail SSE stream (gastownhall#3942): close the idle-run session-link staleness the stream shipped with. The per-run detail stream only pushed when the **bead fold** changed (a bead event fired `build()` → `notifySubscribers`). Session lifecycle events (`session.updated`/`woke`/`stopped`/…) change the live session links the detail projection layers on, but **not** the bead fold — so on an otherwise-idle run a session-link flip stayed stale until the next bead event or the 3s sessions TTL. `foldNext` now detects `session.*` events in the tail (they share the `session.` prefix; `proj.Apply` ignores them) and calls `refreshSessionEnrichment`: - `sessionsCache.invalidate(city)` — a new `singleFlightCache.invalidate` that expires the entry (sets ttl 0) while **preserving the last-good value and the monotonic version** (a delete would reset the version and risk a memo-key collision), and - `notifySubscribers()` — wake the detail-stream subscribers. Each subscriber then rebuilds via `detail()`, refetches the now-expired sessions (single-flight collapses concurrent rebuilds to one loopback read), and the per-connection **byte-dedup** drops the frame when the run's own links didn't move (e.g. the event was for an unrelated session in the same city). ## Correctness (adversarially reviewed) - **No fold-storm:** the projector advances `LastSeq` for every event and the byte offset advances past the session event, so it is never re-folded — **at most one refresh per tail poll**. (The rare rotation-catch-up path is left to recover via the next poll / TTL.) - **Not a no-op:** session state genuinely feeds the detail bytes (`RunSessionLink` via `resolveRunSessionLink`, `progress.sessionLinkCount`), so a real session change produces different bytes and the dedup emits a frame. - **Concurrency:** `invalidate` is race-clean under the cache lock; invalidate-before-notify ordering means a woken subscriber recomputes. One documented narrow window: an `invalidate` can be masked by a compute that elected before it (self-heals on the next event / reset TTL) — matches the cache's eventual-consistency contract. ## Tests - `TestFoldNextSessionEventRefreshesSessionsAndNotifies` — the mechanism: a `session.*`-only event bumps the generation + invalidates the sessions cache. **Verified it fails without the change** (generation never advances). - `TestRunDetailStreamPushesFrameOnSessionAliasFlip` — **end-to-end**: a session-linked run, a stateful supervisor flipping the linked session's alias `alpha-worker`→`beta-worker`, a `session.updated` (no bead event) → asserts a **second SSE frame arrives with the moved link bytes** (and dedup let it through). Verified it hangs+fails without the change. `go test -race ./internal/api/dashboardbff/...`, `make dashboard-check`, `go vet` — all green. No OpenAPI/wire change. Stacked on gastownhall#3949 (the typecheck:test gate) → gastownhall#3943 → the run-detail stack. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…reaper-aware reap; token-fenced async stop (gastownhall#4089) ## Why Two agent processes could end up working one bead because a respawn happened while the old process was still alive. Four related process-lifecycle defects, from a prior audit. ## What changed - **Fail-closed orphan scan** (`internal/runtime/tmux/adapter.go`, `internal/session/manager.go`): when `ListRunning` errors during the pre-start orphan scan, `FindRuntimesBySessionID` no longer marks every scanned process "tracked" (which made `killExistingOrphans` skip them and let `Start` proceed). It returns the same-session roots untracked so they are killed, bounded to same-session-ID + same-city. - **Confirmed-dead-before-start** (`internal/runtime/proctable/kill_unix.go`, `internal/runtime/process_control.go`): after SIGKILL, `KillByPID` waits (bounded by `ManagedProcessReapGrace`, 3s) until the pid is gone-or-zombie and returns an error if it survives. **All four** `killExistingOrphans` call sites now gate on that error before starting: the `Create` path plus the three resume/respawn paths (`ensureRunning`, `ensureRunningRuntimeOnly`, `retryFreshStartAfterStaleKey`), which operate on a stable reused bead ID and are where a surviving orphan actually occurs. A recycled pid is disambiguated by `/proc/<pid>/stat` start-time so it isn't misread as still-alive. - **Subreaper-aware orphan reap** (`internal/runtime/tmux/tmux.go`, `internal/workspacesvc/orphan_reap.go`): the reparent test is now "parent outside the known descendant set" (tmux) / `ppid == 1 OR ppid == detected subreaper pid` (workspacesvc), so orphans that reparent to `systemd --user` under a `user@.service` subreaper are still collected. Detection failure falls back to strict `ppid == 1` on plain-init hosts. - **Token-fenced async drain-ack stop** (`cmd/gc/session_reconciler.go`): `queueDrainAckAsyncStop` threads the expected `GC_INSTANCE_TOKEN` (captured at queue time) and skips the kill on a definite mismatch, so a stalled async kill can't hit a name-reused replacement, mirroring `verifiedStop`. ## Test plan - `go build ./...`, `go vet`, `golangci-lint run ./internal/session/... ./internal/runtime/proctable/...`: clean (0 issues) - `go test ./internal/session/... ./internal/runtime/... ./internal/workspacesvc/... ./internal/pidutil/... ./cmd/gc/...`: pass - New tests: `KillByPID` confirms death before returning; PID-reuse disambiguation (matching/recycled/empty); reparent collects init + subreaper orphans, skips known descendants; subreaper detection (systemd-user / plain-init / cyclic); token-fence skips a reused name and kills a matching session; and behavioral tests that `Start`/`StartRuntimeOnly` refuse (no `start:` event) and unwind the route when an orphan can't be confirmed dead, plus positive cases proving no over-refusal. The refusal tests were verified to fail if the gate is stubbed to a no-op. ## Note A genuinely-wedged orphan can add up to `ManagedProcessStopGrace + ManagedProcessReapGrace` (~8s) to a single `Create`/respawn call while it confirms death, then refuses that attempt (retried next tick). It is bounded and on the per-request/respawn goroutine, not a shared serialized loop, but a caller behind a tight synchronous SLA will see the occasional multi-second stall when an orphan is actually stuck. --------- Co-authored-by: sjarmak <sjarmak@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e reconciler guards (gastownhall#4087) ## Why Follow-up to gastownhall#4082, which shielded the `StateCache` liveness path but left the reconciler-facing `ListRunning` sites reading a full tmux outage as "zero sessions." The repo already has the convention to prevent that (`PartialListError` / `IsPartialListError` in `internal/runtime/provider_core.go`) and four reconciler sites already guard on it, but nothing emitted the signal for a single-tmux total outage, so the guards were dark. ## What changed - `internal/runtime/tmux/adapter.go`: `Provider.ListRunning` reports a totally unreachable tmux server (`ErrNoServer`) as a `runtime.PartialListError` (nil names) instead of the old empty success `(nil, nil)`, activating the existing guards with no new plumbing. - `internal/runtime/tmux/tmux.go`: the raw `list-sessions` call moves to a private `listSessionNames()` that propagates `ErrNoServer`; `ListSessions()` stays a thin wrapper that re-absorbs `ErrNoServer → (nil, nil)` for its two tmux-internal callers (`FindSessionByWorkDir`, `CleanupOrphanedSessions`), whose behavior is unchanged. Emitting at the provider layer (not `ListSessions`) keeps the blast radius on the reconciler-facing path only. - `engdocs/design/runtime-partial-discipline.md`: moves the `ListRunning` sites from "still exposed" to landed. Two sites gain a genuine safety fix: the pool `on_death` hooks (`city_runtime.go:960`) no longer fire the user's on_death command for every slot on a blip (the false death storm), and the config-reload provider swap (`:1899`) no longer proceeds with zero visible sessions. The shutdown sites (`:3466`/`:3478`) were already a no-op stop set under the old path; only their diagnostic message changes. ## Blast-radius audit All ~20 `ListRunning` callers were checked. None treats the new non-nil error as a trigger for destructive action: each either already guards on `IsPartialListError` and defers, degrades to the StateCache-backed per-session `IsRunning` (protected by gastownhall#4082), or returns the same empty/nil result it produced under the old absorbed-error path. The auto/hybrid composites fold the signal through `MergeBackendListResults`; both `IsPartialListError` and `errors.Is(err, ErrNoServer)` still resolve through the nesting. ## Test plan - `go build ./...`, `go vet ./internal/runtime/... ./cmd/gc/...`: clean - `go test ./internal/runtime/...`: pass - New `internal/runtime/tmux/adapter_unit_test.go`: provider emits `PartialListError` on no-server; a genuine tmux failure is not misclassified as partial; `ListSessions` still absorbs for internal callers. The pre-existing destructive-site guard tests (on_death, provider-swap) now exercise a reachable production path. --------- Co-authored-by: sjarmak <sjarmak@users.noreply.github.com>
…iagnosing (gastownhall#4088) ## Why Two divergence-state classes let beads stall invisibly. Stranded in_progress work whose assignee is a dead pool worker was only *diagnosed*, never repaired, so it sat assigned to a dead session forever. And an open + `gc.routed_to` + dead-assignee bead is invisible to every probe (they all require `--unassigned`), so the pool never reclaims it. ## What changed - **Repair stranded pool-worker work** (`cmd/gc/session_reconciler.go`, `session_beads.go`): the stranded-pool-worker path now wires the existing, tested `unclaimWorkAssignedToRetiredSessionBead` + `closeBead` helpers to actually unassign/reopen the work and close the dead session bead, instead of only emitting a diagnostic. It fires only after confirmed continuous non-liveness (see below), and if any unassign fails it leaves the session bead open and retries next tick rather than closing over a stale assignee. - **Continuous-non-liveness confirmation window**: the repair gates on the `stranded_event_emitted_at` marker aging past a 2-minute grace, and (this is the load-bearing part) the marker is cleared on any tick where the session is observed alive (`clearStrandedEventMarker`, gated on `target.alive`, the exact complement of the `!target.alive` strand condition). So every distinct stranding episode ages a fresh window; a worker that strands, gets respawned/recovered, then re-strands cannot be repaired on a stale marker from the earlier episode. - **Observability for the existing Class-2 sweep** (`cmd/gc/dead_assignee_event.go`, `city_runtime.go`, event/openapi plumbing): the pre-existing `releaseOrphanedPoolAssignments` already clears open+routed+dead-assignee beads with full liveness gating; this adds a `bead.dead_assignee_reopened` event so those repairs are no longer silent. No new destructive path was added for Class 2. ## Test plan - `go build ./...`, `go vet ./cmd/gc/`, `gofmt -l`: clean - `go test ./cmd/gc/ -run 'Strand|DeadAssignee|Reassign|Unclaim|ReleaseOrphaned|Reconcile'`: pass - New tests: recover-then-restrand re-arms the window (repair defers on the fresh episode); continuous-window repair fires end-to-end through the real reconcile; a failed unassign keeps the session bead open; the dead-assignee event carries the typed payload. ## Known follow-up Narrow residual: if the *durable* marker-clear write fails repeatedly during a store outage aligned precisely with a respawn/re-strand window, a stale marker could survive and fire once. It is low-probability, defended by the `releaseOrphanedPoolAssignments` backstop, and is the same cross-restart durability tradeoff the diagnostic-emit path already accepts. A process-local recovered-since guard (mirroring the emit path) would close it; tracked as a follow-up rather than blocking this change. --------- Co-authored-by: sjarmak <sjarmak@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…igger-config loss) (gastownhall#4029) ## What this does Lands **S32** — two commits on the retry/create control path: 1. **RetryHandler delegates to CreateHandler** via `CreateParams.RetrySource` (`internal/convergence`), collapsing the duplicated retry-vs-create bead construction into one path. **Intended behavior fix:** a retried bead that carries trigger config now respects the trigger instead of dropping it. 2. **Extract `processAttemptControl` behind a 3-method strategy seam** (`internal/dispatch/control.go`), unifying the attempt-control dispatch. New tests: `internal/convergence/retry_test.go`, `internal/dispatch/control_test.go`. ## Gates - `go build ./internal/convergence ./internal/dispatch ./cmd/gc` — pass - `go vet ./internal/convergence ./internal/dispatch` — pass - `go test ./internal/convergence ./internal/dispatch` — pass ## Review verdict LAND via label PR (`status/needs-review-auto`) — behavior change (intended fix) on the retry/create control path warrants the auto-review pass. Optional nits deliberately **not** folded (left for auto-review): derive `RetryResult.Iteration` / `FirstWispID` from the actual create outcome; wrap CreateHandler validation errors with the source bead ID. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…all-fix docs: use Homebrew core install command
…orts (gastownhall#4035) Two surgical fixes to how the control dispatcher consumes `gc.failure_class`, root-caused from two production adopt-pr-v2 molecule deaths in maintainer-city. ## FIX 1 — ralph treadmill (`internal/dispatch/ralph.go`) `processRalphCheck` branched only on GatePass vs `attempt >= maxAttempts` and never read `gc.failure_class`, so a HARD-class subject failure (e.g. `external_live_head_changed`) cloned attempts 1..N — a treadmill — before `abort_scope`-killing the molecule (observed on gascity#3943: pre-approval-ci iterations 1→5, all `external_live_head_changed`/hard, then abort). Now a subject that closed `gc.outcome=fail` with `gc.failure_class=hard` terminates the loop in **one attempt** (`Action "hard-fail"`), mirroring `processRetryEval`'s hard handling in `retry.go`. Empty/transient classes stay repairable and still clone up to `max_attempts`. ## FIX 2 — superseded hard abort outvotes passing iterations (`internal/dispatch/runtime.go`) `terminalAbortScopeFailure` applied the `isRetryAttemptSubject` supersession guard only in the default/unknown-class branch; the `hard` branch returned `true` unconditionally. A superseded `abort_scope` attempt (carries `gc.attempt` + `gc.logical_bead_id`) whose logical bead later passed therefore flipped the workflow root to fail at finalize even though later iterations recovered (observed on gascity#4008: a transient `control_dispatch_error` on `review-loop.iteration.3.review-codex` attempt 3, superseded by passing iterations 4-5, still failed the molecule at finalize). The guard now applies to the `hard` class too; a genuinely terminal, non-superseded hard `abort_scope` failure still fails the root. ## Interaction (verified) The review loop **is** a ralph loop, but the transient `control_dispatch_error` is stamped on a nested scope **member**, and `propagateScopeMemberMetadata` drops all `gc.*` keys while `setOutcomeAndClose` sets only `gc.outcome` — so `gc.failure_class` never reaches the iteration scope body the ralph check reads as its subject. FIX 1 therefore cannot hard-terminate the review loop on a transient `control_dispatch_error`; FIX 2 alone covers gastownhall#4008. No quarantine reclassification needed. ## Tests `go test ./internal/dispatch/...` → 294 pass (incl. new `ralph_test.go` hard-terminate + soft-still-retries, and `runtime_test.go` superseded-hard-not-terminal + non-superseded-still-terminal). `go build ./cmd/gc/` + `go vet ./internal/dispatch/...` clean. Deployed to maintainer-city as `dev-d0a41db54` for live validation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ompile-once) (gastownhall#4037) Collapses all workflow-launch shapes onto a single launchWorkflow chokepoint with one duplicate-molecule dedupe guard and compile-once formula handling. Closes the gastownhall#1053 duplicate-molecule window across all launch shapes, plus gastownhall#720. Sibling of S13. Gates green; Fable-reviewed behavior-preserved. Spec at /data/projects/gascity/.claude/worktrees/simplification/engdocs/simplification/specs/S14-launch-chokepoint-spec.md. NOTE: spike/staged for review, not auto-merge. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lone tombstone aliasing) (gastownhall#4032) ## What this does Lands **S05** — unifies the Agent patch/override merge and deep-clone paths in `internal/config`. Two merge bodies + two clone bodies collapse to one each (genuine −109 reduction), and it **fixes a latent tombstone-aliasing bug in `Clone`**. The manual field-sync convention is replaced with a reflect-based `TestAgentCloneIsDeep` (a stronger guard than the old copy-paste checklist). Touches the `config.Agent` field-sync zone (`AgentPatch` / merge / `Clone` / `poolAgents`), `cmd/gc/pool.go`, and the `AGENTS.md` note documenting the convention. ## Gates - `go build ./internal/config ./cmd/gc` — pass - `go vet ./internal/config ./cmd/gc` — pass - `go test ./internal/config` — pass - `go test ./cmd/gc` pool + agent-clone/field-sync/override/patch (`-run`) — pass ## Review verdict LAND via label PR (`status/needs-review-auto`) — field-sync-sensitive area plus a Clone-semantics fix warrant the auto-review pass. Nit deliberately not folded (left for auto-review): drop/comment the dead tombstone copies in `toAgentPatch`. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…astownhall#3984 follow-up) (gastownhall#3987) ## Post-merge hardening for the supervisor webhook receiver Follow-up to the merged **gastownhall#3984** (`feat(webhook): generic supervisor webhook receiver`). A post-merge review of the landed range `e1ba0a13..0f0bb10` found six major issues plus several minors; this PR applies the R1–R4 hardening the design proposal and security red-team required. ### Major fixes - **Rig binding (R4).** Added `Webhook.Rig`. A rig-scoped webhook now dispatches to its own rig and refuses a rule that targets a foreign rig — previously a `scope="rig"` webhook could never dispatch (no rig binding). - **`bearer_env` / `allowed_cidrs` enforced (R1).** Both documented controls were silent no-ops. Bearer tokens are now compared constant-time; `allowed_cidrs` is matched against the direct connection address (X-Forwarded-For is deliberately not trusted, mirroring the supervisor's `remote_addr_class` policy). Both are validated in the `GC_WEBHOOK_*` operator namespace at config load. - **Public webhooks cannot fire exec orders (R4).** Public deliveries are limited to formula orders, removing the in-process `sh -c` RCE sink the red-team flagged. - **Slack/Discord event rules match (correctness).** The event type is now derived from the verified body, so payload-carried rules (e.g. `event = "message"`, non-PING Discord interactions) match instead of being verified no-ops. - **Perimeter/rate-limit ordering (R2/DoS).** The visibility perimeter and rate limiter run before the method check, and the cheap unauthenticated rejects (perimeter, method, rate-limit) are non-evented — removing the event-flood amplifier. A non-POST probe of a private/tenant hook now returns 404 (was 405, which leaked existence). - **`allow_public` content-scoped consent (R3).** A grant is honored only when its digest matches the webhook's current security-relevant content, so a content-swap upgrade auto-downgrades the hook to tenant until the operator re-consents to the new digest (the warning names the digest). ### Minor fixes - Overflow-safe replay-window check (a far-future signed timestamp no longer clamp-underflows past the window in Slack/Discord). - Required order params treat an empty extracted value as missing. - Config validation mirrors the verifier's secret-env namespace/presence rules (and requires `secret_env` for `discord-ed25519`). - Per-hook fair dedup eviction so a high-volume hook cannot evict a quiet hook's replay entries under the shared per-city cap. - `recover()` boundary around detached dispatch goroutines (a panic while processing webhook-derived args no longer crashes the supervisor). ### Notes - `handleHookProxy` (cyclomatic 21→5) and `ValidateWebhooks` (cognitive 28→~2) were decomposed into focused helpers. - Config schema/docs regenerated for the new `rig` field; OpenAPI unchanged. ### Tests New unit + end-to-end coverage for every fix (own-rig dispatch vs foreign-rig reject, bearer/CIDR rejection, public→exec refusal, Slack `event.type` matching, non-evented perimeter/method rejects, content-digest consent, far-future timestamp rejection, per-hook dedup fairness, dispatch-goroutine panic recovery). Fast local suite + `go vet` + `golangci-lint` green. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…pt ref-string surgery) (gastownhall#4044) ## What this does Lands **S38** (part of the simplification track, gastownhall#3789): replaces `findLatestAttempt`'s four-stage dotted-step-ref string surgery with a durable `gc.control_for` lineage stamp. Every attempt/iteration root is now stamped with `gc.control_for` at mint time (compile-time seeds in `formula/retry.go` + `formula/ralph.go`; runtime mints in `dispatch/control.go:buildAttemptRecipe`, written *after* the metadata copy loop so a formula-authored value can't shadow it). Attempt-lineage recovery then becomes one string equality against the control's identity set `{ID, gc.step_ref, gc.step_id}` plus an integer `max(gc.attempt)` — no ref parsing. Clone coherence for in-iteration ralph retries is kept via a post-create bead-ID remap (`dispatch/ralph.go`) and `MetadataRefs`. Leans on the existing `beadmeta.ControlForMetadataKey` (`gc.control_for`) — no new metadata key, no wire/event changes. Confined to `internal/dispatch` + `internal/formula`; typed-wire/events, worker boundary, `config.Agent` field-sync, and `cmd/gc` projections are untouched. ## Behavior-preserving The dense four-stage cascade is **demoted**, not deleted: `latestAttemptFromCandidatesLegacyRefSurgery` is moved verbatim and marked DEPRECATED, invoked only as a guarded fallback when no candidate carries a stamp (pre-S38 in-flight molecules). A package-level `legacyAttemptLineageHits` counter makes the pre-stamp drain observable. Existing unstamped fixtures resolve through this fallback unchanged. **Phase 4 (cascade deletion, ~80 LOC of the densest dispatch code) is deferred** to the release after the legacy-hit counter drains to zero in production — this PR is Phases 1-3 (write, clone-coherence, read-flip-with-fallback). ## Tests Stamp coverage per mint path (T1), read-side table test (T2), shadow parity primary==legacy on stamped fixtures (T3), legacy-population fallback + counter (T4), and W6/W7 clone-remap mechanisms (T5). ## Notes - Rebased onto current `main` (over S12/S13/S37/S04/S09/S06/S26). S37 (`dispatch/fanout.go`+`runtime.go`) and S13 (`sling/`) touch different files than S38 (`dispatch/control.go`+`ralph.go`), so the rebase was clean; S38 semantics unchanged. - Spec: `engdocs/simplification/specs/S38-control-for-lineage-spec.md` (on the simplification audit branch). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(+ F1/F2/F3 fail-closed follow-ups) (gastownhall#4023) ## What this does Lands **S16** — surfaces the seven swallowed errors on the reconciler's destructive/routing paths (trace/log + retry-next-tick; fail CLOSED on destructive paths) — and folds in the three review follow-ups (F1/F2/F3). **Commit 1** (`simplify(S16)`): the original S16 change (sling convoy-recovery fail-closed, reconciler orphan-close fail-closed on liveness error, dispatch route-config error surfacing + lazy cache, drain reload error, attempt-log corruption trace). **Commit 2** (folded follow-ups): - **F1** — direct end-to-end test that the liveness-error fail-closed gate fires in the running reconciler. A healthy liveness observation closes an undesired, dead orphan; an injected observation error keeps the bead open (skip) and logs the guard line. The two runs differ only in the liveness error, isolating the guard. (`TestReconcileOrphanCloseFailsClosedOnLivenessError`.) - **F2** — extend the liveness-error fail-closed gate to the three sibling `!providerAlive` destructive paths S16 left ungated, each mirroring the orphan-close guard (trace `skipped_liveness_error` + skip this tick): **pending-create rollback**, **failed-create close**, **drain-ack finalize**. The plain Ctrl-C drain path is `providerAlive`-only and already safe — left untouched; the misleading orphan-close comment is corrected. - **F3** — distinguish `beads.ErrNotFound` in `needsConvoyRecovery`: a genuinely deleted parent still triggers convoy recovery, while transient parent-read errors keep failing closed against the gastownhall#2987 duplicate-convoy vector. (`TestNeedsConvoyRecoveryDistinguishesDeletedParent`, both branches.) ## F2 safety (wedge review) At the reconciler call site the liveness handle resolves from the loaded session bead, so a **dead-but-observable** runtime returns `(Running=false, nil)`. `livenessErr` is non-nil **only** on a genuine observation failure (handle construction, or the store re-read in `manager.Get`). So the gate fails closed on real errors and **never** wedges cleanup of a confirmed-dead session (that path keeps `livenessErr == nil` and proceeds). The failure mode under a persistent observation error is "leave the bead open and re-observe next tick" — the safe direction. ## Gates - `go build ./...` — pass - `go vet ./cmd/gc ./internal/sling ./internal/dispatch` — pass - `go test ./internal/sling ./internal/dispatch` — pass - `go test ./cmd/gc` reconciler suite (`-run` reconcile/session/drain/heal/ orphan/liveness/pending/failed/close/trace/convoy) — pass _(The full `cmd/gc` package exceeds the raw 600s `go test` timeout — a known sharding limit per `TESTING.md`, not an assertion failure.)_ ## Review verdict LAND via label PR (`status/needs-review-auto`) — destructive-path behavior change (fail-closed extension); routed through auto-review per the simplification walkthrough decision. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…molecule_failed" literals (gastownhall#4048) ## Summary Extracts the bare `"molecule_failed"` metadata-key string into a single compiler-checked `beadmeta.MoleculeFailedMetadataKey` constant and routes the **9 production map-key / composite-literal-key sites** through it: - `internal/molecule/molecule.go` — `findExistingAttach`, `existingAttachIDMapping`, `markFailed` - `internal/dispatch/control.go` — `failedAttemptAttachRootID` ListQuery, `isFailedPartialMolecule` - `internal/dispatch/drain.go` — `ensureDrainItemRoot`, `closeFailedDrainItemRoots` - `internal/sling/sling.go` — `closeFailedGraphV2RootsByKey` - `cmd/gc/cmd_formula.go` — `closeFormulaCookFailedGraphV2Roots` **Byte-identical on-store value.** The constant equals the old literal, so no bead round-trips differently. Error-message prose (`molecule.go` "is marked molecule_failed") and the test-file literals are intentionally left as string literals — they are the wire-string drift guards proving the constant emits the identical value. Placed in the existing non-`gc.` "dispatch metadata keys" const block (alongside `MoleculeIDMetadataKey`), **not** in `KnownMetadataKeys` — that block's drift guard only covers the `gc.` namespace, exactly like the sibling `molecule_id` key. ## Scope This is the smallest, lowest-risk slice of a larger cleanup that routes business logic through typed domain objects / confined codecs instead of cracking raw beads inline. Deliberately **constant-only**: no `molecule.State` typed view was added, because every reader of `molecule_failed` is the dispatcher/substrate (where the bead legitimately *is* the domain object). ## Verification - `go build ./...`, `go vet ./...` clean - `go test ./internal/beadmeta ./internal/molecule ./internal/dispatch ./internal/sling` green - `go test ./cmd/gc -run 'Formula|Molecule|Drain'` green - `rg '"molecule_failed"'` confirms zero remaining production map-key literals - TDD: pinned-value test added first (red on undefined constant), then constant (green) Part of the raw-bead-leak cleanup epic. Refs `ga-vpemze`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ections (gastownhall#4051) ## Summary Deletes `internal/api` re-implementations of confined `internal/session` codecs and routes the remaining API-side session-metadata cracks through `session.Info` projections. - **Mailbox address dedup** — `apiSessionMailboxAddress` deleted → `session.MailboxAddress`. `apiSessionMailboxAddresses` was **not** identical to `session.MailboxAddresses` (the API variant appends `session_name` *unconditionally last*, the CLI codec only as a last-resort fallback — a deliberate prior fix, `bf576b04a`), so rather than collapse them, a new `session.MailboxAddressesIncludingRuntimeName` shares a `mailboxAddresses(b, includeRuntimeName bool)` body and preserves the API semantics. - **Assignee identities** — new `session.AssigneeIdentities`/`AssigneeIdentifier` replace `internal/api`'s `sessionBeadAssigneeIdentifier`; `handler_beads.go` assignee enumeration routes through the codec. - **Session-metadata cracks** — `handler_sessions.go`, `huma_handlers_sessions_command.go`, `session_resolution.go`, and `cmd/gc/pool_session_name.go` read `session.Info` projections instead of raw `b.Metadata[...]`. `session_resolution.go` deliberately uses the raw `MetadataState` mirror (not `Info.State`, which folds awake→active). ## Behavior preservation All three equivalences verified byte-for-byte: mailbox address set + order, assignee identity forms (the assignee-term ordering delta is inert — the sole consumer dedupes by `(rig, ID)` and re-sorts with a total order), and the `MetadataState`-vs-`Info.State` distinction. The `cmd/gc` bead-form peer stays inline in the hot reconciler loop, guarded by the classifier-equivalence oracle. ## Verification - `go build ./...`, `go vet ./internal/session ./internal/api ./cmd/gc` clean; `gofmt` clean - `go test ./internal/session ./internal/api` green; `go test ./cmd/gc -run 'SessionBeadAssigneeIdentities|InfoEquiv|Classifier'` green - `bf576b04a` oracle `TestMailAPIQueriesAllResolvedSessionMailboxAddresses` + `TestOpenAPISpecInSync` green; `make test` exit 0 - TDD: `MailboxAddressesIncludingRuntimeName` / `AssigneeIdentities` / `AssigneeIdentifier` pin tests written first - Fable adversarial review: approve (all equivalences confirmed) ## Follow-up (pre-existing, out of scope) Sibling raw session-metadata cracks remain at `internal/api/handler_status.go` (~503-505, 740) and `huma_handlers_sessions_query.go:296` — a later slice can route them through `session.InfoFromPersistedBead` (the raw mirrors already exist). Part of the raw-bead-leak cleanup epic. Refs `ga-7jftpc`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…o codec (gastownhall#4056) ## Summary Introduces a typed `session.WaitInfo` codec (confined to `internal/session/waits.go`) and routes `gc wait`'s read/render/decision paths through it, eliminating the inline `objectFromBead(bead).<property>` cracking — the clearest textbook instance of the antipattern. - `internal/session/waits.go` — new `WaitInfo` + `WaitInfoFromBead` codec (+ `splitWaitDepIDs`, a verbatim move of the old `splitWaitIDs`); `ListSessionWaitBeads` → `ListSessionWaits` returning `[]WaitInfo`; the 3 in-package consumers retyped. - `cmd/gc/cmd_wait.go` — `waitJSONFromBead`→`waitJSONFromInfo`, `writeWaitDetail`, all loaders, deps-readiness, `waitNudgeID`, `nextWaitDeliveryAttempt`, `readyWaitSetForList` build from `WaitInfo`. **Metadata reads dropped 48 → 4** (the 4 survivors are *session*-bead readers — the separate session.Info opportunity, out of scope). - Write codecs (`retryClosedWait`, `setWaitTerminalState`, the `cmdSessionWait` meta map) deliberately stay on raw beads — no over-reach. ## Behavior preservation `WaitInfoFromBead` decodes the same 10 keys with identical absent-key defaults; the `schema_version`-1 CLI JSON is byte-identical (pinned by `TestWaitJSONFromInfo_MatchesBeadProjection`, re-run independently by review). **One display-only nit (documented, accepted):** `writeWaitDetail`'s Deps line now renders the normalized (trimmed, empties-dropped) `DepIDs` join. This is byte-identical for every in-tree writer (the only writer joins comma-no-space; retry clones verbatim); it differs only for out-of-band hand-edited metadata like `"gc-1, gc-2"`, which no system code produces. Reverting would re-introduce a raw `b.Metadata` read, so it's left as the normalized form. ## Verification - `gofmt` clean; `go build ./...`, `go vet ./internal/session ./cmd/gc` and `go vet ./...` clean - `go test ./internal/session` green; `go test ./cmd/gc -run Wait` green (incl. new equivalence + golden tests); acceptance `Wait` green — all independently re-run at the branch tip - No wire/OpenAPI/dashboard impact - Fable adversarial review: approve (equivalence independently re-verified; the one-off `make test` blip is the documented `eventfeed TestMuxSource` parallel-sweep flake, not a regression) Part of the raw-bead-leak cleanup epic. Refs `ga-qjcta7`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…wins (gastownhall#4055) ## Summary Retires the dead/legacy raw-session wake helpers now that production wake decisions run entirely on the typed `ComputeAwakeSet` path. Net **+230 / −494**. - **Slice 1 (CLI gate):** adds `TestSessionReason_MultiReasonColumnCharacterization` pinning the exact `gc session` REASON column (`session,config,attached`, `wait`, `,pin` suffix, `-` collapse). It passes on unchanged code and stays green through every slice — proving the CLI output is byte-identical. - **Slice 2:** deletes the test-only drain wrappers `advanceSessionDrains` / `advanceSessionDrainsWithSessions` (zero production callers — the reconciler calls `advanceSessionDrainsWithSessionsTraced` directly) and migrates the 11 test call sites to feed the traced core **explicit** `wakeEvals` encoding each test's original premise. No assertion weakened. - **Slice 3:** deletes 7 dead functions (`computeWakeEvaluations`, `capWakeConfigByDemand`, `applyDependencyWakeReasons`, `removeWakeReason`, `preferredDependencySessions`, `compareDependencyCandidate`, `hasDependencyWakeRoot`) and the `WakeDependency` constant. **Keeps** `wakeReasons`/`evaluateWakeReasons` (the CLI REASON-column display helpers — multi-reason, so they cannot collapse to single-reason `ComputeAwakeSet`) and `containsWakeReason`, with an accurate scope comment. - **Slice 4:** deletes 3 raw-bead ghost twins (`scaleCheckPartialSessionPreservable`, `scaleCheckPartialSessionRetainable`, `isPendingPoolCreate`); the production `*Info` siblings survive. ## Behavior preservation Production wake/sleep decisions are unchanged — every deleted function was already dead in production (main-branch comments documented "never fires in production"). Both rg zero-hit checks confirm the deleted symbols are gone (only `*Info` forms remain). The two deviations are comment/debug-trace-string only (renaming stale references to the deleted `advanceSessionDrains` name to avoid dangling ghosts). ## Verification - `go build ./...`, `go vet ./...`, `gofmt` clean - Characterization gate + drain suite (`TestAdvanceSessionDrains|CompleteDrain|DrainTracker|CancelSessionDrain`) + `TestWakeReasons|EvaluateWakeReasons|SessionClassifierInfoEquivalence` green - Bounded blast-radius run (Session/Wake/Drain/Pool/Reconcil/DesiredState/ScaleCheck/Classifier/Sleep/Idle/Heal/Cancel) `ok 182s` - Fable adversarial review: approve — independently re-verified dead-code, preserved test premises, and the characterization pin > Note for CI: use the sharded `make test-cmd-gc-process-parallel` target; monolithic > `go test ./cmd/gc/` times out on an unrelated network-hanging init test in this environment. First slice of the session-class adoption stack (O1 → O2 → O4). Refs `ga-6aaj6q`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…vergence GateOutput (gastownhall#4050) ## Summary Routes the order feed/history read edges through the typed `orders.OrderRun` decode instead of cracking tracking-bead labels/`Status` inline, and gives the `convergence.gate_*` exec-gate vocabulary its **own** `internal/convergence` projection rather than folding it into orders. - `internal/orders/store.go` — new `RunFromTrackingBead`, `RunOutcome.IsExec/Display`, `OrderRun.State`, `Store.ListTracking`, `Store.LatestOpenRun` - `internal/convergence/gate_output.go` (new) — `GateOutput` + `GateOutputFromMetadata`/ `HasOutput`/`CombinedOutput`; convergence owns the `convergence.gate_*` keys - `internal/api/orders_feed.go`, `huma_handlers_orders.go`, `handler_orders.go` — decode `OrderRun`s + `GateOutput`; deleted `orderTrackingStatus`, `orderTrackingScopedName`, `orderLabelsContain*`, `lastRunOutcomeFromLabels`, `orderRunHasOutput` (6 raw `convergence.gate_*` reads removed) ## Ownership note `convergence.gate_*` is a distinct exec-gate vocabulary, so it lives in `internal/convergence`, **not** `orders.OrderRun` — the order tracking bead does not own those keys. ## Behavior preservation Display status and scoped-name derivation are byte-identical to the deleted inline logic; the store reads mirror the prior raw queries (including `LatestOpenRun`'s deliberate `IncludeClosed` omission, pin-tested). `Display()` was verified byte-equivalent to `lastRunOutcomeFromLabels` for every outcome label set a production writer emits; a doc comment on `outcomeFromLabels` now records the single-outcome-family-per-bead invariant this relies on. ## Verification - `go build ./...`, `go vet` clean; `gofmt` clean - `go test ./internal/orders ./internal/convergence ./internal/api` green; `go test ./cmd/gc -run Order` green - `TestOpenAPISpecInSync` green (no wire drift; no generated/dashboard file touched) - TDD: decode/projection tests written first; API outcome table ported verbatim into the orders package - Fable adversarial review: approve (behavior byte-identical on all reachable inputs) ## Follow-ups (pre-existing, out of scope) `huma_handlers_orders.go` history-fetch still hand-builds the `order-run:` label / raw `ListQuery`, and `cmd/gc/order_dispatch.go markTrackingFailure` writes the `{wisp,wisp-failed}` pair via raw `store.Update` — both are pre-existing sites for a later `orders.Store` history-read / `SetOutcomeWithCursor` slice. Part of the raw-bead-leak cleanup epic. Refs `ga-wp0309`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… guard (gastownhall#4045) Phase 1 of the Info-migration: routes all Info writes through one tick-scoped mutator + a source-scan guard that makes the forgotten-fold coherence bug class unrepresentable; raw-bead mirror retained so behavior is unchanged. Phases 2 (mirror removal) & 3 (god-file split) are folded into the S19 reconciler roadmap. gastownhall#3789/gastownhall#1029/gastownhall#3872. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…codec (gastownhall#4049) ## Summary Mints a single `molecule.WorkflowBeadFromBead` codec and routes the **three byte-identical `workflowBeadResponse` build loops** in `internal/api` through one mapper, eliminating triplicated raw-bead metadata cracks: - `handler_convoy_dispatch.go` — `snapshotFromStore` - `convoy_sql.go` — `tryFullWorkflowSQL` - `convoy_event_stream.go` — `projectWorkflowEvent` `WorkflowBeadFromBead` (+ standalone `WorkflowStatus`/`WorkflowKind`/`WorkflowAttempt`) are verbatim moves of the former inline `internal/api` helpers, so status (`Status` + `gc.outcome`), kind, and attempt derivation are unchanged. ## No wire change `BeadGraphResponse` continues to ship `beads.Bead` as before — this PR is the internal codec + loop-consolidation only. `TestOpenAPISpecInSync` and `make dashboard-check` both pass; no `openapi.json`, generated TS, or dashboard file is touched. (Returning a typed view on the wire is a deliberate, separate follow-up.) ## Scope discipline `WorkflowBead` carries **only** the 10 fields the mapper consumes. It does not duplicate `api.resolvedWorkflowID` (which stays the single implementation at its 6 call sites) or project speculative unused fields — no premature abstraction, one source of truth. ## Verification - `go build ./...`, `go vet ./internal/molecule ./internal/api` clean; `gofmt` clean - `go test ./internal/molecule ./internal/api` green (incl. `TestOpenAPISpecInSync`) - `make dashboard-check` green - TDD: codec unit tests written first (status table, kind, attempt, nil-metadata clone); `TestWorkflowBeadResponseFromBeadEquivalence` pins the mapper byte-equal to the old inline expression - Fable adversarial review: behavior-preservation verified clean; the one blocker (speculative field over-reach) fixed by trimming Part of the raw-bead-leak cleanup epic. Refs `ga-rt98y1`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… read via DecodeShadow (gastownhall#4052) ## Summary Finishes the nudge Phase-2 migration so `cmd/gc` stops cracking raw nudge/session beads and stops re-stamping vocabulary the store owns. - **`nudgequeue.Store.SweepStale(beadID, closeReason, now)`** (new) confines the `gc-swept` terminal-key vocabulary (`state` / `terminal_reason` / `commit_boundary` / `terminal_at` / `close_reason`). `cmd/gc/nudge_mail_sweep.go` calls it instead of the inline `SetMetadataBatch` + `Close` block. - Nudge ids read via **`nudgequeue.DecodeShadow(b).ID`** (the read codec exported for exactly this) instead of `b.Metadata["nudge_id"]` at both sweep call sites. - `cmd/gc/cmd_nudge.go` reads `session_name`/`continuation_epoch` off a `session.InfoFromPersistedBead` projection; the raw-bead resolver `resolveNudgeTargetFromSessionBead` is deleted (single `session.Info` resolver now). ## Behavior preservation `SweepStale` mirrors the deleted inline sweep byte-for-byte: same five keys/values, same `SetMetadataBatch`-fail-skips-`Close` ordering, identical `"nudge %s: set metadata/close: %w"` error text, nil-receiver safe. `DecodeShadow(b).ID` and the `session.Info` mirrors (`SessionNameMetadata`, `ContinuationEpoch`) are verbatim untrimmed/trimmed reads matching the deleted forms. ## Verification - `gofmt` clean; `go vet ./internal/nudgequeue ./cmd/gc` clean; `go build ./...` clean - `go test ./internal/nudgequeue` green; `go test ./cmd/gc -run 'Nudge|Sweep'` green; `make test` exit 0 - TDD: `TestSweepStaleEmitsByteIdenticalWrites` (golden 5-key map + one Close), fail-skips-Close, nil-no-op; resolver equivalence goldens captured empirically before deleting the bead form — including a full-struct `reflect.DeepEqual` golden pinning derived fields (`cityPath`/`cityName`/parsed agent/resolved provider) - Fable adversarial review: approve (writes byte-identical); two nits applied (stale-comment reword + restored full-struct golden) Part of the raw-bead-leak cleanup epic. Refs `ga-4ikiot`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extracts the async-start staleness machinery in `cmd/gc/session_lifecycle_parallel.go` into a typed `sessionpkg.PendingCreateLease` value with explicit legal transitions and a `LeaseCommitVerdict` enum, then repoints the `asyncStart*` callers to thin delegations over the lease. - `internal/session/pending_create_lease.go`: `PendingCreateLease` value type (`LeaseFromBead` / `LeaseFromInfo` constructors), the `LeaseCommitVerdict` enum, the single-sourced `stateConfirmsPendingStart` state gate, and the `SameIdentity` / `CommitVerdict` / `Confirm` transitions. - `cmd/gc/session_lifecycle_parallel.go`: `asyncStartIdentityMatches`, `asyncStartSessionStillCurrent`, and `asyncStartStaleRuntimeCleanupAllowed` become thin delegations to the lease. **Hardens the pending-create bug family** (gastownhall#1542 / gastownhall#2073 / gastownhall#2895 / gastownhall#3849) by making the previously ad-hoc boolean checks typed transitions on a single lease value — the identity match, the still-current gate, and the stale-runtime cleanup decision can no longer drift apart. **Semantics-preserved, Fable-reviewed:** an exhaustive parity grid (`TestCommitVerdict_ParityWithLegacyBooleans`) proves `CommitVerdict` reproduces the legacy `asyncStartSessionStillCurrent` / `asyncStartStaleRuntimeCleanupAllowed` booleans exactly for every (prepared, current) combination, and that the fused enum never yields KeepRuntime on the pure state gate. No persisted keys change; no store/provider I/O moves. All gates green. Spec: `engdocs/simplification/specs/S28-pending-create-lease-spec.md` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gastownhall#4100) ## Summary Adds `internal/api/readauth.go`, the read-side twin of the write-auth gate (gastownhall#3791 / gastownhall#3792): an **opt-in, fail-closed** middleware requiring a signed, single-use, request-bound `X-GC-City-Read` grant (audience `gc-city-read`) on every GET/HEAD of the typed per-city API under `/v0/city/{cityName}`. With no `read_auth_verify_key` configured the middleware is **not installed** and reads stay open — behavior is bit-identical to today (opt-in). `read_auth_required` (or `GC_CITY_READ_REQUIRED=1`) makes a missing key a boot-time hard fail. This lets an authority-fronted deployment require an authenticated grant to read a city's beads, mail, sessions, and agent transcripts instead of trusting network position. Highlights: - Reuses `internal/citywriteauth` unchanged (a second `Verifier` with a distinct audience). Reads carry no body, so the grant binds method+path+query over an empty body and is consumed at admission; there is no CSRF or read-only front-door (a read changes no state and must work in read-only mode). SSE stream reads are gated at connect. - Refactor: extracts the shared path grammar into `cityScopedObjectPath`; `cityScopedObjectMutation` delegates to it (write path unchanged). - **Scope boundary (v1):** gates only the typed `/v0/city` reads. The supervisor-scope aggregate event feed (`/v0/events[/stream]`) and the default-on dashboard host plane (`/api/*`) remain ungated — documented in the config field so operators don't over-trust the boundary; gating them is follow-up. - No new public wire/OpenAPI surface (mux-level gate, like write-auth). Only new strings are `X-GC-City-Read` / `gc-city-read`, mirroring the existing `X-GC-City-Write` / `gc-city-write`. ## Testing - [x] `golangci-lint` (0 issues), `gofmt` clean, `go vet ./...`, `go test ./test/docsync`, `make generate` in sync, and full `internal/api` + `internal/config` + `internal/supervisor` tests all pass — run manually because the local pre-commit hook kept losing a fleet-wide golangci-lint lock. - [ ] `make check` — deferred to CI (local run blocked by the fleet golangci-lint lock; equivalent gates run manually, above). - [x] Added `internal/api/readauth_test.go` (missing-grant 401, valid grant, HEAD/method binding, audience isolation both directions, query binding, SSE admission, control-char reject, replay, wrong-city, read-only mode, resolved-verifier behavioral, and full end-to-end through `SupervisorMux.Handler()`). ## Checklist - [x] Linked an issue, or explained why one is not needed — additive, opt-in read-side complement to the gastownhall#3791/gastownhall#3792 write-auth gate. - [x] Added or updated tests for behavior changes - [x] Updated docs for user-facing changes (`read_auth_verify_key` / `read_auth_required` config fields; `docs/reference/config.md` + schema regenerated via `make generate`) - [x] Called out breaking changes or migration notes — none; opt-in, default behavior unchanged. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ssion reconciler (gastownhall#4025) ## What this does Lands **S20** — turns the session reconciler's forward-compat unknown-state skip from a silent per-tick stderr storm into a durable, throttled signal — and folds the review follow-up + completes the OpenAPI change the source commit left half-done. **Commit 1** (`simplify(S20)`, slice-a, signal-only): new typed event `session.unknown_state` (constant + `KnownEventTypes`) with a registered `api.SessionUnknownStatePayload`; `emitSessionUnknownStateDiagnostic` reuses the stranded-diagnostic throttle-marker pattern — durable `unknown_state_first_seen` / `_value` / `_escalated_at` markers, emit only on first sight or a changed unrecognized value (gastownhall#2389), survives restarts (gastownhall#2085), and one escalated re-emit past 30m (gastownhall#1497). No state mutation; recovery stays with operators/pack subscribers. **Commit 2** (folded follow-up + genclient regen): - **Follow-up** — clear the unknown-state markers when a session recovers to a known state (`clearSessionUnknownStateMarkers`, called on the known-state path). The markers are durable, so without this a later recurrence of the *same* unrecognized value would look like "same state as last tick" and be silently suppressed — the recurrence would never re-signal. No-op (no write) when the session carries no markers. Covered by `TestClearSessionUnknownStateMarkers_RecurrenceReemitsAfterRecovery`. - **genclient regen** — the source commit added the event payload to `internal/api/openapi.json` but did not regenerate the Go client, so `TestGeneratedClientInSync` (CI: `preflight-generated` → `spec-ci`) was red. Ran `go generate ./internal/api/genclient` to add the `SessionUnknownStatePayload` client types. The vendored dashboard hey-api client (`types.gen.ts`) is sourced from the external `gascity-dashboard` repo and is not regenerated/drift-checked by this repo's `dashboard-ci` gate, so it is intentionally untouched. ## Deferred Slice-b — the actual typed `SessionState` enum simplification the title promises — is deferred and filed as bead **ga-cx470v** so it isn't lost. ## Gates - `go build ./...` — pass - `go vet ./cmd/gc ./internal/api ./internal/api/genclient ./internal/events` — pass - `go test ./internal/api ./internal/events ./internal/api/genclient` — pass (incl. `TestGeneratedClientInSync`, `TestOpenAPISpecInSync`, `TestEveryKnownEventTypeHasRegisteredPayload`) - `go test ./cmd/gc` reconciler + unknown-state suite (`-run`) — pass ## Review verdict APPROVED — land with the follow-up folded into the same PR (per the simplification walkthrough). Routed via label PR (`status/needs-review-auto`) because it touches the typed-event / OpenAPI wire surface. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-canonical fix(status): derive ready work from canonical projection
## Summary - Preserve `GC_ACCEPTANCE_BEADS_PROVIDER` through the acceptance target's scrubbed environment so explicit SQLite coverage reaches the test process. - Make the acceptance and bdstore targets deterministic by clearing caller `GOFLAGS` and disabling caller `GOENV`/`GOWORK` state. - Add one fast Medium contract that proves provider forwarding, exact suite selection, subprocess status, and host-state isolation across the real Make and shard boundaries. - Update the checked subprocess ledger. Raw subprocess calls increase by one; Small-test subprocess debt remains unchanged because the new owner is explicitly Medium. ## Why The nightly SQLite job could set a provider at the workflow level while `env -i` silently dropped it before `go test`. The same boundary also allowed caller Go configuration to narrow a coverage-sensitive target—for example, `GOFLAGS=-run=^$` could produce a green run with no tests selected. This change makes those boundaries explicit and executable without moving or deleting any coverage. ## Verification - [x] `GOFLAGS=-mod=readonly go test ./scripts -count=1` - [x] `GOFLAGS=-mod=readonly go test -count=1 ./internal/testpolicy/resourcecensus` - [x] `.githooks/pre-commit` (changed-package lint, generated-file checks, `go vet ./...`, docs sync) - [x] Pre-push `make test-fast-parallel` equivalent completed with all eight shards green - [x] Mutation proof: presence-conditioned provider rewriting fails - [x] Mutation proof: argv and `GOFLAGS=-run=^$` suite narrowing fail | Measurement | Result | | --- | ---: | | Focused provider/suite contract | 0.135s | | `./scripts` package | 20.651s | | Resource-policy package | 3.032s | ## Review - Approved staged-diff SHA-256: `d1704cb5e10574f30cec6892965b659199140a06bcee72dd1725839d0b45a94b` - Three independent delegated reviewers approved correctness, adversarial bypass resistance, and maintainability before commit. - Rebase onto current `origin/main` was patch-identical (`git range-diff` showed `=`). ## Checklist - [x] No standalone issue is needed; this is the bounded provider-boundary slice of P0.1 in the testing-pyramid hardening plan. - [x] Added tests for every behavior change. - [x] Updated the checked testing-resource documentation. - [x] No user-facing API or migration change.
Summary - Gate gc prime --hook SessionStart output behind a real managed Gas City session context. - Require GC_SESSION_ID and GC_SESSION_NAME to match an open session bead in active, awake, creating, or start-pending state before injecting hook context. - Keep explicit gc prime fallback behavior unchanged and keep non-SessionStart hook behavior unchanged. Source / behavior before - Active Codex hooks in td-core and td-city run GC_MANAGED_SESSION_HOOK=1 GC_HOOK_EVENT_NAME=SessionStart gc prime --hook --hook-format codex. - td-core resolves as a rig under td-city, so ordinary Codex sessions in td-core received the default Gas City Agent prompt even without a managed Gas City session. Behavior after - Unmanaged SessionStart hook returns no hook output. - Real managed sessions still receive hook context once the live session bead check passes. - Explicit gc prime with no city still emits the default fallback prompt. Verification - CGO_ENABLED=0 go test ./cmd/gc -run ^TestDoPrime - CGO_ENABLED=0 go build -o /tmp/gc-xgc-hook-gate ./cmd/gc - From /Users/jpb/workspace/tuxedodrive/td-core, unmanaged SessionStart with cleared GC_SESSION_ID/GC_SESSION_NAME exited 0 with empty stdout. - Explicit fallback check still printed the default Gas City Agent prompt. Notes - Direct push to gastownhall/gascity was denied for jonathanpberger (viewerPermission=READ), so this PR is opened from the existing jonathanpberger/gascity fork. --------- Co-authored-by: JPB Mini <jpb@JPBs-Mac-mini.local>
## Why CI topology is part of the test contract. Text-based checks could miss executable workflow paths, misclassify matrix data as environment ownership, and depend on ambient Python or Go configuration. That made it possible for the fast PR lane to drift toward provider-backed tests without a reliable, fast guardrail. ## What changed - Add a semantic Actions policy reader for workflows and composite actions. - Fingerprint every display-free PR and nightly execution path, including reusable-workflow `uses`, `with`, `secrets`, and composite outputs. - Restrict provider ownership checks to actual workflow, job, step, container, service, and composite-step environment positions. - Keep matrix data named `env` out of provider ownership decisions. - Require deterministic filters and a single primary Go proof in the live PR topology. - Replace process-heavy Python coverage-policy setup with stdlib-only behavior tests that run under `python3 -S`. - Run the policy immediately after `setup-go` in `preflight-static`. ## Performance and verification - `make test-ci-policy`: 1.1-1.25s, including hostile `GOFLAGS`, `GOENV`, `GOWORK`, `PYTHONPATH`, and `PYTHONHOME` checks - `make test-fast-parallel`: 422.84s baseline - `go test -count=1 ./scripts` - remaining 31 workflow Python tests - `actionlint` - `go vet ./...` - `.githooks/pre-commit` The 422.84s baseline exposed `unit-core`—especially `examples/bd/dolt`—as the next critical-path target. This PR adds the cheap semantic guardrail; the next slice moves process-heavy behavioral permutations to crisp in-process/conformance tests and leaves one explicitly owned real-Dolt proof in the provider lane. ## Review Three independent reviewers approved the exact staged tree. The patch ID remained unchanged through rebase.
…keys (gastownhall#3965) (gastownhall#4019) ## What `gc config explain <agent>` omitted the resolved idle/lifecycle-timeout keys from its agent block, so their values and provenance had to be read from the pack `agent.toml` directly. This adds rows for the four timeout keys that are set on an agent: - `idle_timeout` (Agent.IdleTimeout) - `sleep_after_idle` (Agent.SleepAfterIdle) - `max_session_age` (Agent.MaxSessionAge) - `max_session_age_jitter` (Agent.MaxSessionAgeJitter) ## Why Reported in gastownhall#3965: `explainAgent` renders name/dir/provider/scaling/etc. but no idle rows, even when a refinery agent configures both `idle_timeout` and `sleep_after_idle`. These keys drive idle-suspend (compute_awake_set) and session-age recycling, so their resolved value + source file matter when debugging why a session slept or was recycled — exactly what `explain` exists to surface. ## Scope Intentionally scoped to the idle/lifecycle-timeout cluster the issue names, rather than dumping every `config.Agent` field. `explainAgent` renders a curated subset today; a blanket "render all resolved keys" change is a larger, separate design call (field ordering, truncation of slice/map fields, whether `toml:"-"` internals should ever appear). Happy to follow up on that if the maintainers want it — this PR closes the concrete idle-provenance gap with the minimal, consistent addition. ## How Each key follows the existing conditional `explainField(w, key, value, source)` pattern — a key is rendered only when set, so unconfigured agents get no spurious rows. ## Test `TestExplainAgentRendersLifecycleTimeoutKeys` — an agent with all four keys set renders a row (key + value) for each. Reverting the fix reproduces the reported "missing key" symptom (RED). `TestExplainAgentOmitsUnsetLifecycleTimeoutKeys` — an agent with none set renders no timeout rows (guards the conditional pattern). Validated with `-tags gms_pure_go`: gofmt clean, `go vet` clean, targeted + `ConfigExplain|ConfigShow|ExplainAgent` suites green.
## Why `examples/bd/dolt` was the fast suite's critical package: 111 compact-script fixture constructions ran serially even though each scenario owns a temporary city, fake binaries, logs, state files, and a unique listener. The focused package took 317.815 seconds. ## What changed - Mark compact-script scenarios parallel at their shared fixture boundary. - Bound real shell fan-out to eight concurrent scenarios, independent of host CPU count. - Release the process slot through `t.Cleanup` for every success and failure path. - Keep production scripts, assertions, fixtures, and the tagged real-Dolt proof unchanged. ## Results | Measurement | Before | After | | --- | ---: | ---: | | Focused `examples/bd/dolt` package | 317.815s | 81.958s | | Package improvement | — | 74.2% lower / 3.88x faster | | Race-enabled package | — | 82.413s | | Warm `make test-fast-parallel` observation | 422.84s | 262.84s | A post-rebase/cold-contention run completed successfully in 367.24s. That variance exposed the next critical path—`internal/api` at 191.178s plus cold compilation—so this PR is one measured test-runtime slice, not a claim that every cold run is already below five minutes. ## Verification - `go test -count=1 ./examples/bd/dolt` - `go test -count=1 -race ./examples/bd/dolt` - `make test-fast-parallel` - focused resource-ledger test - `go vet ./...` - `.githooks/pre-commit` - mandatory pre-push fast suite Three delegated reviewers unanimously approved the exact staged diff (`36f170e070af328b220c38e8d991b85e85ad7043c84a27df689581857b5c2690`) for isolation, correctness, test-pyramid fit, performance, and maintainability.
…nhall#4022) ## Summary On a busy town the Dolt `sql-server` sits at ~2 cores doing almost nothing but TCP accept + auth + session-init — connection churn measured at ~41 opens/sec — because every idle nudge-poll tick dials Dolt before it looks at the (non-Dolt) queue. Read `state.json` first; open the Dolt front door only when there's real maintenance work. ## Root cause In the shipped-default legacy nudge mode (`daemon.nudge_dispatcher="legacy"`, `internal/config/config.go:2646`), one `gc nudge poll` sidecar runs per session and ticks every 2s (`defaultNudgePollInterval`, `cmd/gc/cmd_nudge.go:53`). Each idle tick, `claimDueQueuedNudgesForTarget` → `claimDueQueuedNudgesMatching` opened a fresh Dolt store **before** inspecting the queue; its siblings `listQueuedNudges` / `listQueuedNudgesForTarget` / `ackQueuedNudgesWithOutcome` / `releaseQueuedNudgeClaims` did the same. But the queue-of-record is the flock'd `state.json` (`nudgequeue.WithState`), **not** Dolt — the store is opened only to shadow terminal-state beads during recover/prune/terminalize, which are **no-ops on an empty queue**. Each `openNudgeBeadStore` (`nudge_beads.go:37` → `openStoreAtForCity`) dials ~2 server connections (main pool + a `SHOW DATABASES` init probe). So N idle sidecars × 1 open / 2s × ~2 conns is the churn that pins the sql-server at ~2 cores. A prior fix (`TestNudgePollHelpersCloseEveryStoreTheyOpen`) made these helpers *close* what they open — but a balanced open+close every idle tick is itself the churn. The beads library documents the intended contract explicitly ("open once, reuse for the daemon's lifetime"); this per-tick open/close fights it. ## Fix Read `state.json` first and open Dolt only when there's real front-door work. A small `nudgeMaintenanceStore` opens the store lazily on first need — gated by `nudgeQueueHasWork` (non-empty Pending/InFlight/Dead) inside the `withNudgeQueueState` transaction — and closes only a handle it opened. Idle ticks now open **zero** stores; non-empty queues open **exactly once** and run every maintenance pass unchanged. `ack`'s terminal-stamp path uses an idempotent `ensureOpen()`, so the store is present exactly when there are terminal items (which only exist when Pending/InFlight were non-empty). No signature changes to the maintenance/delivery functions, so the store-owns-vs-borrows close contract is untouched. ## Proof - New `TestNudgePollHelpersSkipDoltOpenOnEmptyQueue`: 5 idle ticks × 5 helpers ⇒ **`opens == 0`** (pre-fix `opens == 25`, captured verbatim below). - New `TestNudgePollHelpersOpenOnceWhenQueueHasWork`: each helper opens+closes exactly once on a non-empty queue (no regression). - Existing `TestNudgePollHelpersCloseEveryStoreTheyOpen` (the open/close balance guard) stays green. ``` pre-fix: cmd_nudge_test.go:4800: empty-queue poll opened the Dolt store 25 times, want 0 post-fix: PASS TestNudgePollHelpersSkipDoltOpenOnEmptyQueue / ...OpenOnceWhenQueueHasWork / ...CloseEveryStoreTheyOpen ``` `go build ./...`, `go vet ./cmd/gc/...`, `gofmt -l` clean; `go test ./cmd/gc/ -run Nudge` ok. ## Complementary operational lever `daemon.nudge_dispatcher="supervisor"` replaces the per-session polling sidecars with a single supervised dispatcher and is the other half of the churn story. This PR fixes the shipped **legacy default** so operators aren't forced to flip modes to get a healthy sql-server. ## Risks - A non-empty queue whose items need no bead write (e.g. all Pending still in the future) now opens a store it may not dereference — a deliberately conservative over-open that keeps behavior byte-identical to today and preserves the nil-front invariant (front is only dereferenced while iterating a non-empty slice). - If `openNudgeBeadStore` fails, `front` stays nil exactly as before — unchanged failure semantics. - Follow-ups (not bundled): thread the sidecar's already-open persistent handle (`cmd_nudge.go:642`) into the delivery hot path via a `…WithStore` variant to skip the single non-idle open too; adaptive idle backoff (2s→~30s, reset on `pingNudgeWakeSocket`). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Summary - Add one shared hermetic Codex session-path fixture for API tests. - Redirect the always-merged default `HOME/.codex/sessions` root into test-owned storage. - Preserve a distinct configured search path and every existing HTTP, transcript, parser, and SSE assertion. - Leave production discovery behavior, timeouts, and coverage unchanged. ## Root cause Codex transcript discovery intentionally merges the default session root with configured search paths. Three API fixtures supplied a temporary configured root but inherited the real host default. On this shared host that meant scanning an 814 MB tree with 6,250 rollout files and eight account symlinks. The stream request had a 500 ms context timeout, but synchronous filesystem discovery could not observe that cancellation and consumed 146 seconds. ## Measurements | Scope | Before | After | Improvement | | --- | ---: | ---: | ---: | | Stream error-frame test body | 146.17s | 0.50s | 99.7% lower; 292x faster | | `internal/api` package | 215.381s | 70.301s | 67.4% lower; 3.06x faster | | Machine-aware fast suite sample | 367.24s | 247.48s | 32.6% lower; 1.48x faster | The broad fast-suite sample is now 4m07s, leaving about 52 seconds of margin under the five-minute feedback target. It is one observed sample, not a p95 claim. ## Validation - All five Codex API fixtures: 0.695s - Focused Codex API fixtures with `-race`: pass - Full `internal/api`: pass - Resource-census policy: pass - CI topology policy: pass - `go vet ./...`: pass - `make test-fast-parallel`: pass in 247.48s - Active pre-commit and pre-push hooks: pass - Three-reviewer delegated council: unanimous approval of staged SHA-256 `6b88a769c1b094baa405adda851a136ef7ede09e43d58258a57f1a8af9f45e34` ## Non-goals - No production transcript-search semantic changes. - No skipped assertions, shortened waits, or timeout masking. - No real provider or end-to-end coverage moved into the fast lane. Tracking: `ga-80po0c.8`.
## Summary - Replace the pre-push hook's fixed three-job cap with one canonical machine-aware concurrency policy. - Use the same policy from `make test-fast-parallel` and direct `scripts/test-local-parallel` runs. - Preserve explicit `LOCAL_TEST_JOBS` overrides and the complete fast-suite inventory. ## Policy - Automatic jobs are bounded by detected CPUs, 4 GiB of available memory per job, and a ceiling of 16. - Linux uses `MemAvailable` plus the tightest finite cgroup v1/v2 ancestor budget, including hybrid v2-to-v1 fallback. - macOS reserves 4 GiB before calculating capacity. - Unknown memory falls back to three jobs; exhausted memory still permits one job. ## Performance | Gate | Wall time | Result | | --- | ---: | --- | | Fixed three-job pre-push baseline | ~383s | Over five minutes | | Final uncached machine-aware suite | 231.55s | 3m51.55s, all eight jobs passed | That removes about 151 seconds from the gate, a roughly 40% improvement. No tests were removed, skipped, or reclassified. ## Verification - Actual `.githooks/pre-push`: all eight jobs passed with `LOCAL_TEST_JOBS=16` - `go test ./scripts -count=1` - `go test ./internal/testpolicy/resourcecensus -run TestRepositoryLedgerMatchesCensusAndDocumentation -count=1` - Resource ratchets unchanged at 523 / 396 / 394 subprocess call sites - `make test-ci-policy` - `go test ./test/docsync -count=1` - `go vet ./...` - Bash syntax and ShellCheck - Three-reviewer delegated council unanimously approved staged hash `073bfdda7821933af3f504662c4d6b4af7e8d1662d3948fc619de15bd618165e` Tracker: `ga-80po0c.9`
## Summary - Add a versioned, deterministic JSON timing-history snapshot through `test-timing-summary --format=json`. - Reuse the existing strict schema-v1 loader, duplicate detector, runner-profile partitioning, and statistical calculations. - Preserve the default Markdown report byte-for-byte through the same canonical snapshot model. ## Why Historical shard planning needs a machine-readable contract before protected persistence or planner activation. Without one, the next phase would need a second parser or prose scraping, splitting timing policy across implementations. ## Contract - Groups top-level runnable tests by comparable job, variant, runner label, OS, architecture, and CPU count. - Retains every successful observation with tested SHA and exact artifact identity while keeping failure and skip counts. - Emits p50, p75, p95, population variance, and explicit authority thresholds: p75 at 5 successes and p95 at 20. - Keeps zero-success units with `null` statistics and `successful_observations: []`. - Sorts opaque run IDs and attempts as raw strings in a documented canonical tuple. - Rejects duplicate conflicts and unit-identity conflicts, including conflicts across runner profiles. ## Compatibility and scope - Default Markdown output is locked by an exact golden assertion. - Schema v1 cannot prove event, ref, conclusion, or chronological recency; the documentation states those limits explicitly. - This snapshot is workflow-neutral input. It does not add CI permissions, protected persistence, retention, aliases, shard selection, or planner authority. ## Performance - Affected package wall time: 0.40s after versus 0.36s baseline. - Isolated fast suite: 235.17s, under the 5-minute policy target. - Push hook: all eight machine-aware fast jobs passed. - Resource-census ratchets remain unchanged. ## Verification - `go test ./internal/testpolicy/timingsummary -run TestBuildSnapshot -count=20` - `go test -race ./internal/testpolicy/timingsummary -count=1` - `go test ./scripts -count=1` - `go test -count=1 ./internal/testpolicy/resourcecensus -run ^TestRepositoryLedgerMatchesCensusAndDocumentation$` - `go test ./test/docsync -count=1` - `make test-ci-policy` - `go vet ./...` - `make test-fast-parallel` - Three delegated council reviewers unanimously approved exact binary diff SHA-256 `90a1ecebc660ad7090c41beecfc999eeb2704ee4b69fb6123318e4082ff137db`. Tracker: `ga-80po0c.4.1`
## Summary - Persist normalized timing evidence by run, artifact, unit, and outcome. - Merge identical artifacts idempotently and reject conflicts before changing stored bytes. - Retain whole run cohorts by parsed completion time and rebuild the existing snapshot after pruning. - Add an explicit mutation CLI mode without changing byte-exact read-only Markdown or JSON output. ## Why The reporting snapshot intentionally collapses failed and skipped observations into counts. That makes overlapping snapshots unsafe to merge and would double-count evidence. This storage boundary retains artifact identity for pass, fail, and skip rows so later default-branch automation can build trustworthy timing history for shard planning. ## Trust boundary This change validates the run-envelope shape and checks each artifact's workflow, run ID, run attempt, and tested SHA. It deliberately does not: - authenticate the supplied envelope; - prove that every expected shard is present; - serialize concurrent writers; - publish a protected ci-metrics branch; or - activate timing-based shard planning. Those responsibilities remain in the follow-up trusted workflow. ## Performance evidence - One real 12-artifact cohort with 14,795 rows: 0.78s update, 62 MB peak RSS, 4.56 MB database. - Fifty retained cohorts: 3.19s latest update, 49.2 MB database, 586 MB peak RSS. - The focused timing-summary test package completes in about 0.06s. - No required-check topology or PR-path test selection changes in this PR. ## Verification - go test ./internal/testpolicy/timingsummary -count=20 - go test -race ./internal/testpolicy/timingsummary -count=1 - go test ./scripts -count=1 - go test ./test/docsync -count=1 - make test-ci-policy - go vet ./... - make test-fast-parallel - .githooks/pre-commit - Three independent delegated reviewers unanimously approved frozen diff fb5f9f55967aa5c11d5c4c0a007e7b6a16abb7f833b2ea52d79d96cbdba98c61 ## Tracking Bead: ga-80po0c.4.2
…all#4068) ## Problem `doRelaunchSession` (internal/runtime/tmux/adapter.go) respawns the agent into `cfg.WorkDir` without running `pre_start`. The fresh-start path (`doStartSession`) runs `runPreStart` and treats failures as fatal, with this rationale in the code: > Failures are fatal because launching into an unprepared workDir can point agents at the wrong repo or skip required bootstrap state entirely. The relaunch path skips that step entirely, so pool warm-box re-homes (launch-only fingerprint changes, upstream switches, etc.) start agents in work dirs that never got their `pre_start` preparation. For configs where `pre_start` provisions the per-session work dir (e.g. a git worktree setup script), the relaunched agent lands in a directory that doesn't exist or points at the wrong checkout. ## Fix One hunk: call `runPreStart` at the top of `doRelaunchSession`, mirroring `doStartSession`'s fatal-on-failure semantics, plus a context-cancellation check after it (matching the function's existing style of checking `ctx.Err()` between steps). `pre_start` commands are required to be idempotent already (they re-run on every fresh start of the same config), so running them again on relaunch is safe by construction. ## Testing - `go test ./internal/runtime/tmux/` passes with the change (verified on top of both dd8730a and current main 0ec2df8). - Running in production on a real deployment since 2026-07-08: a gc city with a pool whose `pre_start` runs a git-worktree setup script. Before the change, warm-box relaunches re-homed agents into unprepared work dirs; after it, every relaunch runs the setup script first and lands in a prepared dir (verified live via the script's debug log). --------- Co-authored-by: Klas Hesselman <211416696+swedeinasia-flow@users.noreply.github.com>
## Why `TestPrepareWaitWakeState_ResolvesRigDependencyBeads` was paying for a complete managed-Dolt city and rig even though its responsibility is wait-readiness behavior. Its baseline test body took **80.68s**. This change moves that behavior proof onto explicit in-memory city and rig stores. One separate test still owns the real managed-Dolt composition boundary. ## What changed - Added a narrow, read-only dependency seam owned by the wait consumer. - Routed controller wait reads through the existing `storeref.Resolve` split-store resolver. - Supplied the controller's already-open city and rig stores directly and deterministically. - Rewrote the behavior test around distinct `gcg-*` city IDs and `ga-*` rig IDs. - Covered closed, open, missing, and hard-read-error outcomes without filesystem, process, network, polling, or wall-clock dependencies. ## Test ownership | Invariant | Owner after this change | | --- | --- | | Split-store dependency routing and readiness behavior | `TestPrepareWaitWakeState_ResolvesRigDependencyBeads` using `MemStore` | | Closed/open/missing/hard-error policy | The same small table-driven test | | Managed-Dolt provider composition across city and rig stores | Existing `TestCmdSessionWait_AllowsRigDependencyBeads` | ## Measured impact | Measurement | Before | After | | --- | ---: | ---: | | Test body | 80.68s | 0.00s reported | | Package test time | 82.282s | 1.937s | | Focused command wall time | 130.19s | 42.55s | The focused command still pays the large `cmd/gc` compile/link cost; the behavioral test itself no longer starts managed Dolt. The retained real-provider composition test took 70.47s and remains intentionally singular. ## TDD evidence - **RED:** the focused compile failed before production code existed for `prepareWaitWakeStateWithSnapshot` and `waitDependencyStoreSet`. - **GREEN:** all four in-memory cases pass, including `-race -count=20`. - **REFACTOR:** controller composition now passes explicit stores through the same narrow seam exercised by the small test. ## Verification - [x] Focused wait tests - [x] New behavior test with `-race -count=20` - [x] `internal/storeref` with `-race -count=20` - [x] `make test-fast-parallel` - [x] `go vet ./...` - [x] `.githooks/pre-commit` - [x] Three independent delegated reviewers approved the exact patch hash The broader process sweep passed shards 1, 2, and 4. Shards 3, 5, and 6 reached an unrelated host-tool limitation: `/home/ubuntu/.local/bin/bd` was built with `CGO_ENABLED=0` and cannot initialize embedded Dolt. The deterministic failure evidence is in `/data/tmp/gc-local-tests.YVN5YA`; retrying it would not validate this patch.
…e-location evaluation, order tz, [workspace] timezone (gastownhall#4080) ### Symptom On a TZ-pinned (America/New_York) box, ET-anchored cron orders fired at the **UTC reading** of their slot — a `"30 19 * * *"` digest dispatched at 19:30**Z** (15:30 ET) — and then fired **again** at the real ET slot: two fires per day. The behavior survived restarts and was insensitive to `TZ` being correctly present in the supervisor env. ### Root cause `checkCron` mixed two time domains. The live match (a) evaluates cron fields in `now`'s location (process TZ — correct). The catch-up scan (b) — added by 0ce89a0 (gastownhall#2721, v1.3.0) — anchors at the last-run bead's `CreatedAt`, which the doltlite read store **always returns UTC-located** (`parseTimeString`'s zone-less `time.Parse` layouts). The scan therefore walked minutes on the UTC wall clock and matched `hour 19` at 19:30Z, env-independently; the lookback floor meanwhile derived from `now` (ET), so the function mixed locations even within path (b). After the early fire, path (a) matched again at the real ET slot → double fire. **Latent since v1.3.0**: it arms only when lastRun is non-zero on a non-UTC box — on UTC boxes both domains coincide, which is why UTC-box CI never caught it. ### Fix - `checkCron` resolves **one explicit location** up front and normalizes both domains into it: `loc := resolveOrderLocation(order, now)`; `now = now.In(loc)`; `last = last.In(loc)`. Resolution: order `tz` → city-default `[workspace] timezone` (stamped onto no-tz orders at scan time in `orderdiscovery.ScanAll`, keeping `CheckTrigger`'s signature stable) → `now.Location()`, which for the live dispatcher is `time.Now()`'s process-local zone. - New spec field `tz` on orders (`[order] tz = "America/New_York"`) and city default `[workspace] timezone`. **Bad zone names fail loudly**: order validation rejects an invalid `tz` at load; a bad workspace timezone fails order discovery outright; `checkCron` fails closed (`bad tz: …`) rather than silently falling back. - Defense-in-depth: `TZ` joins `ProviderProcessPassthroughEnv`, so gc-spawned sessions inherit the host zone — closing the constructed-env vector for any in-session `gc order check`. ### DST policy (explicit + tested, in the resolved location) - **Fall-back** (tested on 2026-11-01 US): the repeated hour yields two instants with one wall-clock reading; an order fires **at most once per wall-clock slot** — dedupe by wall-clock date+HH:MM against lastRun, in both the live match and the catch-up scan. - **Spring-forward** (tested on 2027-03-14 US): schedule minutes inside the nonexistent hour cannot match a real instant; the catch-up scan detects the offset jump and fires **once at the first real minute after the gap** (e.g. `"30 2 * * *"` fires at 03:00 EDT) — nearest-following-minute, like classic vixie-cron, rather than silently skipping the day. ### Back-compat - Orders without `tz` in cities without `[workspace] timezone` keep process-local semantics — byte-for-byte the previous live-match behavior. UTC boxes see zero change. - Deploy-moment note (one-time): an order whose *last* fire was an early UTC-reading fire will correctly fire again at its real local slot on the first post-fix day — one extra fire relative to "already ran today", at the slot that was always intended. Pre-fix it would have double-fired at that moment anyway. - `[orders.overrides]` intentionally does not grow a `tz` knob in this PR (minimal surface; trivial follow-up if wanted). ### Proof Pre-fix (repro at 0ec2df8), verbatim: ``` --- FAIL: TestTZPrefix_CatchupFiresAtUTCReading_PM — due=true reason="cron: caught up missed occurrence" at 2026-07-07T19:30:30Z (should not fire until 19:30 ET / 23:30Z) --- FAIL: TestTZPrefix_CatchupFiresAtUTCReading_AM — due=true ... at 2026-07-07T07:00:19Z (the exact live-fire signature) --- FAIL: TestTZPrefix_ExactlyOneFirePerDay — 2 fires in one day, want exactly 1: [2026-07-07T15:30:00-04:00 (cron: caught up missed occurrence) 2026-07-07T19:30:00-04:00 (cron: schedule matched)] ``` Post-fix: all promoted cases pass — early-fire regressions, correct-slot control, exactly-one-fire-per-slot across a simulated day of ticks (with a doltlite-shaped UTC store round-trip, under both UTC-located and ET-located caller clocks), spec-tz env independence with UTC-located `now`s, multi-day catch-up in-zone, both DST transitions incl. full transition-night simulations, and fail-closed bad-tz. Full `./internal/orders/... ./internal/orderdiscovery/... ./internal/config/... ./internal/processenv/... ./internal/api/... ./cmd/gc/... ./test/docsync/...` green; also green under `TZ=UTC` and `TZ=Australia/Lord_Howe` (30-minute DST zone). Build/vet/gofmt clean; reference docs regenerated (`go run ./cmd/genschema`). Refs gastownhall#2721 (0ce89a0 — origin of the catch-up scan). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Why `TestCmdSessionList_ManagedExecLifecycleProviderReadsSessions` starts a complete managed-Dolt city to prove two things that already have clearer owners: - fast file-backed tests own session-list projection and rendering; - one retained managed CLI test owns real `exec:gc-beads-bd` city composition. Removing this duplicate preserves both invariants while eliminating another managed-Dolt startup from the process shard. ## Invariant ownership | Invariant | Owner after this change | | --- | --- | | Session-list projection, table rendering, and JSON behavior | Fast `cmdSessionList` tests in `cmd_session_test.go` | | Generic exec-store Get/List behavior | `internal/beads/exec` conformance | | Real city-scoped `exec:gc-beads-bd` CLI composition | `TestCmdMailInbox_ManagedExecLifecycleProviderReadsInbox` | | Managed rig raw/provider consistency | Existing managed `cmd_bd` tests | The Dolt regression audit now names those owners and its verification command no longer references the deleted test. ## Measured impact | Measurement | Removed test body | | --- | ---: | | Exact-main local run | 63.03s | | Prior CI shard sample | 21.95s | The expected benefit is the removed test's runtime in whichever shard owns it; this PR does not claim an equivalent whole-shard reduction. The retained real managed-provider owner passed locally in 80.69s and remains intentionally singular. ## Policy ratchet The deleted test contained three `t.Setenv` calls. The checked environment-debt baselines now bank that exact reduction: | Census | Before | After | | --- | ---: | ---: | | Small `cmd/gc` environment calls | 4,352 | 4,349 | | Source `cmd/gc` environment calls | 4,358 | 4,355 | | Files | 200 | 200 | **RED:** the first fast-suite run failed because all four checked representations still held the higher baseline. **GREEN:** the bootstrap policy, TOML ledger, and `TESTING.md` rows were lowered together; the focused census and full fast suite then passed. ## Verification - [x] Fast session-list owners under `-race -count=20` - [x] `internal/beads/exec` contract subset under `-race -count=20` - [x] Retained real managed-provider composition proof - [x] Resource-census/document synchronization test - [x] `make test-fast-parallel` - [x] `go vet ./...` - [x] `.githooks/pre-commit` - [x] Three independent delegated reviewers approved the exact staged patch
) Summary:\n- document GET and POST /gc/v0/device as the server-rendered device approval surface\n- make HTML/form browser routes the explicit exception to the JSON wire rule\n- correct verification URI examples and discovery/conformance entries\n\nVerification:\n- make check-docs\n- .githooks/pre-commit\n- pre-push fast suite (all 8 jobs passed)\n\nTracking: ga-zly6sf.3
Resolves 25 conflicts + several semantic-merge gaps, preserving fork resilience/perf features atop upstream. Generated API schemas/genclient regenerated from merged source (union: fork degraded/gc_bd_inflight + upstream conditional-writes/waits). Key judgment calls: - order_dispatch.go: adopt upstream orders.Store.HasOpenWork front door (canonical object-model route; equivalent bounded semantics), drop fork hasOpenOrderWorkFlat + order_gate_flat.go (superseded upstream) - native_dolt_store.go: union upstream reconnect machinery + fork projectID - build_desired_state.go: keep fork single-scope-read collapse via upstream readyDemandCache Two regressions found in review + fixed (both compiled clean but broke safety contracts): - session_reconciler.go: fork list-liveness fast path bypassed upstream's new fail-closed guard (livenessErr stayed nil) — fast path now only when the session is visible/alive, else probe authoritatively (TestReconcileOrphanCloseFailsClosedOnLivenessError) - bd_env.go: early loadCityConfig ran before the managed-recovery spawn and broke its cancellation contract — cfg load + canary moved after recovery (TestNativeDoltOpenEnvForScopeContextCancelsManagedRecovery) go build ./... passes; go vet clean; internal/config + internal/api pass; targeted cmd/gc (dispatch/reconciler/native/budget/phantom) passes.
| if err != nil { | ||
| return nil, fmt.Errorf("productmetrics: canonicalize signed pause: %w", err) | ||
| } | ||
| message := make([]byte, 0, len(pauseDomainPrefix)+len(encoded)) |
…ightly hash - Regenerate gc-supervisor-client TS types (index/types/zod.gen.ts) that lagged the merged openapi — fixes 'Preflight / generated artifacts'. - Update expectedNightlyExecutionHash to accept upstream's merged nightly workflow execution shape — fixes cipolicy TestCurrentWorkflowsMatchPolicy.
… discriminator The prior fail-closed fix keyed the snapshot fast path on visibleSet[name], which forced a per-session probe for every ABSENT session — breaking the phantom-reap O(1) contract (TestReconcileSessionBeads_UsesVisibilitySnapshot ForOrphanedSessions expects 0 IsRunning calls). A present runtime provider (sp!=nil) with a usable list IS an authoritative liveness snapshot, so absence means dead — decide from visibleSet with no probe. Only when sp==nil (nothing observed) or the list errored do we probe, surfacing the liveness error for the fail-closed guards. Satisfies both the phantom-reap perf test and TestReconcileOrphanCloseFailsClosedOnLivenessError.
…tability, and build-desired-state partial fixture Resync integration fixes for CI green on the 237-commit upstream merge: - Product-metrics command census: add the fork's `gc provider`, `gc provider quota`, `gc provider rotate-key`, and `gc beads state` commands to productmetrics_command_census.json and regenerate (metrics_census_gen.go, command_ids_gen.go, schema enum). The upstream census manifest never knew the fork's live commands, so census validation failed and applyProductionProductMetricsCommandCensus returned early — leaving command annotations nil (classifier panic) and command IDs unrecorded (lifecycle early-JSON/bad-flag tests). - gc-env read baseline: add GC_BEADS_NATIVE_STORE_CANARY (config canary var) and GC_HOOK_STORE_UNAVAILABLE (hook stderr contract token) to the golden. Both are deliberate fork additions absent from the upstream golden the resync imported. - gc beads state: route buildBeadsStateLiveSets through the sessions class front door (session.Store.ListAll) instead of cracking raw session beads, satisfying TestTypedClassCodecCensusRatchet. Uses Info's raw mirrors (SessionNameMetadata, MetadataState) to preserve exact behavior. - hook stdin-drain tests: resolve the wrapped `true` binary via exec.LookPath instead of hardcoding /bin/true, which is absent on the macOS (mac-regression) CI runners. - build_desired_state_test: revert controllerDemandPartialStore to the upstream simple fixture (partial on every unfiltered controller-demand read). The merge kept upstream's Test B (StoreQueryPartial=true, which matches the adopted upstream readyDemandCache shared-read production) but the fork's ordinal fixture that masked the assigned-work read as clean; the simple fixture models the real production and keeps ScaleCheckPartialPoolBlocksNewCreates passing (its retention is gated on the scoped PoolScaleCheckPartial, not global StoreQueryPartial).
…ed fork commands The census now includes gc provider-quota/provider-rotate-key/beads-state (ids 195-197), legitimately growing the production command catalog to 193. Update the RoundTripsWithoutExpandingProduction ratchet count to match — consistent with the census-matches-builtins tests that require them.
…e census TestMergeOracleFieldCoverage (gascity_native_beads) requires every CachingStore/CacheStats field be compared or justified-excluded. The fork's circuitTripped/availabilityGate/unavailableSkipLogged/degradedReads and CacheStats.DegradedReads are resilience/read-path state orthogonal to the reconcile bead-state end-state — justified-exclude them.
…ync growth The resync legitimately grew subprocess/environment/http-test-server/ fixed-sleep/slow-process-gate usage past the ratchet baselines. Bumped all three synchronized tiers: bootstrapPolicy (census.go), the checked ledger (test/test-resources.toml), and the rendered TESTING.md table.
- TestHasOpenWorkStrictPropagates{OpenScan,AncestorGet}Error: the gate now
routes through the orders.Store.HasOpenWork front door (label list +
descendant walk), not the deleted flat gate's whole-scope scan/ancestor
Get. Retarget the fault injection to the front door's real reads and
assert its fail-closed messages — the safety contract is preserved.
- TestCollectAssignedWorkBeadsCachedMatchesUncached: the single-scope-read
collapse now applies to the direct path too (via collectAssignedWorkBeads),
not only the cache; assert both paths do <=1 backing read and still agree.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merges upstream
gastownhall/gascitymain (237 commits) into our fork.Conflicts resolved (25) + semantic-merge gaps
Fork resilience/perf features preserved atop upstream. Generated API schemas + genclient regenerated from merged source (union: fork
degraded/gc_bd_inflight+ upstream conditional-writes/waits), not hand-merged.Key judgment calls:
orders.Store.HasOpenWorkfront door (canonical object-model route the CI invariant requires; equivalent bounded semantics), dropping the fork'shasOpenOrderWorkFlat+order_gate_flat.go(superseded upstream by the membership index — same finding that closed perf(orders): flat-membership open-work gate, O(scope) not O(tree) gastownhall/gascity#3315).projectID.readyDemandCache.Two regressions found in review and fixed
Both compiled clean but silently broke safety contracts:
livenessErrstayed nil), so an orphan could be closed on an uncertain observation. Fixed: fast path only when the session is visible/alive; otherwise probe authoritatively so the fail-closed guards fire. (TestReconcileOrphanCloseFailsClosedOnLivenessError)loadCityConfigran before the managed-recovery spawn and broke its cancellation contract (leaked a dolt sql-server). Fixed: cfg load + native-store canary moved after recovery. (TestNativeDoltOpenEnvForScopeContextCancelsManagedRecovery)Validation
go build ./...passes,go vetcleaninternal/config+internal/apipass; targetedcmd/gc(dispatch/reconciler/native/budget/phantom/trace) passescmd/gcsharded suite runs in CI (too large for a singlego testlocally)